From 26386510f25b5b66889e3716aaf9ebeb48b3ecaf Mon Sep 17 00:00:00 2001 From: duckdoom5 Date: Mon, 10 Mar 2025 16:17:12 +0100 Subject: [PATCH] Improved XML style comment parsing --- src/AST/Comment.cs | 29 +++++ src/Generator.Tests/Passes/TestPasses.cs | 23 +++- .../Generators/CSharp/CSharpCommentPrinter.cs | 107 ++++++++++++++++-- src/Generator/Passes/CleanCommentsPass.cs | 97 +++++++++++++--- tests/dotnet/Native/Passes.h | 37 ++++++ 5 files changed, 266 insertions(+), 27 deletions(-) diff --git a/src/AST/Comment.cs b/src/AST/Comment.cs index 02b2dd4b..dc70f20a 100644 --- a/src/AST/Comment.cs +++ b/src/AST/Comment.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace CppSharp.AST { @@ -409,10 +410,17 @@ namespace CppSharp.AST { public string Name; public string Value; + + public override string ToString() + { + return $"{Name}=\"{Value}\""; + } } public List Attributes; + public bool SelfClosing { get; set; } + public HTMLStartTagComment() { Kind = DocumentationCommentKind.HTMLStartTagComment; @@ -423,6 +431,15 @@ namespace CppSharp.AST { visitor.VisitHTMLStartTag(this); } + + public override string ToString() + { + var attrStr = string.Empty; + if (Attributes.Count != 0) + attrStr = " " + string.Join(' ', Attributes.Select(x => x.ToString())); + + return $"<{TagName}{attrStr}{(SelfClosing ? "/" : "")}>"; + } } /// @@ -439,6 +456,11 @@ namespace CppSharp.AST { visitor.VisitHTMLEndTag(this); } + + public override string ToString() + { + return $""; + } } /// @@ -457,6 +479,13 @@ namespace CppSharp.AST { visitor.VisitText(this); } + + public override string ToString() + { + return Text; + } + + public bool IsEmpty => string.IsNullOrEmpty(Text) && !HasTrailingNewline; } /// diff --git a/src/Generator.Tests/Passes/TestPasses.cs b/src/Generator.Tests/Passes/TestPasses.cs index 2a72a628..77515380 100644 --- a/src/Generator.Tests/Passes/TestPasses.cs +++ b/src/Generator.Tests/Passes/TestPasses.cs @@ -123,10 +123,12 @@ namespace CppSharp.Generator.Tests.Passes [Test] public void TestCleanCommentsPass() { - var c = AstContext.FindClass("TestCommentsPass").FirstOrDefault(); + var c = AstContext.Class("TestCommentsPass"); + var c2 = AstContext.Class("TestCommentsPass2"); passBuilder.AddPass(new CleanCommentsPass()); passBuilder.RunPasses(pass => pass.VisitDeclaration(c)); + passBuilder.RunPasses(pass => pass.VisitClassDecl(c2)); var para = (ParagraphComment)c.Comment.FullComment.Blocks[0]; var textGenerator = new TextGenerator(); @@ -134,6 +136,25 @@ namespace CppSharp.Generator.Tests.Passes Assert.That(textGenerator.StringBuilder.ToString().Trim(), Is.EqualTo("/// A simple test.")); + + var textGenerator2 = new TextGenerator(); + textGenerator2.Print(c2.Methods[0].Comment.FullComment, CommentKind.BCPLSlash); + + Assert.That(textGenerator2.StringBuilder.ToString().Trim(), + Is.EqualTo( + "/// Gets a value\n" + + "/// One" + )); + + var textGenerator3 = new TextGenerator(); + textGenerator3.Print(c2.Methods[1].Comment.FullComment, CommentKind.BCPLSlash); + + Assert.That(textGenerator3.StringBuilder.ToString().Trim(), + Is.EqualTo( + "/// Sets a value. Get it with \n" + + "/// The value to set\n" + + "/// The parameter (typeof)" + )); } [Test] diff --git a/src/Generator/Generators/CSharp/CSharpCommentPrinter.cs b/src/Generator/Generators/CSharp/CSharpCommentPrinter.cs index 85ec9342..38d6e5c2 100644 --- a/src/Generator/Generators/CSharp/CSharpCommentPrinter.cs +++ b/src/Generator/Generators/CSharp/CSharpCommentPrinter.cs @@ -37,6 +37,7 @@ namespace CppSharp.Generators.CSharp switch (blockCommandComment.CommandKind) { case CommentCommandKind.Brief: + sections.Add(new Section(CommentElement.Summary)); blockCommandComment.ParagraphComment.GetCommentSections(sections); break; case CommentCommandKind.Return: @@ -85,14 +86,23 @@ namespace CppSharp.Generators.CSharp break; } case DocumentationCommentKind.ParagraphComment: - var summaryParagraph = sections.Count == 1; - var paragraphComment = (ParagraphComment)comment; + { + bool summaryParagraph = false; + if (sections.Count == 0) + { + sections.Add(new Section(CommentElement.Summary)); + summaryParagraph = true; + } + var lastParagraphSection = sections.Last(); + var paragraphComment = (ParagraphComment)comment; foreach (var inlineContentComment in paragraphComment.Content) { inlineContentComment.GetCommentSections(sections); if (inlineContentComment.HasTrailingNewline) lastParagraphSection.NewLine(); + + lastParagraphSection = sections.Last(); } if (!string.IsNullOrEmpty(lastParagraphSection.CurrentLine.ToString())) @@ -132,10 +142,33 @@ namespace CppSharp.Generators.CSharp } case DocumentationCommentKind.HTMLStartTagComment: { + var startTag = (HTMLStartTagComment)comment; + var sectionType = CommentElementFromTag(startTag.TagName); + + if (IsInlineCommentElement(sectionType)) + { + var lastSection = sections.Last(); + lastSection.CurrentLine.Append(startTag); + break; + } + + sections.Add(new Section(sectionType) + { + Attributes = startTag.Attributes.Select(a => a.ToString()).ToList() + }); break; } case DocumentationCommentKind.HTMLEndTagComment: { + var endTag = (HTMLEndTagComment)comment; + var sectionType = CommentElementFromTag(endTag.TagName); + + if (IsInlineCommentElement(sectionType)) + { + var lastSection = sections.Last(); + lastSection.CurrentLine.Append(endTag); + } + break; } case DocumentationCommentKind.HTMLTagComment: @@ -252,15 +285,71 @@ namespace CppSharp.Generators.CSharp } } + private static CommentElement CommentElementFromTag(string tag) + { + return tag.ToLowerInvariant() switch + { + "c" => CommentElement.C, + "code" => CommentElement.Code, + "example" => CommentElement.Example, + "exception" => CommentElement.Exception, + "include" => CommentElement.Include, + "list" => CommentElement.List, + "para" => CommentElement.Para, + "param" => CommentElement.Param, + "paramref" => CommentElement.ParamRef, + "permission" => CommentElement.Permission, + "remarks" => CommentElement.Remarks, + "return" or "returns" => CommentElement.Returns, + "summary" => CommentElement.Summary, + "typeparam" => CommentElement.TypeParam, + "typeparamref" => CommentElement.TypeParamRef, + "value" => CommentElement.Value, + "seealso" => CommentElement.SeeAlso, + "see" => CommentElement.See, + "inheritdoc" => CommentElement.InheritDoc, + _ => CommentElement.Unknown + }; + } + + /// From https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/documentation-comments#d3-recommended-tags + /// Enum value is equal to sorting priority private enum CommentElement { - Summary, - Typeparam, - Param, - Returns, - Exception, - Remarks, - Example + C = 1000, // Set text in a code-like font + Code = 1001, // Set one or more lines of source code or program output + Example = 11, // Indicate an example + Exception = 8, // Identifies the exceptions a method can throw + Include = 1, // Includes XML from an external file + List = 1002, // Create a list or table + Para = 1003, // Permit structure to be added to text + Param = 5, // Describe a parameter for a method or constructor + ParamRef = 1004, // Identify that a word is a parameter name + Permission = 7, // Document the security accessibility of a member + Remarks = 9, // Describe additional information about a type + Returns = 6, // Describe the return value of a method + See = 1005, // Specify a link + SeeAlso = 10, // Generate a See Also entry + Summary = 2, // Describe a type or a member of a type + TypeParam = 4, // Describe a type parameter for a generic type or method + TypeParamRef = 1006, // Identify that a word is a type parameter name + Value = 3, // Describe a property + InheritDoc = 0, // Inherit documentation from a base class + Unknown = 9999, // Unknown tag } + + private static bool IsInlineCommentElement(CommentElement element) => + element switch + { + CommentElement.C => true, + CommentElement.Code => true, + CommentElement.List => true, + CommentElement.Para => true, + CommentElement.ParamRef => true, + CommentElement.See => true, + CommentElement.TypeParamRef => true, + CommentElement.Unknown => true, // Print unknown tags as inline + _ => ((int)element) >= 1000 + }; } } diff --git a/src/Generator/Passes/CleanCommentsPass.cs b/src/Generator/Passes/CleanCommentsPass.cs index dffa273c..71ddb8e1 100644 --- a/src/Generator/Passes/CleanCommentsPass.cs +++ b/src/Generator/Passes/CleanCommentsPass.cs @@ -7,10 +7,6 @@ namespace CppSharp.Passes { public class CleanCommentsPass : TranslationUnitPass, ICommentVisitor { - public CleanCommentsPass() => VisitOptions.ResetFlags( - VisitFlags.ClassBases | VisitFlags.FunctionReturnType | - VisitFlags.TemplateArguments); - public bool VisitBlockCommand(BlockCommandComment comment) => true; public override bool VisitParameterDecl(Parameter parameter) => @@ -46,25 +42,92 @@ namespace CppSharp.Passes public bool VisitParagraph(ParagraphComment comment) { - for (int i = 0; i < comment.Content.Count; i++) + // Fix clang parsing html tags as TextComment's + var textComments = comment.Content + .Where(c => c.Kind == DocumentationCommentKind.TextComment) + .Cast() + .ToArray(); + + for (var i = 0; i < textComments.Length; ++i) { - if (comment.Content[i].Kind == DocumentationCommentKind.InlineCommandComment && - i + 1 < comment.Content.Count && - comment.Content[i + 1].Kind == DocumentationCommentKind.TextComment) + TextComment textComment = textComments[i]; + if (textComment.IsEmpty) + { + comment.Content.Remove(textComment); + continue; + } + + if (!textComment.Text.StartsWith('<')) + continue; + + if (textComment.Text.Length < 2 || i + 1 >= textComments.Length) + continue; + + bool isEndTag = textComment.Text[1] == '/'; + + // Replace the TextComment node with a HTMLTagComment node + HTMLTagComment htmlTag = isEndTag ? new HTMLEndTagComment() : new HTMLStartTagComment(); + htmlTag.TagName = textComment.Text[(1 + (isEndTag ? 1 : 0))..]; + + // Cleanup next element + TextComment next = textComments[i + 1]; + int tagEnd = next.Text.IndexOf('>'); + if (tagEnd == -1) + continue; + + if (!isEndTag) { - var textComment = (TextComment)comment.Content[i + 1]; - textComment.Text = Helpers.RegexCommentCommandLeftover.Replace( - textComment.Text, string.Empty); + var startTag = (htmlTag as HTMLStartTagComment)!; + var tagRemains = next.Text[..tagEnd]; + + if (tagRemains.EndsWith('/')) + { + startTag.SelfClosing = true; + tagRemains = tagRemains[..^1]; + } + + var attributes = tagRemains.Split(' ', StringSplitOptions.RemoveEmptyEntries); + foreach (var attribute in attributes) + { + var args = attribute.Split('='); + startTag.Attributes.Add(new HTMLStartTagComment.Attribute + { + Name = args[0].Trim(), + Value = args.ElementAtOrDefault(1)?.Trim(' ', '"'), + }); + } } + + // Strip tagRemains from next element + next.Text = next.Text[(tagEnd + 1)..]; + + if (string.IsNullOrEmpty(next.Text)) + { + htmlTag.HasTrailingNewline = next.HasTrailingNewline; + comment.Content.Remove(next); + } + + // Replace element + var insertPos = comment.Content.IndexOf(textComment); + comment.Content.RemoveAt(insertPos); + comment.Content.Insert(insertPos, htmlTag); } - foreach (var item in comment.Content.Where(c => c.Kind == DocumentationCommentKind.TextComment)) + + for (int i = 0; i < comment.Content.Count; i++) { - var textComment = (TextComment)item; + if (comment.Content[i].Kind != DocumentationCommentKind.InlineCommandComment) + continue; + + if (i + 1 >= comment.Content.Count) + continue; + + if (comment.Content[i + 1].Kind != DocumentationCommentKind.TextComment) + continue; + - if (textComment.Text.StartsWith("<", StringComparison.Ordinal)) - textComment.Text = $"{textComment.Text}>"; - else if (textComment.Text.StartsWith(">", StringComparison.Ordinal)) - textComment.Text = textComment.Text.Substring(1); + var textComment = (TextComment)comment.Content[i + 1]; + textComment.Text = Helpers.RegexCommentCommandLeftover.Replace( + textComment.Text, string.Empty); } return true; } diff --git a/tests/dotnet/Native/Passes.h b/tests/dotnet/Native/Passes.h index ee3c8c7a..fb2962c0 100644 --- a/tests/dotnet/Native/Passes.h +++ b/tests/dotnet/Native/Passes.h @@ -45,6 +45,43 @@ class TestCommentsPass { }; +/// A more complex test. +class TestCommentsPass2 +{ +public: + /// Gets a value + /// One + float GetValueWithComment() + { + return 1.0f; + } + + /// Sets a value. Get it with + /// The value to set + /// The parameter (typeof) + float SetValueWithParamComment(float value) + { + return value; + } + + /// + /// This method changes the point's location by + /// the given x- and y-offsets. + /// For example: + /// + /// Point p = new Point(3, 5); + /// p.Translate(-1, 3); + /// + /// results in p's having the value (2, 8). + /// + /// + /// The relative x-offset. + /// The relative y-offset. + void Translate(int dx, int dy) + { + } +}; + struct TestReadOnlyProperties { int readOnlyProperty;