Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
117 changes: 117 additions & 0 deletions TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,123 @@ 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();

// C# 10+ named arguments on [Obsolete] (DiagnosticId, UrlFormat) must be
// preserved on the generated forward so consumers using
// `#pragma warning disable CUSTOM001` continue to suppress the mock's
// call site, not just the source interface's.
[Obsolete("Replaced", DiagnosticId = "CUSTOM001", UrlFormat = "https://example.test/{0}")]
string? WithDiagnosticId();
}

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>();
}
}
""";

AssertGeneratedCodeHasNoObsoleteWarnings(source);
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
68 changes: 64 additions & 4 deletions TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,67 @@ 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}");
}
}

/// <summary>
/// Compiles the generator output and asserts no CS0612 (obsolete, no message), CS0618
/// (obsolete with message), or CS0672 ('override missing Obsolete') warnings are emitted.
/// Direct compile-time guard for the [Obsolete]-propagation portion of #5626. Proves
/// the snapshot's [Obsolete] attribute placement actually suppresses the warnings, not
/// just appears in the generated text.
/// </summary>
protected static void AssertGeneratedCodeHasNoObsoleteWarnings(
string source,
IEnumerable<MetadataReference>? additionalReferences = null,
CSharpParseOptions? parseOptions = null)
{
var (_, diagnostics) = RunGeneratorWithCompilationDiagnostics(source, additionalReferences, parseOptions);
var obsoleteWarnings = diagnostics
.Where(d => d.Severity >= DiagnosticSeverity.Warning
&& (string.Equals(d.Id, "CS0612", StringComparison.Ordinal)
|| string.Equals(d.Id, "CS0618", StringComparison.Ordinal)
|| string.Equals(d.Id, "CS0672", StringComparison.Ordinal)))
.ToList();
if (obsoleteWarnings.Count > 0)
{
var messages = string.Join(Environment.NewLine, obsoleteWarnings.Select(d => d.ToString()));
throw new InvalidOperationException(
$"Generated code emits {obsoleteWarnings.Count} obsolete 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 +217,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 +231,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
Loading