Skip to content

Commit ab71a5d

Browse files
committed
Implement support for paramref and typeparamref in Markdown
1 parent 6e3ce41 commit ab71a5d

5 files changed

Lines changed: 182 additions & 16 deletions

File tree

DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/RefactoringRules/DOC900CodeFixProvider+DocumentationCommentPrinter.cs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33

44
namespace DocumentationAnalyzers.RefactoringRules
55
{
6+
using System;
67
using System.Collections.Generic;
78
using System.Globalization;
89
using System.Text;
910
using CommonMark;
1011
using CommonMark.Syntax;
12+
using DocumentationAnalyzers.Helpers;
13+
using Microsoft.CodeAnalysis;
1114

1215
internal partial class DOC900CodeFixProvider
1316
{
@@ -41,10 +44,10 @@ internal static class DocumentationCommentPrinter
4144
/// Convert a block list to HTML. Returns 0 on success, and sets result.
4245
/// </summary>
4346
/// <remarks>Orig: blocks_to_html.</remarks>
44-
public static void BlocksToHtml(System.IO.TextWriter writer, Block block, CommonMarkSettings settings)
47+
public static void BlocksToHtml(System.IO.TextWriter writer, Block block, CommonMarkSettings settings, ISymbol documentedSymbol)
4548
{
4649
var wrapper = new DocumentationCommentTextWriter(writer);
47-
BlocksToHtmlInner(wrapper, block, settings);
50+
BlocksToHtmlInner(wrapper, block, settings, documentedSymbol);
4851
}
4952

5053
/// <summary>
@@ -215,7 +218,7 @@ private static void EscapeHtml(StringContent inp, DocumentationCommentTextWriter
215218
target.Write(buffer, lastPos - 0, part.Length - lastPos + 0);
216219
}
217220

218-
private static void BlocksToHtmlInner(DocumentationCommentTextWriter writer, Block block, CommonMarkSettings settings)
221+
private static void BlocksToHtmlInner(DocumentationCommentTextWriter writer, Block block, CommonMarkSettings settings, ISymbol documentedSymbol)
219222
{
220223
var stack = new Stack<BlockStackEntry>();
221224
var inlineStack = new Stack<InlineStackEntry>();
@@ -240,13 +243,13 @@ private static void BlocksToHtmlInner(DocumentationCommentTextWriter writer, Blo
240243
case BlockTag.Paragraph:
241244
if (tight)
242245
{
243-
InlinesToHtml(writer, block.InlineContent, settings, inlineStack);
246+
InlinesToHtml(writer, block.InlineContent, settings, documentedSymbol, inlineStack);
244247
}
245248
else
246249
{
247250
writer.EnsureLine();
248251
writer.WriteConstant("<para>");
249-
InlinesToHtml(writer, block.InlineContent, settings, inlineStack);
252+
InlinesToHtml(writer, block.InlineContent, settings, documentedSymbol, inlineStack);
250253
writer.WriteLineConstant("</para>");
251254
}
252255

@@ -295,7 +298,7 @@ private static void BlocksToHtmlInner(DocumentationCommentTextWriter writer, Blo
295298

296299
x = block.Heading.Level;
297300
writer.WriteConstant(x > 0 && x < 7 ? HeaderOpenerTags[x - 1] : "<h" + x.ToString(CultureInfo.InvariantCulture) + ">");
298-
InlinesToHtml(writer, block.InlineContent, settings, inlineStack);
301+
InlinesToHtml(writer, block.InlineContent, settings, documentedSymbol, inlineStack);
299302
writer.WriteLineConstant(x > 0 && x < 7 ? HeaderCloserTags[x - 1] : "</h" + x.ToString(CultureInfo.InvariantCulture) + ">");
300303
break;
301304

@@ -463,7 +466,7 @@ private static void InlinesToPlainText(DocumentationCommentTextWriter writer, In
463466
/// <summary>
464467
/// Writes the inline list to the given writer as HTML code.
465468
/// </summary>
466-
private static void InlinesToHtml(DocumentationCommentTextWriter writer, Inline inline, CommonMarkSettings settings, Stack<InlineStackEntry> stack)
469+
private static void InlinesToHtml(DocumentationCommentTextWriter writer, Inline inline, CommonMarkSettings settings, ISymbol documentedSymbol, Stack<InlineStackEntry> stack)
467470
{
468471
var uriResolver = settings.UriResolver;
469472
bool withinLink = false;
@@ -498,9 +501,25 @@ private static void InlinesToHtml(DocumentationCommentTextWriter writer, Inline
498501
break;
499502

500503
case InlineTag.Code:
501-
writer.WriteConstant("<c>");
502-
EscapeHtml(inline.LiteralContent, writer);
503-
writer.WriteConstant("</c>");
504+
if (documentedSymbol.HasAnyParameter(inline.LiteralContent, StringComparer.Ordinal))
505+
{
506+
writer.WriteConstant("<paramref name=\"");
507+
EscapeHtml(inline.LiteralContent, writer);
508+
writer.WriteConstant("\"/>");
509+
}
510+
else if (documentedSymbol.HasAnyTypeParameter(inline.LiteralContent, StringComparer.Ordinal))
511+
{
512+
writer.WriteConstant("<typeparamref name=\"");
513+
EscapeHtml(inline.LiteralContent, writer);
514+
writer.WriteConstant("\"/>");
515+
}
516+
else
517+
{
518+
writer.WriteConstant("<c>");
519+
EscapeHtml(inline.LiteralContent, writer);
520+
writer.WriteConstant("</c>");
521+
}
522+
504523
break;
505524

506525
case InlineTag.RawHtml:

DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/RefactoringRules/DOC900CodeFixProvider.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,11 @@ private async Task<Document> CreateChangedDocumentAsync(CodeFixContext context,
155155
SyntaxTrivia leadingTrivia = SyntaxFactory.DocumentationCommentExterior(leadingTriviaBuilder.ToString());
156156

157157
string newLineText = context.Document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp);
158+
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken);
159+
var documentedSymbol = semanticModel.GetDeclaredSymbol(parentToken.Parent.FirstAncestorOrSelf<SyntaxNode>(SyntaxNodeExtensionsEx.IsSymbolDeclaration), context.CancellationToken);
160+
158161
DocumentationCommentTriviaSyntax contentsOnly = RemoveExteriorTrivia(documentationCommentTriviaSyntax);
159-
contentsOnly = contentsOnly.ReplaceNodes(contentsOnly.ChildNodes(), (originalNode, rewrittenNode) => RenderBlockElementAsMarkdown(originalNode, rewrittenNode, newLineText));
162+
contentsOnly = contentsOnly.ReplaceNodes(contentsOnly.ChildNodes(), (originalNode, rewrittenNode) => RenderBlockElementAsMarkdown(originalNode, rewrittenNode, newLineText, documentedSymbol));
160163
string renderedContent = contentsOnly.Content.ToFullString();
161164
string[] lines = renderedContent.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
162165
SyntaxList<XmlNodeSyntax> newContent = XmlSyntaxFactory.List();
@@ -262,7 +265,7 @@ private SyntaxNode RemoveUnnecessaryParagraphs(XmlElementSyntax originalNode, Xm
262265
return rewrittenNode.WithContent(content);
263266
}
264267

265-
private SyntaxNode RenderBlockElementAsMarkdown(SyntaxNode originalNode, SyntaxNode rewrittenNode, string newLineText)
268+
private SyntaxNode RenderBlockElementAsMarkdown(SyntaxNode originalNode, SyntaxNode rewrittenNode, string newLineText, ISymbol documentedSymbol)
266269
{
267270
if (!(rewrittenNode is XmlElementSyntax elementSyntax))
268271
{
@@ -282,7 +285,7 @@ private SyntaxNode RenderBlockElementAsMarkdown(SyntaxNode originalNode, SyntaxN
282285
return rewrittenNode;
283286
}
284287

285-
string rendered = RenderAsMarkdown(elementSyntax.Content.ToString()).Trim();
288+
string rendered = RenderAsMarkdown(elementSyntax.Content.ToString(), documentedSymbol).Trim();
286289
return elementSyntax.WithContent(
287290
XmlSyntaxFactory.List(
288291
XmlSyntaxFactory.NewLine(newLineText).WithoutTrailingTrivia(),
@@ -291,7 +294,7 @@ private SyntaxNode RenderBlockElementAsMarkdown(SyntaxNode originalNode, SyntaxN
291294
XmlSyntaxFactory.Text(" ")));
292295
}
293296

294-
private string RenderAsMarkdown(string text)
297+
private string RenderAsMarkdown(string text, ISymbol documentedSymbol)
295298
{
296299
Block document;
297300
using (var reader = new StringReader(text))
@@ -303,7 +306,7 @@ private string RenderAsMarkdown(string text)
303306
StringBuilder builder = new StringBuilder();
304307
using (var writer = new StringWriter(builder))
305308
{
306-
DocumentationCommentPrinter.BlocksToHtml(writer, document, CommonMarkSettings.Default);
309+
DocumentationCommentPrinter.BlocksToHtml(writer, document, CommonMarkSettings.Default, documentedSymbol);
307310
}
308311

309312
return builder.ToString();

DocumentationAnalyzers/DocumentationAnalyzers.Test/RefactoringRules/DOC900UnitTests.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ void Method(int param) { }
179179
var fixedCode = @"
180180
class TestClass {
181181
///$$ <summary>
182-
/// Provide a value for <c>param</c>.
182+
/// Provide a value for <paramref name=""param""/>.
183183
/// </summary>
184184
void Method(int param) { }
185185
}
@@ -197,5 +197,44 @@ void Method(int param) { }
197197
NumberOfIncrementalIterations = 2,
198198
}.RunAsync();
199199
}
200+
201+
[Fact]
202+
public async Task TestLocalTypeParameterReferenceAsync()
203+
{
204+
var testCode = @"
205+
///$$ <summary>
206+
/// Provide a value for `T`.
207+
/// </summary>
208+
class TestClass<T> {
209+
///$$ <summary>
210+
/// Provide a value for `T2`.
211+
/// </summary>
212+
void Method<T2>() { }
213+
}
214+
";
215+
var fixedCode = @"
216+
///$$ <summary>
217+
/// Provide a value for <typeparamref name=""T""/>.
218+
/// </summary>
219+
class TestClass<T> {
220+
///$$ <summary>
221+
/// Provide a value for <typeparamref name=""T2""/>.
222+
/// </summary>
223+
void Method<T2>() { }
224+
}
225+
";
226+
227+
await new CSharpCodeFixTest<DOC900RenderAsMarkdown, DOC900CodeFixProvider, XUnitVerifier>
228+
{
229+
TestCode = testCode,
230+
FixedCode = fixedCode,
231+
FixedState = { MarkupHandling = MarkupMode.Allow },
232+
BatchFixedState = { MarkupHandling = MarkupMode.Allow },
233+
234+
// The first iteration fully renders the documentation. The second iteration offers a code fix to render
235+
// documentation, but no changes are made by the fix so the iterations stop.
236+
NumberOfIncrementalIterations = 3,
237+
}.RunAsync();
238+
}
200239
}
201240
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT license. See LICENSE in the project root for license information.
3+
4+
namespace DocumentationAnalyzers.Helpers
5+
{
6+
using System;
7+
using System.Linq;
8+
using Microsoft.CodeAnalysis;
9+
10+
internal static class SymbolExtensions
11+
{
12+
/// <summary>
13+
/// Determines if a symbol has a parameter with a given name.
14+
/// </summary>
15+
/// <param name="symbol">The symbol.</param>
16+
/// <param name="name">The name of the parameter.</param>
17+
/// <param name="comparer">The comparer to use for symbol names.</param>
18+
/// <returns><see langword="true"/> if a parameter with the name <paramref name="name"/> is accessible in the
19+
/// context of the specified <paramref name="symbol"/>; otherwise, <see langword="false"/>.</returns>
20+
public static bool HasAnyParameter(this ISymbol symbol, string name, StringComparer comparer)
21+
{
22+
if (symbol.Kind == SymbolKind.Method)
23+
{
24+
var methodSymbol = (IMethodSymbol)symbol;
25+
return methodSymbol.Parameters.Any(parameter => comparer.Equals(parameter.Name, name));
26+
}
27+
28+
return false;
29+
}
30+
31+
/// <summary>
32+
/// Determines if a symbol has a type parameter with a given name.
33+
/// </summary>
34+
/// <param name="symbol">The symbol.</param>
35+
/// <param name="name">The name of the type parameter.</param>
36+
/// <param name="comparer">The comparer to use for symbol names.</param>
37+
/// <returns><see langword="true"/> if a type parameter with the name <paramref name="name"/> is accessible in
38+
/// the context of the specified <paramref name="symbol"/>; otherwise, <see langword="false"/>.</returns>
39+
public static bool HasAnyTypeParameter(this ISymbol symbol, string name, StringComparer comparer)
40+
{
41+
for (var currentSymbol = symbol; currentSymbol != null; currentSymbol = currentSymbol.ContainingSymbol)
42+
{
43+
switch (currentSymbol.Kind)
44+
{
45+
case SymbolKind.NamedType:
46+
if (((INamedTypeSymbol)symbol).TypeParameters.Any(parameter => comparer.Equals(parameter.Name, name)))
47+
{
48+
return true;
49+
}
50+
51+
break;
52+
53+
case SymbolKind.Method:
54+
if (((IMethodSymbol)symbol).TypeParameters.Any(parameter => comparer.Equals(parameter.Name, name)))
55+
{
56+
return true;
57+
}
58+
59+
break;
60+
}
61+
}
62+
63+
return false;
64+
}
65+
}
66+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT license. See LICENSE in the project root for license information.
3+
4+
namespace DocumentationAnalyzers.Helpers
5+
{
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
9+
internal static class SyntaxNodeExtensionsEx
10+
{
11+
public static bool IsSymbolDeclaration(this SyntaxNode node)
12+
{
13+
switch (node.Kind())
14+
{
15+
case SyntaxKind.ClassDeclaration:
16+
case SyntaxKind.ConstructorDeclaration:
17+
case SyntaxKind.ConversionOperatorDeclaration:
18+
case SyntaxKind.DelegateDeclaration:
19+
case SyntaxKind.DestructorDeclaration:
20+
case SyntaxKind.EnumDeclaration:
21+
case SyntaxKind.EnumMemberDeclaration:
22+
case SyntaxKind.EventDeclaration:
23+
case SyntaxKind.EventFieldDeclaration:
24+
case SyntaxKind.FieldDeclaration:
25+
case SyntaxKind.IndexerDeclaration:
26+
case SyntaxKind.InterfaceDeclaration:
27+
case SyntaxKind.MethodDeclaration:
28+
case SyntaxKind.NamespaceDeclaration:
29+
case SyntaxKind.OperatorDeclaration:
30+
case SyntaxKind.PropertyDeclaration:
31+
case SyntaxKind.StructDeclaration:
32+
return true;
33+
34+
default:
35+
return false;
36+
}
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)