Skip to content

Commit 2638651

Browse files
committed
Improved XML style comment parsing
1 parent 1978e3b commit 2638651

File tree

5 files changed

+266
-27
lines changed

5 files changed

+266
-27
lines changed

src/AST/Comment.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34

45
namespace CppSharp.AST
56
{
@@ -409,10 +410,17 @@ public struct Attribute
409410
{
410411
public string Name;
411412
public string Value;
413+
414+
public override string ToString()
415+
{
416+
return $"{Name}=\"{Value}\"";
417+
}
412418
}
413419

414420
public List<Attribute> Attributes;
415421

422+
public bool SelfClosing { get; set; }
423+
416424
public HTMLStartTagComment()
417425
{
418426
Kind = DocumentationCommentKind.HTMLStartTagComment;
@@ -423,6 +431,15 @@ public override void Visit<T>(ICommentVisitor<T> visitor)
423431
{
424432
visitor.VisitHTMLStartTag(this);
425433
}
434+
435+
public override string ToString()
436+
{
437+
var attrStr = string.Empty;
438+
if (Attributes.Count != 0)
439+
attrStr = " " + string.Join(' ', Attributes.Select(x => x.ToString()));
440+
441+
return $"<{TagName}{attrStr}{(SelfClosing ? "/" : "")}>";
442+
}
426443
}
427444

428445
/// <summary>
@@ -439,6 +456,11 @@ public override void Visit<T>(ICommentVisitor<T> visitor)
439456
{
440457
visitor.VisitHTMLEndTag(this);
441458
}
459+
460+
public override string ToString()
461+
{
462+
return $"</{TagName}>";
463+
}
442464
}
443465

444466
/// <summary>
@@ -457,6 +479,13 @@ public override void Visit<T>(ICommentVisitor<T> visitor)
457479
{
458480
visitor.VisitText(this);
459481
}
482+
483+
public override string ToString()
484+
{
485+
return Text;
486+
}
487+
488+
public bool IsEmpty => string.IsNullOrEmpty(Text) && !HasTrailingNewline;
460489
}
461490

462491
/// <summary>

src/Generator.Tests/Passes/TestPasses.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,38 @@ public void TestFunctionToStaticPass()
123123
[Test]
124124
public void TestCleanCommentsPass()
125125
{
126-
var c = AstContext.FindClass("TestCommentsPass").FirstOrDefault();
126+
var c = AstContext.Class("TestCommentsPass");
127+
var c2 = AstContext.Class("TestCommentsPass2");
127128

128129
passBuilder.AddPass(new CleanCommentsPass());
129130
passBuilder.RunPasses(pass => pass.VisitDeclaration(c));
131+
passBuilder.RunPasses(pass => pass.VisitClassDecl(c2));
130132

131133
var para = (ParagraphComment)c.Comment.FullComment.Blocks[0];
132134
var textGenerator = new TextGenerator();
133135
textGenerator.Print(para, CommentKind.BCPLSlash);
134136

135137
Assert.That(textGenerator.StringBuilder.ToString().Trim(),
136138
Is.EqualTo("/// <summary>A simple test.</summary>"));
139+
140+
var textGenerator2 = new TextGenerator();
141+
textGenerator2.Print(c2.Methods[0].Comment.FullComment, CommentKind.BCPLSlash);
142+
143+
Assert.That(textGenerator2.StringBuilder.ToString().Trim(),
144+
Is.EqualTo(
145+
"/// <summary>Gets a value</summary>\n" +
146+
"/// <returns>One</returns>"
147+
));
148+
149+
var textGenerator3 = new TextGenerator();
150+
textGenerator3.Print(c2.Methods[1].Comment.FullComment, CommentKind.BCPLSlash);
151+
152+
Assert.That(textGenerator3.StringBuilder.ToString().Trim(),
153+
Is.EqualTo(
154+
"/// <summary>Sets a value. Get it with <see cref=\"GetValueWithComment\"/></summary>\n" +
155+
"/// <param name=\"value\">The value to set</param>\n" +
156+
"/// <returns>The parameter (typeof<float>)</returns>"
157+
));
137158
}
138159

139160
[Test]

