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
64 changes: 64 additions & 0 deletions MattSourceGenHelpers.GeneratorTests/GeneratorDiagnosticsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -419,4 +419,68 @@ static IMethodImplementationGenerator GetValue_Generator() =>
Assert.That(diagnostics.Value.Where(d => d.Severity == DiagnosticSeverity.Error), Is.Empty,
"Fluent generator with a CompilationReference for abstractions should produce no error diagnostics");
}

// -----------------------------------------------------------------------
// MSGH001 – targeting an existing but non-partial method
// -----------------------------------------------------------------------

[Test]
public void GeneratesMethod_TargetingNonPartialMethod_EmitsMSGH001()
{
string source = """
using MattSourceGenHelpers.Abstractions;

namespace TestNamespace;

public partial class MyClass
{
public string NonPartialMethod() => "hello";

[GeneratesMethod("NonPartialMethod")]
private static string MyGenerator() => "world";
}
""";

ImmutableArray<Diagnostic> diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source);

Diagnostic? msgh001 = diagnostics.FirstOrDefault(d => d.Id == "MSGH001");
Assert.That(msgh001, Is.Not.Null,
"Expected MSGH001 when targeting an existing but non-partial method");
Assert.That(msgh001!.GetMessage(), Does.Contain("NonPartialMethod"),
"Error message should mention the non-partial method name");
}

// -----------------------------------------------------------------------
// Bool switch parameter – valid configuration
// -----------------------------------------------------------------------

[Test]
public void GeneratesMethod_SwitchCaseWithBoolParameter_ValidConfiguration_ProducesNoDiagnosticErrors()
{
string source = """
using MattSourceGenHelpers.Abstractions;
using System;

namespace TestNamespace;

public static partial class MyClass
{
public static partial string GetLabel(bool flag);

[GeneratesMethod(nameof(GetLabel))]
[SwitchCase(arg1: true)]
[SwitchCase(arg1: false)]
private static string GetLabel_Generator(bool flag) => flag ? "Yes" : "No";

[GeneratesMethod(nameof(GetLabel))]
[SwitchDefault]
private static Func<bool, string> GetLabel_Default() => _ => "Unknown";
}
""";

ImmutableArray<Diagnostic> diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source);

Assert.That(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error), Is.Empty,
"Valid bool switch case generator should produce no error diagnostics");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,7 @@ private static string GenerateSwitchMethodSource(
ITypeSymbol? parameterType = partialMethod.Parameters.Length > 0 ? partialMethod.Parameters[0].Type : null;
foreach ((object key, string value) in cases)
{
string formattedKey = parameterType?.TypeKind == TypeKind.Enum
? $"{parameterType.ToDisplayString()}.{key}"
: key.ToString()!;
string formattedKey = FormatKeyAsCSharpLiteral(key, parameterType);
builder.AppendLine($" case {formattedKey}: return {value};");
}

Expand Down Expand Up @@ -318,4 +316,20 @@ internal static string FormatValueAsCSharpLiteral(string? value, ITypeSymbol ret
_ => value
};
}

