Add production-ready incremental source generator for automatic Description attributes from XML comments (#899)
Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
parent
83098e7cc0
commit
ad86b53347
9 changed files with 2080 additions and 2 deletions
|
|
@ -46,11 +46,19 @@
|
||||||
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
|
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Source Generator & Analyzer dependencies -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Build Infra & Packaging -->
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Testing dependencies -->
|
<!-- Build Infra & Packaging dependencies-->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Testing dependencies -->
|
||||||
|
<ItemGroup>
|
||||||
<PackageVersion Include="Anthropic.SDK" Version="5.8.0" />
|
<PackageVersion Include="Anthropic.SDK" Version="5.8.0" />
|
||||||
<PackageVersion Include="coverlet.collector" Version="6.0.4">
|
<PackageVersion Include="coverlet.collector" Version="6.0.4">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|
|
||||||
|
|
@ -62,11 +62,13 @@
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/src/">
|
<Folder Name="/src/">
|
||||||
<File Path="src/Directory.Build.props" />
|
<File Path="src/Directory.Build.props" />
|
||||||
|
<Project Path="src/ModelContextProtocol.Analyzers/ModelContextProtocol.Analyzers.csproj" />
|
||||||
<Project Path="src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj" />
|
<Project Path="src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj" />
|
||||||
<Project Path="src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj" />
|
<Project Path="src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj" />
|
||||||
<Project Path="src/ModelContextProtocol/ModelContextProtocol.csproj" />
|
<Project Path="src/ModelContextProtocol/ModelContextProtocol.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
|
<Project Path="tests/ModelContextProtocol.Analyzers.Tests/ModelContextProtocol.Analyzers.Tests.csproj" />
|
||||||
<Project Path="tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj" />
|
<Project Path="tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj" />
|
||||||
<Project Path="tests/ModelContextProtocol.TestOAuthServer/ModelContextProtocol.TestOAuthServer.csproj" />
|
<Project Path="tests/ModelContextProtocol.TestOAuthServer/ModelContextProtocol.TestOAuthServer.csproj" />
|
||||||
<Project Path="tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj" />
|
<Project Path="tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,11 @@ await using McpServer server = McpServer.Create(new StdioServerTransport("MyServ
|
||||||
await server.RunAsync();
|
await server.RunAsync();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Descriptions can be added to tools, prompts, and resources in a variety of ways, including via the `[Description]` attribute from `System.ComponentModel`.
|
||||||
|
This attribute may be placed on a method to provide for the tool, prompt, or resource, or on individual parameters to describe each's purpose.
|
||||||
|
XML comments may also be used; if an `[McpServerTool]`, `[McpServerPrompt]`, or `[McpServerResource]`-attributed method is marked as `partial`,
|
||||||
|
XML comments placed on the method will be used automatically to generate `[Description]` attributes for the method and its parameters.
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
The starting point for this library was a project called [mcpdotnet](https://github.com/PederHP/mcpdotnet), initiated by [Peder Holdgaard Pedersen](https://github.com/PederHP). We are grateful for the work done by Peder and other contributors to that repository, which created a solid foundation for this library.
|
The starting point for this library was a project called [mcpdotnet](https://github.com/PederHP/mcpdotnet), initiated by [Peder Holdgaard Pedersen](https://github.com/PederHP). We are grateful for the work done by Peder and other contributors to that repository, which created a solid foundation for this library.
|
||||||
|
|
|
||||||
31
src/ModelContextProtocol.Analyzers/Diagnostics.cs
Normal file
31
src/ModelContextProtocol.Analyzers/Diagnostics.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Text;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Text;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace ModelContextProtocol.Analyzers;
|
||||||
|
|
||||||
|
/// <summary>Provides the diagnostic descriptors used by the assembly.</summary>
|
||||||
|
internal static class Diagnostics
|
||||||
|
{
|
||||||
|
public static DiagnosticDescriptor InvalidXmlDocumentation { get; } = new(
|
||||||
|
id: "MCP001",
|
||||||
|
title: "Invalid XML documentation for MCP method",
|
||||||
|
messageFormat: "XML comment for method '{0}' is invalid and cannot be processed to generate [Description] attributes.",
|
||||||
|
category: "mcp",
|
||||||
|
defaultSeverity: DiagnosticSeverity.Warning,
|
||||||
|
isEnabledByDefault: true,
|
||||||
|
description: "The XML documentation comment contains invalid XML and cannot be processed to generate Description attributes.");
|
||||||
|
|
||||||
|
public static DiagnosticDescriptor McpMethodMustBePartial { get; } = new(
|
||||||
|
id: "MCP002",
|
||||||
|
title: "MCP method must be partial to generate [Description] attributes",
|
||||||
|
messageFormat: "Method '{0}' has XML documentation that could be used to generate [Description] attributes, but the method is not declared as partial.",
|
||||||
|
category: "mcp",
|
||||||
|
defaultSeverity: DiagnosticSeverity.Warning,
|
||||||
|
isEnabledByDefault: true,
|
||||||
|
description: "Methods with MCP attributes should be declared as partial to allow the source generator to emit Description attributes from XML documentation comments.");
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<IsRoslynComponent>true</IsRoslynComponent>
|
||||||
|
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||||
|
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="..\Common\Polyfills\**\*.cs" />
|
||||||
|
<Compile Include="..\Common\Polyfills\System\Runtime\CompilerServices\IsExternalInit.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
414
src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs
Normal file
414
src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs
Normal file
|
|
@ -0,0 +1,414 @@
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Text;
|
||||||
|
using System.CodeDom.Compiler;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Text;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace ModelContextProtocol.Analyzers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Source generator that creates [Description] attributes from XML comments
|
||||||
|
/// for partial methods tagged with MCP attributes.
|
||||||
|
/// </summary>
|
||||||
|
[Generator]
|
||||||
|
public sealed class XmlToDescriptionGenerator : IIncrementalGenerator
|
||||||
|
{
|
||||||
|
private const string GeneratedFileName = "ModelContextProtocol.Descriptions.g.cs";
|
||||||
|
private const string McpServerToolAttributeName = "ModelContextProtocol.Server.McpServerToolAttribute";
|
||||||
|
private const string McpServerPromptAttributeName = "ModelContextProtocol.Server.McpServerPromptAttribute";
|
||||||
|
private const string McpServerResourceAttributeName = "ModelContextProtocol.Server.McpServerResourceAttribute";
|
||||||
|
private const string DescriptionAttributeName = "System.ComponentModel.DescriptionAttribute";
|
||||||
|
|
||||||
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
|
{
|
||||||
|
// Use ForAttributeWithMetadataName for each MCP attribute type
|
||||||
|
var toolMethods = CreateProviderForAttribute(context, McpServerToolAttributeName);
|
||||||
|
var promptMethods = CreateProviderForAttribute(context, McpServerPromptAttributeName);
|
||||||
|
var resourceMethods = CreateProviderForAttribute(context, McpServerResourceAttributeName);
|
||||||
|
|
||||||
|
// Combine all three providers
|
||||||
|
var allMethods = toolMethods
|
||||||
|
.Collect()
|
||||||
|
.Combine(promptMethods.Collect())
|
||||||
|
.Combine(resourceMethods.Collect())
|
||||||
|
.Select(static (tuple, _) =>
|
||||||
|
{
|
||||||
|
var ((tool, prompt), resource) = tuple;
|
||||||
|
return tool.AddRange(prompt).AddRange(resource);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combine with compilation to get well-known type symbols.
|
||||||
|
var compilationAndMethods = context.CompilationProvider.Combine(allMethods);
|
||||||
|
|
||||||
|
// Write out the source for all methods.
|
||||||
|
context.RegisterSourceOutput(compilationAndMethods, static (spc, source) => Execute(source.Left, source.Right, spc));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IncrementalValuesProvider<MethodToGenerate> CreateProviderForAttribute(
|
||||||
|
IncrementalGeneratorInitializationContext context,
|
||||||
|
string attributeMetadataName) =>
|
||||||
|
context.SyntaxProvider.ForAttributeWithMetadataName(
|
||||||
|
attributeMetadataName,
|
||||||
|
static (node, _) => node is MethodDeclarationSyntax,
|
||||||
|
static (ctx, ct) =>
|
||||||
|
{
|
||||||
|
var methodDeclaration = (MethodDeclarationSyntax)ctx.TargetNode;
|
||||||
|
var methodSymbol = (IMethodSymbol)ctx.TargetSymbol;
|
||||||
|
return new MethodToGenerate(methodDeclaration, methodSymbol);
|
||||||
|
});
|
||||||
|
|
||||||
|
private static void Execute(Compilation compilation, ImmutableArray<MethodToGenerate> methods, SourceProductionContext context)
|
||||||
|
{
|
||||||
|
if (methods.IsDefaultOrEmpty ||
|
||||||
|
compilation.GetTypeByMetadataName(DescriptionAttributeName) is not { } descriptionAttribute)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather a list of all methods needing generation.
|
||||||
|
List<(IMethodSymbol MethodSymbol, MethodDeclarationSyntax MethodDeclaration, XmlDocumentation? XmlDocs)> methodsToGenerate = new(methods.Length);
|
||||||
|
foreach (var methodModel in methods)
|
||||||
|
{
|
||||||
|
var xmlDocs = ExtractXmlDocumentation(methodModel.MethodSymbol, context);
|
||||||
|
|
||||||
|
// Generate implementation for partial methods.
|
||||||
|
if (methodModel.MethodDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword))
|
||||||
|
{
|
||||||
|
methodsToGenerate.Add((methodModel.MethodSymbol, methodModel.MethodDeclaration, xmlDocs));
|
||||||
|
}
|
||||||
|
else if (xmlDocs is not null && HasGeneratableContent(xmlDocs, methodModel.MethodSymbol, descriptionAttribute))
|
||||||
|
{
|
||||||
|
// The method is not partial but has XML docs that would generate attributes; issue a diagnostic.
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
|
Diagnostics.McpMethodMustBePartial,
|
||||||
|
methodModel.MethodDeclaration.Identifier.GetLocation(),
|
||||||
|
methodModel.MethodSymbol.Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a single file with all partial declarations.
|
||||||
|
if (methodsToGenerate.Count > 0)
|
||||||
|
{
|
||||||
|
string source = GenerateSourceFile(compilation, methodsToGenerate, descriptionAttribute);
|
||||||
|
context.AddSource(GeneratedFileName, SourceText.From(source, Encoding.UTF8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static XmlDocumentation? ExtractXmlDocumentation(IMethodSymbol methodSymbol, SourceProductionContext context)
|
||||||
|
{
|
||||||
|
string? xmlDoc = methodSymbol.GetDocumentationCommentXml();
|
||||||
|
if (string.IsNullOrWhiteSpace(xmlDoc))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (XDocument.Parse(xmlDoc).Element("member") is not { } memberElement)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = CleanXmlDocText(memberElement.Element("summary")?.Value);
|
||||||
|
var remarks = CleanXmlDocText(memberElement.Element("remarks")?.Value);
|
||||||
|
var returns = CleanXmlDocText(memberElement.Element("returns")?.Value);
|
||||||
|
|
||||||
|
// Combine summary and remarks for method description.
|
||||||
|
var methodDescription =
|
||||||
|
string.IsNullOrWhiteSpace(remarks) ? summary :
|
||||||
|
string.IsNullOrWhiteSpace(summary) ? remarks :
|
||||||
|
$"{summary}\n{remarks}";
|
||||||
|
|
||||||
|
Dictionary<string, string> paramDocs = new(StringComparer.Ordinal);
|
||||||
|
foreach (var paramElement in memberElement.Elements("param"))
|
||||||
|
{
|
||||||
|
var name = paramElement.Attribute("name")?.Value;
|
||||||
|
var value = CleanXmlDocText(paramElement.Value);
|
||||||
|
if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
paramDocs[name!] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return documentation even if empty - we'll still generate the partial implementation
|
||||||
|
return new(methodDescription ?? string.Empty, returns ?? string.Empty, paramDocs);
|
||||||
|
}
|
||||||
|
catch (System.Xml.XmlException)
|
||||||
|
{
|
||||||
|
// Emit warning for invalid XML
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
|
Diagnostics.InvalidXmlDocumentation,
|
||||||
|
methodSymbol.Locations.FirstOrDefault(),
|
||||||
|
methodSymbol.Name));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CleanXmlDocText(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove leading/trailing whitespace and normalize line breaks
|
||||||
|
var lines = text!.Split('\n')
|
||||||
|
.Select(line => line.Trim())
|
||||||
|
.Where(line => !string.IsNullOrEmpty(line));
|
||||||
|
|
||||||
|
return string.Join(" ", lines).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GenerateSourceFile(
|
||||||
|
Compilation compilation,
|
||||||
|
List<(IMethodSymbol MethodSymbol, MethodDeclarationSyntax MethodDeclaration, XmlDocumentation? XmlDocs)> methods,
|
||||||
|
INamedTypeSymbol descriptionAttribute)
|
||||||
|
{
|
||||||
|
StringWriter sw = new();
|
||||||
|
IndentedTextWriter writer = new(sw);
|
||||||
|
|
||||||
|
writer.WriteLine("// <auto-generated/>");
|
||||||
|
writer.WriteLine($"// ModelContextProtocol.Analyzers {typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}");
|
||||||
|
writer.WriteLine();
|
||||||
|
writer.WriteLine("#pragma warning disable");
|
||||||
|
writer.WriteLine();
|
||||||
|
writer.WriteLine("using System.ComponentModel;");
|
||||||
|
writer.WriteLine("using ModelContextProtocol.Server;");
|
||||||
|
writer.WriteLine();
|
||||||
|
|
||||||
|
// Group methods by namespace and containing type
|
||||||
|
var groupedMethods = methods.GroupBy(m =>
|
||||||
|
m.MethodSymbol.ContainingNamespace.Name == compilation.GlobalNamespace.Name ? "" :
|
||||||
|
m.MethodSymbol.ContainingNamespace?.ToDisplayString() ??
|
||||||
|
"");
|
||||||
|
|
||||||
|
bool firstNamespace = true;
|
||||||
|
foreach (var namespaceGroup in groupedMethods)
|
||||||
|
{
|
||||||
|
if (!firstNamespace)
|
||||||
|
{
|
||||||
|
writer.WriteLine();
|
||||||
|
}
|
||||||
|
firstNamespace = false;
|
||||||
|
|
||||||
|
// Check if this is the global namespace (methods with null ContainingNamespace)
|
||||||
|
bool isGlobalNamespace = string.IsNullOrEmpty(namespaceGroup.Key);
|
||||||
|
if (!isGlobalNamespace)
|
||||||
|
{
|
||||||
|
writer.WriteLine($"namespace {namespaceGroup.Key}");
|
||||||
|
writer.WriteLine("{");
|
||||||
|
writer.Indent++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by containing type within namespace
|
||||||
|
bool isFirstTypeInNamespace = true;
|
||||||
|
foreach (var typeGroup in namespaceGroup.GroupBy(m => m.MethodSymbol.ContainingType, SymbolEqualityComparer.Default))
|
||||||
|
{
|
||||||
|
if (typeGroup.Key is not INamedTypeSymbol containingType)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFirstTypeInNamespace)
|
||||||
|
{
|
||||||
|
writer.WriteLine();
|
||||||
|
}
|
||||||
|
isFirstTypeInNamespace = false;
|
||||||
|
|
||||||
|
// Write out the type, which could include parent types.
|
||||||
|
AppendNestedTypeDeclarations(writer, containingType, typeGroup, descriptionAttribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isGlobalNamespace)
|
||||||
|
{
|
||||||
|
writer.Indent--;
|
||||||
|
writer.WriteLine("}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sw.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendNestedTypeDeclarations(
|
||||||
|
IndentedTextWriter writer,
|
||||||
|
INamedTypeSymbol typeSymbol,
|
||||||
|
IGrouping<ISymbol?, (IMethodSymbol MethodSymbol, MethodDeclarationSyntax MethodDeclaration, XmlDocumentation? XmlDocs)> typeGroup,
|
||||||
|
INamedTypeSymbol descriptionAttribute)
|
||||||
|
{
|
||||||
|
// Build stack of nested types from innermost to outermost
|
||||||
|
Stack<INamedTypeSymbol> types = [];
|
||||||
|
for (var current = typeSymbol; current is not null; current = current.ContainingType)
|
||||||
|
{
|
||||||
|
types.Push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate type declarations from outermost to innermost
|
||||||
|
int nestingCount = types.Count;
|
||||||
|
while (types.Count > 0)
|
||||||
|
{
|
||||||
|
// Get the type keyword and handle records
|
||||||
|
var type = types.Pop();
|
||||||
|
var typeDecl = type.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as TypeDeclarationSyntax;
|
||||||
|
string typeKeyword;
|
||||||
|
if (typeDecl is RecordDeclarationSyntax rds)
|
||||||
|
{
|
||||||
|
string classOrStruct = rds.ClassOrStructKeyword.ValueText;
|
||||||
|
if (string.IsNullOrEmpty(classOrStruct))
|
||||||
|
{
|
||||||
|
classOrStruct = "class";
|
||||||
|
}
|
||||||
|
|
||||||
|
typeKeyword = $"{typeDecl.Keyword.ValueText} {classOrStruct}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
typeKeyword = typeDecl?.Keyword.ValueText ?? "class";
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteLine($"partial {typeKeyword} {type.Name}");
|
||||||
|
writer.WriteLine("{");
|
||||||
|
writer.Indent++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate methods for this type.
|
||||||
|
bool firstMethodInType = true;
|
||||||
|
foreach (var (methodSymbol, methodDeclaration, xmlDocs) in typeGroup)
|
||||||
|
{
|
||||||
|
AppendMethodDeclaration(writer, methodSymbol, methodDeclaration, xmlDocs, descriptionAttribute, firstMethodInType);
|
||||||
|
firstMethodInType = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all type declarations.
|
||||||
|
for (int i = 0; i < nestingCount; i++)
|
||||||
|
{
|
||||||
|
writer.Indent--;
|
||||||
|
writer.WriteLine("}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendMethodDeclaration(
|
||||||
|
IndentedTextWriter writer,
|
||||||
|
IMethodSymbol methodSymbol,
|
||||||
|
MethodDeclarationSyntax methodDeclaration,
|
||||||
|
XmlDocumentation? xmlDocs,
|
||||||
|
INamedTypeSymbol descriptionAttribute,
|
||||||
|
bool firstMethodInType)
|
||||||
|
{
|
||||||
|
if (!firstMethodInType)
|
||||||
|
{
|
||||||
|
writer.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the Description attribute for method if needed and documentation exists
|
||||||
|
if (xmlDocs is not null &&
|
||||||
|
!string.IsNullOrWhiteSpace(xmlDocs.MethodDescription) &&
|
||||||
|
!HasAttribute(methodSymbol, descriptionAttribute))
|
||||||
|
{
|
||||||
|
writer.WriteLine($"[Description(\"{EscapeString(xmlDocs.MethodDescription)}\")]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add return: Description attribute if needed and documentation exists
|
||||||
|
if (xmlDocs is not null &&
|
||||||
|
!string.IsNullOrWhiteSpace(xmlDocs.Returns) &&
|
||||||
|
methodSymbol.GetReturnTypeAttributes().All(attr => !SymbolEqualityComparer.Default.Equals(attr.AttributeClass, descriptionAttribute)))
|
||||||
|
{
|
||||||
|
writer.WriteLine($"[return: Description(\"{EscapeString(xmlDocs.Returns)}\")]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy modifiers from original method syntax.
|
||||||
|
// Add return type (without nullable annotations).
|
||||||
|
// Add method name.
|
||||||
|
writer.Write(string.Join(" ", methodDeclaration.Modifiers.Select(m => m.Text)));
|
||||||
|
writer.Write(' ');
|
||||||
|
writer.Write(methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
|
||||||
|
writer.Write(' ');
|
||||||
|
writer.Write(methodSymbol.Name);
|
||||||
|
|
||||||
|
// Add parameters with their Description attributes.
|
||||||
|
writer.Write("(");
|
||||||
|
for (int i = 0; i < methodSymbol.Parameters.Length; i++)
|
||||||
|
{
|
||||||
|
IParameterSymbol param = methodSymbol.Parameters[i];
|
||||||
|
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
writer.Write(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xmlDocs is not null &&
|
||||||
|
!HasAttribute(param, descriptionAttribute) &&
|
||||||
|
xmlDocs.Parameters.TryGetValue(param.Name, out var paramDoc) &&
|
||||||
|
!string.IsNullOrWhiteSpace(paramDoc))
|
||||||
|
{
|
||||||
|
writer.Write($"[Description(\"{EscapeString(paramDoc)}\")] ");
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Write(param.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
|
||||||
|
writer.Write(' ');
|
||||||
|
writer.Write(param.Name);
|
||||||
|
}
|
||||||
|
writer.WriteLine(");");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Checks if a symbol has a specific attribute applied.</summary>
|
||||||
|
private static bool HasAttribute(ISymbol symbol, INamedTypeSymbol attributeType)
|
||||||
|
{
|
||||||
|
foreach (var attr in symbol.GetAttributes())
|
||||||
|
{
|
||||||
|
if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeType))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Escape special characters for C# string literals.</summary>
|
||||||
|
private static string EscapeString(string text) =>
|
||||||
|
string.IsNullOrEmpty(text) ? text :
|
||||||
|
text.Replace("\\", "\\\\")
|
||||||
|
.Replace("\"", "\\\"")
|
||||||
|
.Replace("\r", "\\r")
|
||||||
|
.Replace("\n", "\\n")
|
||||||
|
.Replace("\t", "\\t");
|
||||||
|
|
||||||
|
/// <summary>Checks if XML documentation would generate any Description attributes for a method.</summary>
|
||||||
|
private static bool HasGeneratableContent(XmlDocumentation xmlDocs, IMethodSymbol methodSymbol, INamedTypeSymbol descriptionAttribute)
|
||||||
|
{
|
||||||
|
// Check if method description would be generated
|
||||||
|
if (!string.IsNullOrWhiteSpace(xmlDocs.MethodDescription) && !HasAttribute(methodSymbol, descriptionAttribute))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if return description would be generated
|
||||||
|
if (!string.IsNullOrWhiteSpace(xmlDocs.Returns) &&
|
||||||
|
methodSymbol.GetReturnTypeAttributes().All(attr => !SymbolEqualityComparer.Default.Equals(attr.AttributeClass, descriptionAttribute)))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any parameter descriptions would be generated
|
||||||
|
foreach (var param in methodSymbol.Parameters)
|
||||||
|
{
|
||||||
|
if (!HasAttribute(param, descriptionAttribute) &&
|
||||||
|
xmlDocs.Parameters.TryGetValue(param.Name, out var paramDoc) &&
|
||||||
|
!string.IsNullOrWhiteSpace(paramDoc))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Represents a method that may need Description attributes generated.</summary>
|
||||||
|
private readonly record struct MethodToGenerate(MethodDeclarationSyntax MethodDeclaration, IMethodSymbol MethodSymbol);
|
||||||
|
|
||||||
|
/// <summary>Holds extracted XML documentation for a method.</summary>
|
||||||
|
private sealed record XmlDocumentation(string MethodDescription, string Returns, Dictionary<string, string> Parameters);
|
||||||
|
}
|
||||||
|
|
@ -43,6 +43,23 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Reference analyzers -->
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ModelContextProtocol.Analyzers\ModelContextProtocol.Analyzers.csproj"
|
||||||
|
OutputItemType="Analyzer"
|
||||||
|
ReferenceOutputAssembly="false"
|
||||||
|
PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Package the analyzer/source generator assembly in the NuGet package -->
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Include the analyzer DLL directly in the package. The ProjectReference with OutputItemType="Analyzer" ensures it's built before packing. -->
|
||||||
|
<None Include="$(ArtifactsBinDir)ModelContextProtocol.Analyzers\$(Configuration)\netstandard2.0\ModelContextProtocol.Analyzers.dll"
|
||||||
|
Pack="true"
|
||||||
|
PackagePath="analyzers/dotnet/cs"
|
||||||
|
Visible="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="README.md" pack="true" PackagePath="\" />
|
<None Include="README.md" pack="true" PackagePath="\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="GitHubActionsTestLogger">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Include="xunit.v3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\ModelContextProtocol.Analyzers\ModelContextProtocol.Analyzers.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\ModelContextProtocol.Core\ModelContextProtocol.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue