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:
parent
b2afc2fd65
commit
4a2c36cf0c
2 changed files with 394 additions and 5 deletions
|
|
@ -19,6 +19,14 @@ public sealed class XmlToDescriptionGenerator : IIncrementalGenerator
|
||||||
{
|
{
|
||||||
private const string GeneratedFileName = "ModelContextProtocol.Descriptions.g.cs";
|
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)
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
{
|
{
|
||||||
// Extract method information for all MCP tools, prompts, and resources.
|
// 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))
|
.Where(m => !m.IsKind(SyntaxKind.AsyncKeyword))
|
||||||
.Select(m => m.Text);
|
.Select(m => m.Text);
|
||||||
string modifiersStr = string.Join(" ", modifiers);
|
string modifiersStr = string.Join(" ", modifiers);
|
||||||
string returnType = methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
string returnType = methodSymbol.ReturnType.ToDisplayString(s_fullyQualifiedFormatWithNullability);
|
||||||
string methodName = methodSymbol.Name;
|
string methodName = methodSymbol.Name;
|
||||||
|
|
||||||
// Extract parameters
|
// Extract parameters
|
||||||
|
|
@ -137,7 +145,7 @@ public sealed class XmlToDescriptionGenerator : IIncrementalGenerator
|
||||||
var paramSyntax = i < parameterSyntaxList.Count ? parameterSyntaxList[i] : null;
|
var paramSyntax = i < parameterSyntaxList.Count ? parameterSyntaxList[i] : null;
|
||||||
|
|
||||||
parameters[i] = new ParameterInfo(
|
parameters[i] = new ParameterInfo(
|
||||||
ParameterType: param.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
|
ParameterType: param.Type.ToDisplayString(s_fullyQualifiedFormatWithNullability),
|
||||||
Name: param.Name,
|
Name: param.Name,
|
||||||
HasDescriptionAttribute: descriptionAttribute is not null && HasAttribute(param, descriptionAttribute),
|
HasDescriptionAttribute: descriptionAttribute is not null && HasAttribute(param, descriptionAttribute),
|
||||||
XmlDescription: xmlDocs?.Parameters.TryGetValue(param.Name, out var pd) == true && !string.IsNullOrWhiteSpace(pd) ? pd : null,
|
XmlDescription: xmlDocs?.Parameters.TryGetValue(param.Name, out var pd) == true && !string.IsNullOrWhiteSpace(pd) ? pd : null,
|
||||||
|
|
|
||||||
|
|
@ -1561,7 +1561,7 @@ public partial class XmlToDescriptionGeneratorTests
|
||||||
partial class TestTools
|
partial class TestTools
|
||||||
{
|
{
|
||||||
[Description("Async tool")]
|
[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
|
partial class TestTools
|
||||||
{
|
{
|
||||||
[Description("Static async tool")]
|
[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
|
partial class TestTools
|
||||||
{
|
{
|
||||||
[Description("Async tool with defaults")]
|
[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());
|
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)
|
private GeneratorRunResult RunGenerator([StringSyntax("C#-test")] string source, params string[] expectedDiagnosticIds)
|
||||||
{
|
{
|
||||||
var syntaxTree = CSharpSyntaxTree.ParseText(source);
|
var syntaxTree = CSharpSyntaxTree.ParseText(source);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue