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
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,18 @@ public static IReadOnlyList<ParameterProvider> RenameAndCloneParameters(
/// <param name="customSignature">The customer's partial declaration signature.</param>
/// <param name="implementationParameters">The parameters to use for the implementation.
/// Must all be required (no default values) per the C# partial method rules.</param>
/// <param name="returnType">The return type the implementation should use. Pass the
/// generator's own return type rather than relying on the customer's parsed declaration:
/// the customer's declaration may reference types generated into the same assembly, which
/// are unresolved when the declaration is read (Roslyn surfaces them as error types with no
/// namespace), producing malformed <c>global::.TypeName</c> output. C# requires a partial
/// method's declaration and implementation to share the same return type, so the generator's
/// resolved return type is necessarily the correct one. When <c>null</c>, the customer's
/// parsed return type is used as a fallback.</param>
public static MethodSignature BuildPartialSignature(
MethodSignature customSignature,
IReadOnlyList<ParameterProvider> implementationParameters)
IReadOnlyList<ParameterProvider> implementationParameters,
CSharpType? returnType = null)
{
if (customSignature is null)
{
Expand All @@ -184,7 +193,7 @@ public static MethodSignature BuildPartialSignature(
customSignature.Name,
customSignature.Description,
customSignature.Modifiers | MethodSignatureModifiers.Partial,
customSignature.ReturnType,
returnType ?? customSignature.ReturnType,
customSignature.ReturnDescription,
implementationParameters,
customSignature.Attributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,12 +413,17 @@ internal MethodProvider[] FilterCustomizedMethods(IEnumerable<MethodProvider> sp
private static MethodProvider CreatePartialMethodFromCustomSignature(MethodSignature customSignature, MethodProvider generatedMethod)
{
// Partial method implementations require all parameters to be required (no default values).
// The generator's parameters carry the metadata and the declarations referenced by the
// method body and XML docs; the custom signature only supplies the parameter names.
var requiredParameters = PartialMethodCustomization.RenameAndCloneParameters(
customSignature.Parameters,
generatedMethod.Signature.Parameters,
customSignature.Parameters,
removeDefaults: true);

var partialSignature = PartialMethodCustomization.BuildPartialSignature(customSignature, requiredParameters);
var partialSignature = PartialMethodCustomization.BuildPartialSignature(
customSignature,
requiredParameters,
generatedMethod.Signature.ReturnType);

MethodProvider partialMethod = generatedMethod.BodyExpression != null
? new MethodProvider(partialSignature, generatedMethod.BodyExpression, generatedMethod.EnclosingType, generatedMethod.XmlDocs, generatedMethod.Suppressions)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#nullable disable

namespace Test;

public partial class CustomPartialMethodType
{
static partial void MyMethod(string input);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#nullable disable

namespace Test;

public partial class CustomPartialReturnType
{
// CustomReturnModel is a type generated into the same assembly, so it is unresolved when this
// declaration is read. The generated implementation must still use the generator's resolved
// return type (with its namespace) rather than this unresolved one.
static partial CustomReturnModel MyMethod(string input);
}
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,90 @@ public async Task TestSpecViewReturnsAllMethodsEvenWhenCustomized()
Assert.AreEqual("Method3", specView.Methods[2].Signature.Name);
}

[Test]
public async Task CustomPartialMethodImplementationKeepsParameterNames()
{
await MockHelpers.LoadMockGeneratorAsync(compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var inputParam = new ParameterProvider("input", $"The input value.", typeof(string));
var generatedMethod = new MethodProvider(
new MethodSignature(
"MyMethod",
$"Does something.",
MethodSignatureModifiers.Static,
null,
null,
[inputParam]),
inputParam.Invoke("ToString").Terminate(),
new TestTypeProvider());

var typeProvider = new TestTypeProvider(name: "CustomPartialMethodType", methods: [generatedMethod]);

var method = typeProvider.Methods.Single();
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Partial));

using var codeWriter = new CodeWriter();
codeWriter.WriteMethod(method);
var result = codeWriter.ToString(false);

// The partial implementation's signature parameter must keep the same name the body uses.
// Regression guard: the parameter must not be renamed with a numeric suffix (e.g. "input0").
Assert.IsFalse(
result.Contains("input0"),
$"Partial implementation renamed the parameter with a numeric suffix:\n{result}");
Assert.AreEqual("input", method.Signature.Parameters.Single().Name);
}

[Test]
public async Task CustomPartialMethodImplementationUsesGeneratorReturnType()
{
await MockHelpers.LoadMockGeneratorAsync(compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

// The generator's resolved return type carries a namespace. The custom partial
// declaration references the same type by name, but it is unresolved when read (a
// type generated into the same assembly), so its parsed CSharpType has no namespace.
var generatorReturnType = new CSharpType(
"CustomReturnModel",
"Sample.Models",
isValueType: false,
isNullable: false,
declaringType: null,
args: [],
isPublic: true,
isStruct: false);

var inputParam = new ParameterProvider("input", $"The input value.", typeof(string));
var generatedMethod = new MethodProvider(
new MethodSignature(
"MyMethod",
$"Does something.",
MethodSignatureModifiers.Static,
generatorReturnType,
$"The result.",
[inputParam]),
inputParam.Invoke("ToString").Terminate(),
new TestTypeProvider());

var typeProvider = new TestTypeProvider(name: "CustomPartialReturnType", methods: [generatedMethod]);

var method = typeProvider.Methods.Single();
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Partial));

// The implementation must use the generator's resolved return type (with namespace),
// not the customer's unresolved parsed return type (empty namespace).
Assert.AreEqual("Sample.Models", method.Signature.ReturnType!.Namespace);
Assert.AreEqual("CustomReturnModel", method.Signature.ReturnType.Name);

using var codeWriter = new CodeWriter();
codeWriter.WriteMethod(method);
var result = codeWriter.ToString(false);

// Regression guard: an empty-namespace return type renders as the malformed `global::.`.
Assert.IsFalse(
result.Contains("global::."),
$"Partial implementation wrote a return type with no namespace:\n{result}");
}

[Test]
public async Task TestSpecViewReturnsAllPropertiesEvenWhenSuppressed()
{
Expand Down
Loading