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";
|
||||
|
||||
/// <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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue