diff --git a/sandbox/ConsoleApp1/ConsoleApp1.csproj b/sandbox/ConsoleApp1/ConsoleApp1.csproj index 0364815..8935c24 100644 --- a/sandbox/ConsoleApp1/ConsoleApp1.csproj +++ b/sandbox/ConsoleApp1/ConsoleApp1.csproj @@ -7,7 +7,7 @@ enable false 1591 - true + false diff --git a/src/Claudia.FunctionGenerator/DiagnosticDescriptors.cs b/src/Claudia.FunctionGenerator/DiagnosticDescriptors.cs index c818298..7fc8eaf 100644 --- a/src/Claudia.FunctionGenerator/DiagnosticDescriptors.cs +++ b/src/Claudia.FunctionGenerator/DiagnosticDescriptors.cs @@ -41,7 +41,7 @@ internal static class DiagnosticDescriptors public static readonly DiagnosticDescriptor MethodNeedsDocumentationCommentXml = new( id: "CLFG005", title: "Method needs documentation comment xml", - messageFormat: "The '{0}' method has no documentation comment, define it and generate it(true in PropertyGroup of csproj)", + messageFormat: "The '{0}' method has no documentation comment", category: Category, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); diff --git a/src/Claudia.FunctionGenerator/Emitter.cs b/src/Claudia.FunctionGenerator/Emitter.cs index b4c8545..ab14b25 100644 --- a/src/Claudia.FunctionGenerator/Emitter.cs +++ b/src/Claudia.FunctionGenerator/Emitter.cs @@ -96,18 +96,17 @@ public static class PromptXml void EmitToolDescription(Method method) { - var docComment = method.Symbol.GetDocumentationCommentXml(); - var xml = XElement.Parse(docComment); + var docComment = method.Syntax.GetDocumentationCommentTriviaSyntax()!; - var description = ((string)xml.Element("summary")).Replace("\"", "'").Trim(); + var description = docComment.GetSummary().Replace("\"", "'"); var parameters = new List(); - foreach (var p in xml.Elements("param")) + foreach (var p in docComment.GetParams()) { - var paramDescription = ((string)p).Replace("\"", "'").Trim(); + var paramDescription = p.Description.Replace("\"", "'"); // type retrieve from method symbol - var name = p.Attribute("name").Value.Trim(); + var name = p.Name; var paramType = method.Symbol.Parameters.First(x => x.Name == name).Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); parameters.Add(new XElement("parameter", diff --git a/src/Claudia.FunctionGenerator/Parser.cs b/src/Claudia.FunctionGenerator/Parser.cs index 4c31088..64959ce 100644 --- a/src/Claudia.FunctionGenerator/Parser.cs +++ b/src/Claudia.FunctionGenerator/Parser.cs @@ -64,16 +64,15 @@ internal ParseResult[] Parse() // source.TargetNode var method = (IMethodSymbol)source.TargetSymbol; - var docXml = method.GetDocumentationCommentXml(); - if (string.IsNullOrWhiteSpace(docXml)) + var docComment = source.TargetNode.GetDocumentationCommentTriviaSyntax(); + if (docComment == null) { context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MethodNeedsDocumentationCommentXml, method.Locations[0], method.Name)); continue; } else { - var xml = XElement.Parse(docXml); - var description = ((string)xml.Element("summary")).Replace("\"", "'").Trim(); + var description = docComment.GetSummary().Replace("\"", "'"); if (string.IsNullOrWhiteSpace(description)) { context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MethodNeedsSummary, method.Locations[0], method.Name)); @@ -83,16 +82,16 @@ internal ParseResult[] Parse() var parameterNames = new HashSet(method.Parameters.Select(x => x.Name)); if (parameterNames.Count != 0) { - foreach (var p in xml.Elements("param")) + foreach (var p in docComment.GetParams()) { - var desc = (string)p; + var desc = p.Description; if (string.IsNullOrWhiteSpace(desc)) { context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ParameterNeedsDescription, method.Locations[0], method.Name, p.Name)); continue; } - var name = p.Attribute("name").Value.Trim(); + var name = p.Name; parameterNames.Remove(name); } diff --git a/src/Claudia.FunctionGenerator/RoslynExtensions.cs b/src/Claudia.FunctionGenerator/RoslynExtensions.cs new file mode 100644 index 0000000..dfb0f9d --- /dev/null +++ b/src/Claudia.FunctionGenerator/RoslynExtensions.cs @@ -0,0 +1,86 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.CodeAnalysis.CSharp; + +namespace Claudia.FunctionGenerator; + +internal static class RoslynExtensions +{ + public static DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(this SyntaxNode node) + { + // Hack note: + // ISymbol.GetDocumentationCommtentXml requirestrue . + // However, getting the DocumentationCommentTrivia of a SyntaxNode also requires the same condition. + // It can only be obtained when DocumentationMode is Parse or Diagnostic, but whenfalse , + // it becomes None, and the necessary Trivia cannot be obtained. + // Therefore, we will attempt to reparse and retrieve it. + + // About DocumentationMode and Trivia: https://github.com/dotnet/roslyn/issues/58210 + if (node.SyntaxTree.Options.DocumentationMode == DocumentationMode.None) + { + var withDocumentationComment = node.SyntaxTree.Options.WithDocumentationMode(DocumentationMode.Parse); + var code = node.ToFullString(); + var newTree = CSharpSyntaxTree.ParseText(code, (CSharpParseOptions)withDocumentationComment); + node = newTree.GetRoot(); + } + + foreach (var leadingTrivia in node.GetLeadingTrivia()) + { + if (leadingTrivia.GetStructure() is DocumentationCommentTriviaSyntax structure) + { + return structure; + } + } + + return null; + } + + static IEnumerable GetXmlElements(this SyntaxList content, string elementName) + { + foreach (XmlNodeSyntax syntax in content) + { + if (syntax is XmlEmptyElementSyntax emptyElement) + { + if (string.Equals(elementName, emptyElement.Name.ToString(), StringComparison.Ordinal)) + { + yield return emptyElement; + } + + continue; + } + + if (syntax is XmlElementSyntax elementSyntax) + { + if (string.Equals(elementName, elementSyntax.StartTag?.Name?.ToString(), StringComparison.Ordinal)) + { + yield return elementSyntax; + } + + continue; + } + } + } + + public static string GetSummary(this DocumentationCommentTriviaSyntax docComment) + { + var summary = docComment.Content.GetXmlElements("summary").FirstOrDefault() as XmlElementSyntax; + if (summary == null) return ""; + + return summary.Content.ToString().Replace("///", "").Trim(); + } + + public static IEnumerable<(string Name, string Description)> GetParams(this DocumentationCommentTriviaSyntax docComment) + { + foreach (var item in docComment.Content.GetXmlElements("param").OfType()) + { + var name = item.StartTag.Attributes.OfType().FirstOrDefault()?.Identifier.Identifier.ValueText.Replace("///", "").Trim() ?? ""; + var desc = item.Content.ToString().Replace("///", "").Trim() ?? ""; + yield return (name, desc); + } + + yield break; + } +}