Use fully-qualified type names in generated partial method signatures (#1135)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
This commit is contained in:
Copilot 2026-01-12 11:13:20 -05:00 • committed by GitHub
commit 4a2c36cf0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 394 additions and 5 deletions

View file

@ -19,6 +19,14 @@ public sealed class XmlToDescriptionGenerator : IIncrementalGenerator
{
private const string GeneratedFileName = "ModelContextProtocol.Descriptions.g.cs";
/// <summary>
/// A display format that produces fully-qualified type names with "global::" prefix
/// and includes nullability annotations.
/// </summary>
private static readonly SymbolDisplayFormat s_fullyQualifiedFormatWithNullability =
SymbolDisplayFormat.FullyQualifiedFormat.AddMiscellaneousOptions(
SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Extract method information for all MCP tools, prompts, and resources.
@ -125,7 +133,7 @@ public sealed class XmlToDescriptionGenerator : IIncrementalGenerator
.Where(m => !m.IsKind(SyntaxKind.AsyncKeyword))
.Select(m => m.Text);
string modifiersStr = string.Join(" ", modifiers);
string returnType = methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
string returnType = methodSymbol.ReturnType.ToDisplayString(s_fullyQualifiedFormatWithNullability);
string methodName = methodSymbol.Name;
// Extract parameters
@ -137,7 +145,7 @@ public sealed class XmlToDescriptionGenerator : IIncrementalGenerator
var paramSyntax = i < parameterSyntaxList.Count ? parameterSyntaxList[i] : null;
parameters[i] = new ParameterInfo(
ParameterType: param.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
ParameterType: param.Type.ToDisplayString(s_fullyQualifiedFormatWithNullability),
Name: param.Name,
HasDescriptionAttribute: descriptionAttribute is not null && HasAttribute(param, descriptionAttribute),
XmlDescription: xmlDocs?.Parameters.TryGetValue(param.Name, out var pd) == true && !string.IsNullOrWhiteSpace(pd) ? pd : null,

View file

@ -1561,7 +1561,7 @@ public partial class XmlToDescriptionGeneratorTests
partial class TestTools
{
[Description("Async tool")]
public partial Task<string> DoWorkAsync(string input);
public partial global::System.Threading.Tasks.Task<string> DoWorkAsync(string input);
}
}
""";
@ -1611,7 +1611,7 @@ public partial class XmlToDescriptionGeneratorTests
partial class TestTools
{
[Description("Static async tool")]
public static partial Task<string> StaticAsyncMethod(string input);
public static partial global::System.Threading.Tasks.Task<string> StaticAsyncMethod(string input);
}
}
""";
@ -1663,7 +1663,7 @@ public partial class XmlToDescriptionGeneratorTests
partial class TestTools
{
[Description("Async tool with defaults")]
public static partial Task<string> AsyncWithDefaults([Description("The input")] string input, [Description("Timeout in ms")] int timeout = 1000);
public static partial global::System.Threading.Tasks.Task<string> AsyncWithDefaults([Description("The input")] string input, [Description("Timeout in ms")] int timeout = 1000);
}
}
""";
@ -1719,6 +1719,387 @@ public partial class XmlToDescriptionGeneratorTests
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Generator_WithTypeFromDifferentNamespace_GeneratesFullyQualifiedTypeName(bool useFullyQualifiedTypesInSource)
{
// This test validates that regardless of whether the source code uses fully qualified
// or unqualified type names, the generator always emits fully qualified type names
// with global:: prefix. This fixes the issue where parameter types from different
// namespaces caused build failures.
string usingDirective = useFullyQualifiedTypesInSource ? "" : "using MyApp.Actions;";
string returnType = useFullyQualifiedTypesInSource ? "System.Threading.Tasks.Task<string>" : "Task<string>";
string parameterType = useFullyQualifiedTypesInSource ? "MyApp.Actions.MyAction" : "MyAction";
var result = RunGenerator($$"""
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Threading.Tasks;
{{usingDirective}}
namespace MyApp.Actions
{
public enum MyAction
{
One,
Two
}
}
namespace MyApp
{
[McpServerToolType]
public sealed partial class Tools
{
/// <summary>Do a thing based on an action.</summary>
/// <param name="action">The action to perform.</param>
[McpServerTool]
public async partial {{returnType}} DoThing({{parameterType}} action)
=> await Task.FromResult("ok");
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
// Regardless of source qualification, generated code should always use
// fully qualified type names with global:: prefix
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace MyApp
{
partial class Tools
{
[Description("Do a thing based on an action.")]
public partial global::System.Threading.Tasks.Task<string> DoThing([Description("The action to perform.")] global::MyApp.Actions.MyAction action);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithGenericListParameter_GeneratesFullyQualifiedTypeName()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Collections.Generic;
namespace MyApp.Models
{
public class Item { }
}
namespace MyApp
{
[McpServerToolType]
public sealed partial class Tools
{
/// <summary>Process items.</summary>
/// <param name="items">The items to process.</param>
[McpServerTool]
public static partial string ProcessItems(List<MyApp.Models.Item> items)
=> "ok";
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace MyApp
{
partial class Tools
{
[Description("Process items.")]
public static partial string ProcessItems([Description("The items to process.")] global::System.Collections.Generic.List<global::MyApp.Models.Item> items);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithGenericDictionaryParameter_GeneratesFullyQualifiedTypeName()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Collections.Generic;
namespace MyApp.Models
{
public class Key { }
public class Value { }
}
namespace MyApp
{
[McpServerToolType]
public sealed partial class Tools
{
/// <summary>Process mapping.</summary>
/// <param name="mapping">The mapping to process.</param>
[McpServerTool]
public static partial string ProcessMapping(Dictionary<MyApp.Models.Key, MyApp.Models.Value> mapping)
=> "ok";
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace MyApp
{
partial class Tools
{
[Description("Process mapping.")]
public static partial string ProcessMapping([Description("The mapping to process.")] global::System.Collections.Generic.Dictionary<global::MyApp.Models.Key, global::MyApp.Models.Value> mapping);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithArrayParameter_GeneratesFullyQualifiedTypeName()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace MyApp.Models
{
public class Item { }
}
namespace MyApp
{
[McpServerToolType]
public sealed partial class Tools
{
/// <summary>Process items array.</summary>
/// <param name="items">The items array to process.</param>
[McpServerTool]
public static partial string ProcessItemsArray(MyApp.Models.Item[] items)
=> "ok";
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace MyApp
{
partial class Tools
{
[Description("Process items array.")]
public static partial string ProcessItemsArray([Description("The items array to process.")] global::MyApp.Models.Item[] items);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithNullableReferenceTypeParameter_GeneratesFullyQualifiedTypeName()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace MyApp.Models
{
public class Item { }
}
namespace MyApp
{
[McpServerToolType]
public sealed partial class Tools
{
/// <summary>Process optional item.</summary>
/// <param name="item">The optional item to process.</param>
[McpServerTool]
public static partial string ProcessOptionalItem(MyApp.Models.Item? item)
=> "ok";
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace MyApp
{
partial class Tools
{
[Description("Process optional item.")]
public static partial string ProcessOptionalItem([Description("The optional item to process.")] global::MyApp.Models.Item? item);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithNestedTypeParameter_GeneratesFullyQualifiedTypeName()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace MyApp.Models
{
public class Container
{
public class NestedItem { }
}
}
namespace MyApp
{
[McpServerToolType]
public sealed partial class Tools
{
/// <summary>Process nested item.</summary>
/// <param name="item">The nested item to process.</param>
[McpServerTool]
public static partial string ProcessNestedItem(MyApp.Models.Container.NestedItem item)
=> "ok";
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace MyApp
{
partial class Tools
{
[Description("Process nested item.")]
public static partial string ProcessNestedItem([Description("The nested item to process.")] global::MyApp.Models.Container.NestedItem item);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithNullableValueTypeParameter_GeneratesFullyQualifiedTypeName()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace MyApp.Models
{
public struct MyStruct { }
}
namespace MyApp
{
[McpServerToolType]
public sealed partial class Tools
{
/// <summary>Process optional struct.</summary>
/// <param name="value">The optional struct to process.</param>
[McpServerTool]
public static partial string ProcessOptionalStruct(MyApp.Models.MyStruct? value)
=> "ok";
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace MyApp
{
partial class Tools
{
[Description("Process optional struct.")]
public static partial string ProcessOptionalStruct([Description("The optional struct to process.")] global::MyApp.Models.MyStruct? value);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
private GeneratorRunResult RunGenerator([StringSyntax("C#-test")] string source, params string[] expectedDiagnosticIds)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);