src/Generator/Generators/CSharp/CSharpCommentPrinter.cs

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ private static void GetCommentSections(this Comment comment, List<Section> secti
3737
switch (blockCommandComment.CommandKind)
3838
{
3939
case CommentCommandKind.Brief:
40+
sections.Add(new Section(CommentElement.Summary));
4041
blockCommandComment.ParagraphComment.GetCommentSections(sections);
4142
break;
4243
case CommentCommandKind.Return:
@@ -85,14 +86,23 @@ private static void GetCommentSections(this Comment comment, List<Section> secti
8586
break;
8687
}
8788
case DocumentationCommentKind.ParagraphComment:
88-
var summaryParagraph = sections.Count == 1;
89-
var paragraphComment = (ParagraphComment)comment;
89+
{
90+
bool summaryParagraph = false;
91+
if (sections.Count == 0)
92+
{
93+
sections.Add(new Section(CommentElement.Summary));
94+
summaryParagraph = true;
95+
}
96+
9097
var lastParagraphSection = sections.Last();
98+
var paragraphComment = (ParagraphComment)comment;
9199
foreach (var inlineContentComment in paragraphComment.Content)
92100
{
93101
inlineContentComment.GetCommentSections(sections);
94102
if (inlineContentComment.HasTrailingNewline)
95103
lastParagraphSection.NewLine();
104+
105+
lastParagraphSection = sections.Last();
96106
}
97107

98108
if (!string.IsNullOrEmpty(lastParagraphSection.CurrentLine.ToString()))
@@ -132,10 +142,33 @@ private static void GetCommentSections(this Comment comment, List<Section> secti
132142
}
133143
case DocumentationCommentKind.HTMLStartTagComment:
134144
{
145+
var startTag = (HTMLStartTagComment)comment;
146+
var sectionType = CommentElementFromTag(startTag.TagName);
147+
148+
if (IsInlineCommentElement(sectionType))
149+
{
150+
var lastSection = sections.Last();
151+
lastSection.CurrentLine.Append(startTag);
152+
break;
153+
}
154+
155+
sections.Add(new Section(sectionType)
156+
{
157+
Attributes = startTag.Attributes.Select(a => a.ToString()).ToList()
158+
});
135159
break;
136160
}
137161
case DocumentationCommentKind.HTMLEndTagComment:
138162
{
163+
var endTag = (HTMLEndTagComment)comment;
164+
var sectionType = CommentElementFromTag(endTag.TagName);
165+
166+
if (IsInlineCommentElement(sectionType))
167+
{
168+
var lastSection = sections.Last();
169+
lastSection.CurrentLine.Append(endTag);
170+
}
171+
139172
break;
140173
}
141174
case DocumentationCommentKind.HTMLTagComment:
@@ -252,15 +285,71 @@ public List<string> GetLines()
252285
}
253286
}
254287

288+
private static CommentElement CommentElementFromTag(string tag)
289+
{
290+
return tag.ToLowerInvariant() switch
291+
{
292+
"c" => CommentElement.C,
293+
"code" => CommentElement.Code,
294+
"example" => CommentElement.Example,
295+
"exception" => CommentElement.Exception,
296+
"include" => CommentElement.Include,
297+
"list" => CommentElement.List,
298+
"para" => CommentElement.Para,
299+
"param" => CommentElement.Param,
300+
"paramref" => CommentElement.ParamRef,
301+
"permission" => CommentElement.Permission,
302+
"remarks" => CommentElement.Remarks,
303+
"return" or "returns" => CommentElement.Returns,
304+
"summary" => CommentElement.Summary,
305+
"typeparam" => CommentElement.TypeParam,
306+
"typeparamref" => CommentElement.TypeParamRef,
307+
"value" => CommentElement.Value,
308+
"seealso" => CommentElement.SeeAlso,
309+
"see" => CommentElement.See,
310+
"inheritdoc" => CommentElement.InheritDoc,
311+
_ => CommentElement.Unknown
312+
};
313+
}
314+
315+
/// <summary>From https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/documentation-comments#d3-recommended-tags</summary>
316+
/// <remarks>Enum value is equal to sorting priority</remarks>
255317
private enum CommentElement
256318
{
257-
Summary,
258-
Typeparam,
259-
Param,
260-
Returns,
261-
Exception,
262-
Remarks,
263-
Example
319+
C = 1000, // Set text in a code-like font
320+
Code = 1001, // Set one or more lines of source code or program output
321+
Example = 11, // Indicate an example
322+
Exception = 8, // Identifies the exceptions a method can throw
323+
Include = 1, // Includes XML from an external file
324+
List = 1002, // Create a list or table
325+
Para = 1003, // Permit structure to be added to text
326+
Param = 5, // Describe a parameter for a method or constructor
327+
ParamRef = 1004, // Identify that a word is a parameter name
328+
Permission = 7, // Document the security accessibility of a member
329+
Remarks = 9, // Describe additional information about a type
330+
Returns = 6, // Describe the return value of a method
331+
See = 1005, // Specify a link
332+
SeeAlso = 10, // Generate a See Also entry
333+
Summary = 2, // Describe a type or a member of a type
334+
TypeParam = 4, // Describe a type parameter for a generic type or method
335+
TypeParamRef = 1006, // Identify that a word is a type parameter name
336+
Value = 3, // Describe a property
337+
InheritDoc = 0, // Inherit documentation from a base class
338+
Unknown = 9999, // Unknown tag
264339
}
340+
341+
private static bool IsInlineCommentElement(CommentElement element) =>
342+
element switch
343+
{
344+
CommentElement.C => true,
345+
CommentElement.Code => true,
346+
CommentElement.List => true,
347+
CommentElement.Para => true,
348+
CommentElement.ParamRef => true,
349+
CommentElement.See => true,
350+
CommentElement.TypeParamRef => true,
351+
CommentElement.Unknown => true, // Print unknown tags as inline
352+
_ => ((int)element) >= 1000
353+
};
265354
}
266355
}