private static string FormatKeyAsCSharpLiteral(object key, ITypeSymbol? parameterType)
{
if (parameterType?.TypeKind == TypeKind.Enum)
{
return $"{parameterType.ToDisplayString()}.{key}";
}

return key switch
{
bool b => b ? "true" : "false",
// SyntaxFactory.Literal handles escaping and quoting (e.g. "hello" → "\"hello\"")
string s => SyntaxFactory.Literal(s).Text,
Comment on lines +326 to +331

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FormatKeyAsCSharpLiteral fixes bool/string, but char switch keys will still generate invalid C# (e.g., [SwitchCase(arg1: 'A')] will end up as case A: because the fallback uses key.ToString()). Consider handling char (and potentially other primitive literal types) similarly to FormatValueAsCSharpLiteral, e.g., use SyntaxFactory.Literal(c).Text when key is char or when parameterType.SpecialType == System_Char.

Suggested change
return key switch
{
bool b => b ? "true" : "false",
// SyntaxFactory.Literal handles escaping and quoting (e.g. "hello" → "\"hello\"")
string s => SyntaxFactory.Literal(s).Text,
// Handle char keys explicitly to produce valid C# char literals (e.g., 'A')
if (parameterType?.SpecialType == SpecialType.System_Char)
{
if (key is char c)
{
return SyntaxFactory.Literal(c).Text;
}
var s = key.ToString() ?? string.Empty;
if (s.Length == 1)
{
return SyntaxFactory.Literal(s[0]).Text;
}
// Fallback: treat as string literal; this should not normally be hit for valid char keys
return SyntaxFactory.Literal(s).Text;
}
return key switch
{
bool b => b ? "true" : "false",
// SyntaxFactory.Literal handles escaping and quoting (e.g. "hello" → "\"hello\"")
string s => SyntaxFactory.Literal(s).Text,
char c => SyntaxFactory.Literal(c).Text,

Copilot uses AI. Check for mistakes.
_ => key.ToString()!
Comment on lines +329 to +332

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new string s => SyntaxFactory.Literal(s).Text branch in FormatKeyAsCSharpLiteral isn’t covered by a test (current new integration coverage only exercises the bool branch). Consider adding an end-to-end test similar to BoolSwitchKeyTests but with a string switch parameter (including a value that needs escaping) to prevent regressions in quoting/escaping.

Copilot uses AI. Check for mistakes.
};
}
}
54 changes: 54 additions & 0 deletions MattSourceGenHelpers.Tests/BoolSwitchKeyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using MattSourceGenHelpers.Abstractions;
// ReSharper disable ConvertClosureToMethodGroup

namespace MattSourceGenHelpers.Tests;

[TestFixture]
public class BoolSwitchKeyTests
{
[TestCase(true, "Yes")]
[TestCase(false, "No")]
public void BoolSwitchKey_ProducesExpectedRuntimeOutput(bool flag, string expected)
{
string result = TestBoolSwitchClass.GetBoolLabel(flag);
Assert.That(result, Is.EqualTo(expected));
}

[Test]
public void BoolSwitchKey_ProducesExpectedGeneratedCode()
{
string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("TestBoolSwitchClass_GetBoolLabel.g.cs");
string expectedCode = """
namespace MattSourceGenHelpers.Tests;

static partial class TestBoolSwitchClass
{
public static partial string GetBoolLabel(bool flag)
{
switch (flag)
{
case true: return "Yes";
case false: return "No";
default: return "Unknown";
}
}
}
""".ReplaceLineEndings("\n").TrimEnd();

Assert.That(generatedCode, Is.EqualTo(expectedCode));
}
}

public static partial class TestBoolSwitchClass
{
public static partial string GetBoolLabel(bool flag);

[GeneratesMethod(nameof(GetBoolLabel))]
[SwitchCase(arg1: true)]
[SwitchCase(arg1: false)]
static string GetBoolLabel_Generator(bool flag) => flag ? "Yes" : "No";

[GeneratesMethod(nameof(GetBoolLabel))]
[SwitchDefault]
static Func<bool, string> GetBoolLabel_Default() => _ => "Unknown";
}
159 changes: 159 additions & 0 deletions MattSourceGenHelpers.Tests/EdgeCaseSimplePatternTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using MattSourceGenHelpers.Abstractions;

namespace MattSourceGenHelpers.Tests;

[TestFixture]
public class EdgeCaseSimplePatternTests
{
// -----------------------------------------------------------------------
// Void return type
// -----------------------------------------------------------------------

[Test]
public void VoidReturnGenerator_MethodIsCallable()
{
TestVoidClass instance = new TestVoidClass();
Assert.DoesNotThrow(() => instance.DoSomething());
}

[Test]
public void VoidReturnGenerator_ProducesExpectedGeneratedCode()
{
string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("TestVoidClass_DoSomething.g.cs");
string expectedCode = """
namespace MattSourceGenHelpers.Tests;

partial class TestVoidClass
{
public partial void DoSomething()
{
}
}
""".ReplaceLineEndings("\n").TrimEnd();

Assert.That(generatedCode, Is.EqualTo(expectedCode));
}

// -----------------------------------------------------------------------
// Bool return type
// -----------------------------------------------------------------------

[Test]
public void BoolReturnGenerator_ProducesExpectedRuntimeOutput()
{
bool result = TestBoolReturnClass.IsEnabled();
Assert.That(result, Is.True);
}

[Test]
public void BoolReturnGenerator_ProducesExpectedGeneratedCode()
{
string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("TestBoolReturnClass_IsEnabled.g.cs");
string expectedCode = """
namespace MattSourceGenHelpers.Tests;

static partial class TestBoolReturnClass
{
public static partial bool IsEnabled()
{
return true;
}
}
""".ReplaceLineEndings("\n").TrimEnd();

Assert.That(generatedCode, Is.EqualTo(expectedCode));
}

// -----------------------------------------------------------------------
// Char return type
// -----------------------------------------------------------------------

[Test]
public void CharReturnGenerator_ProducesExpectedRuntimeOutput()
{
char result = TestCharReturnClass.GetSymbol();
Assert.That(result, Is.EqualTo('A'));
}

[Test]
public void CharReturnGenerator_ProducesExpectedGeneratedCode()
{
string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("TestCharReturnClass_GetSymbol.g.cs");
string expectedCode = """
namespace MattSourceGenHelpers.Tests;

static partial class TestCharReturnClass
{
public static partial char GetSymbol()
{
return 'A';
}
}
""".ReplaceLineEndings("\n").TrimEnd();

Assert.That(generatedCode, Is.EqualTo(expectedCode));
}

// -----------------------------------------------------------------------
// Internal method accessibility
// -----------------------------------------------------------------------

[Test]
public void InternalMethodGenerator_ProducesExpectedRuntimeOutput()
{
TestInternalMethodClass instance = new TestInternalMethodClass();
string result = instance.GetValue();
Assert.That(result, Is.EqualTo("internal_value"));
}

[Test]
public void InternalMethodGenerator_ProducesExpectedGeneratedCode()
{
string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("TestInternalMethodClass_GetValue.g.cs");
string expectedCode = """
namespace MattSourceGenHelpers.Tests;

partial class TestInternalMethodClass
{
internal partial string GetValue()
{
return "internal_value";
}
}
""".ReplaceLineEndings("\n").TrimEnd();

Assert.That(generatedCode, Is.EqualTo(expectedCode));
}
}

public partial class TestVoidClass
{
public partial void DoSomething();

[GeneratesMethod(nameof(DoSomething))]
private static void DoSomething_Generator() { }
}

public static partial class TestBoolReturnClass
{
public static partial bool IsEnabled();

[GeneratesMethod(nameof(IsEnabled))]
private static bool IsEnabled_Generator() => true;
}

public static partial class TestCharReturnClass
{
public static partial char GetSymbol();

[GeneratesMethod(nameof(GetSymbol))]
private static char GetSymbol_Generator() => 'A';
}

public partial class TestInternalMethodClass
{
internal partial string GetValue();

[GeneratesMethod(nameof(GetValue))]
private static string GetValue_Generator() => "internal_value";
}