src/Generator/Passes/CleanCommentsPass.cs

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ namespace CppSharp.Passes
77
{
88
public class CleanCommentsPass : TranslationUnitPass, ICommentVisitor<bool>
99
{
10-
public CleanCommentsPass() => VisitOptions.ResetFlags(
11-
VisitFlags.ClassBases | VisitFlags.FunctionReturnType |
12-
VisitFlags.TemplateArguments);
13-
1410
public bool VisitBlockCommand(BlockCommandComment comment) => true;
1511

1612
public override bool VisitParameterDecl(Parameter parameter) =>
@@ -46,25 +42,92 @@ public bool VisitFull(FullComment comment)
4642

4743
public bool VisitParagraph(ParagraphComment comment)
4844
{
49-
for (int i = 0; i < comment.Content.Count; i++)
45+
// Fix clang parsing html tags as TextComment's
46+
var textComments = comment.Content
47+
.Where(c => c.Kind == DocumentationCommentKind.TextComment)
48+
.Cast<TextComment>()
49+
.ToArray();
50+
51+
for (var i = 0; i < textComments.Length; ++i)
5052
{
51-
if (comment.Content[i].Kind == DocumentationCommentKind.InlineCommandComment &&
52-
i + 1 < comment.Content.Count &&
53-
comment.Content[i + 1].Kind == DocumentationCommentKind.TextComment)
53+
TextComment textComment = textComments[i];
54+
if (textComment.IsEmpty)
55+
{
56+
comment.Content.Remove(textComment);
57+
continue;
58+
}
59+
60+
if (!textComment.Text.StartsWith('<'))
61+
continue;
62+
63+
if (textComment.Text.Length < 2 || i + 1 >= textComments.Length)
64+
continue;
65+
66+
bool isEndTag = textComment.Text[1] == '/';
67+
68+
// Replace the TextComment node with a HTMLTagComment node
69+
HTMLTagComment htmlTag = isEndTag ? new HTMLEndTagComment() : new HTMLStartTagComment();
70+
htmlTag.TagName = textComment.Text[(1 + (isEndTag ? 1 : 0))..];
71+
72+
// Cleanup next element
73+
TextComment next = textComments[i + 1];
74+
int tagEnd = next.Text.IndexOf('>');
75+
if (tagEnd == -1)
76+
continue;
77+
78+
if (!isEndTag)
5479
{
55-
var textComment = (TextComment)comment.Content[i + 1];
56-
textComment.Text = Helpers.RegexCommentCommandLeftover.Replace(
57-
textComment.Text, string.Empty);
80+
var startTag = (htmlTag as HTMLStartTagComment)!;
81+
var tagRemains = next.Text[..tagEnd];
82+
83+
if (tagRemains.EndsWith('/'))
84+
{
85+
startTag.SelfClosing = true;
86+
tagRemains = tagRemains[..^1];
87+
}
88+
89+
var attributes = tagRemains.Split(' ', StringSplitOptions.RemoveEmptyEntries);
90+
foreach (var attribute in attributes)
91+
{
92+
var args = attribute.Split('=');
93+
startTag.Attributes.Add(new HTMLStartTagComment.Attribute
94+
{
95+
Name = args[0].Trim(),
96+
Value = args.ElementAtOrDefault(1)?.Trim(' ', '"'),
97+
});
98+
}
5899
}
100+
101+
// Strip tagRemains from next element
102+
next.Text = next.Text[(tagEnd + 1)..];
103+
104+
if (string.IsNullOrEmpty(next.Text))
105+
{
106+
htmlTag.HasTrailingNewline = next.HasTrailingNewline;
107+
comment.Content.Remove(next);
108+
}
109+
110+
// Replace element
111+
var insertPos = comment.Content.IndexOf(textComment);
112+
comment.Content.RemoveAt(insertPos);
113+
comment.Content.Insert(insertPos, htmlTag);
59114
}
60-
foreach (var item in comment.Content.Where(c => c.Kind == DocumentationCommentKind.TextComment))
115+
116+
for (int i = 0; i < comment.Content.Count; i++)
61117
{
62-
var textComment = (TextComment)item;
118+
if (comment.Content[i].Kind != DocumentationCommentKind.InlineCommandComment)
119+
continue;
120+
121+
if (i + 1 >= comment.Content.Count)
122+
continue;
123+
124+
if (comment.Content[i + 1].Kind != DocumentationCommentKind.TextComment)
125+
continue;
126+
63127

64-
if (textComment.Text.StartsWith("<", StringComparison.Ordinal))
65-
textComment.Text = $"{textComment.Text}>";
66-
else if (textComment.Text.StartsWith(">", StringComparison.Ordinal))
67-
textComment.Text = textComment.Text.Substring(1);
128+
var textComment = (TextComment)comment.Content[i + 1];
129+
textComment.Text = Helpers.RegexCommentCommandLeftover.Replace(
130+
textComment.Text, string.Empty);
68131
}
69132
return true;
70133
}

0 commit comments

Comments
 (0)