Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix mapping of generic xref's to cref's #72

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 51 additions & 7 deletions Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -100,11 +101,11 @@ internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter

private static readonly string[] MarkdownHeaders = new[] { "[!NOTE]", "[!IMPORTANT]", "[!TIP]" };

// Note that we need to support generics that use the ` literal as well as the escaped %60
private static readonly string ValidRegexChars = @"[A-Za-z0-9\-\._~:\/#\[\]\{\}@!\$&'\(\)\*\+,;]|(%60|`)\d+";
// Note that we need to support generics that use the ` literal as well as any url encoded character
private static readonly string ValidRegexChars = @"[A-Za-z0-9\-\._~:\/#\[\]\{\}@!\$&'\(\)\*\+,;]|`\d+|%\w{2}";
private static readonly string ValidExtraChars = @"\?=";

private static readonly string RegexDocIdPattern = @"(?<prefix>[A-Za-z]{1}:)?(?<docId>(" + ValidRegexChars + @")+)(?<overload>%2[aA])?(?<extraVars>\?(" + ValidRegexChars + @")+=(" + ValidRegexChars + @")+)?";
private static readonly string RegexDocIdPattern = @"(?<prefix>[A-Za-z]{1}:)?(?<docId>(" + ValidRegexChars + @")+)?(?<extraVars>\?(" + ValidRegexChars + @")+=(" + ValidRegexChars + @")+)?";
private static readonly string RegexXmlCrefPattern = "cref=\"" + RegexDocIdPattern + "\"";
private static readonly string RegexMarkdownXrefPattern = @"(?<xref><xref:" + RegexDocIdPattern + ">)";

Expand Down Expand Up @@ -645,6 +646,7 @@ private static SyntaxTriviaList GetSeeAlsos(List<string> docsSeeAlsoCrefs, Synta
private static SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhitespace)
{
cref = RemoveCrefPrefix(cref);
cref = MapDocIdGenericsToCrefGenerics(cref);
XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref);
XmlEmptyElementSyntax emptyElement = SyntaxFactory.XmlEmptyElement(SyntaxFactory.XmlName(SyntaxFactory.Identifier("altmember")), new SyntaxList<XmlAttributeSyntax>(attribute));
return GetXmlTrivia(emptyElement, leadingWhitespace);
Expand Down Expand Up @@ -968,11 +970,53 @@ private static string ReplacePrimitives(string text)
private static string ReplaceDocId(Match m)
{
string docId = m.Groups["docId"].Value;
string overload = string.IsNullOrWhiteSpace(m.Groups["overload"].Value) ? "" : "O:";
string? prefix = m.Groups["prefix"].Value == "O:" ? "O:" : null;
docId = ReplacePrimitives(docId);
docId = Regex.Replace(docId, @"%60", "`");
docId = Regex.Replace(docId, @"`\d", "{T}");
return overload + docId;
docId = System.Net.WebUtility.UrlDecode(docId);

// Strip '*' character from the tail end of DocId names
if (docId.EndsWith('*'))
{
prefix = "O:";
docId = docId[..^1];
}

return prefix + MapDocIdGenericsToCrefGenerics(docId);
}

private static string MapDocIdGenericsToCrefGenerics(string docId)
{
// Map DocId generic parameters to Xml Doc generic parameters
// need to support both single and double backtick syntax
const string GenericParameterPattern = @"`{1,2}([\d+])";
int genericParameterArity = 0;
return Regex.Replace(docId, GenericParameterPattern, MapDocIdGenericParameterToXmlDocGenericParameter);

string MapDocIdGenericParameterToXmlDocGenericParameter(Match match)
{
int index = int.Parse(match.Groups[1].Value);

if (genericParameterArity == 0)
{
// this is the first match that declares the generic parameter arity of the method
// e.g. GenericMethod``3 ---> GenericMethod{T1,T2,T3}(...);
Debug.Assert(index > 0);
genericParameterArity = index;
return WrapInCurlyBrackets(string.Join(",", Enumerable.Range(0, index).Select(CreateGenericParameterName)));
}

// Subsequent matches are references to generic parameters in the method signature,
// e.g. GenericMethod{T1,T2,T3}(..., List{``1} parameter, ...); ---> List{T2} parameter
return CreateGenericParameterName(index);

// NB this naming scheme does not map to the exact generic parameter names,
// however this is still accepted by intellisense and backporters can rename
// manually with the help of tooling.
string CreateGenericParameterName(int index)
=> genericParameterArity == 1 ? "T" : $"T{index + 1}";

static string WrapInCurlyBrackets(string input) => $"{{{input}}}";
}
}

private static string CrefEvaluator(Match m)
Expand Down
4 changes: 2 additions & 2 deletions Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ public int MyIntMethod(int param1, int param2)
/// <remarks>These are the MyVoidMethod remarks.
/// Multiple lines.
/// Mentions the <see cref="System.ArgumentNullException" />.
/// Also mentions an overloaded method DocID: <see cref="MyNamespace.MyType.MyIntMethod" />.
/// And also mentions an overloaded method DocID with displayProperty which should be ignored when porting: <see cref="MyNamespace.MyType.MyIntMethod" />.</remarks>
/// Also mentions an overloaded method DocID: <see cref="O:MyNamespace.MyType.MyIntMethod" />.
/// And also mentions an overloaded method DocID with displayProperty which should be ignored when porting: <see cref="O:MyNamespace.MyType.MyIntMethod" />.</remarks>
public void MyVoidMethod()
{
}
Expand Down
30 changes: 30 additions & 0 deletions Tests/PortToTripleSlash/TestData/Generics/MyGenericType.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Type Name="MyGenericType&lt;T&gt;" FullName="MyNamespace.MyGenericType">
<TypeSignature Language="DocId" Value="T:MyNamespace.MyGenericType" />
<AssemblyInfo>
<AssemblyName>MyAssembly</AssemblyName>
</AssemblyInfo>
<Docs>
<summary>This is the MyGenericType static class summary.</summary>
</Docs>
<Members>
<Member MemberName="Select&lt;TSource,TResult&gt;">
<MemberSignature Language="DocId" Value="M:MyNamespace.MyGenericType.Select``2(MyNamespace.MyGenericType{``0},System.Func{``0,``1})" />
<Docs>
<summary>Projects each element into a new form.</summary>
<typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
<typeparam name="TResult">The type of the value returned by <paramref name="selector" />.</typeparam>
<param name="source">A sequence of values to invoke a transform function on.</param>
<param name="selector">A transform function to apply to each element.</param>
<altmember cref="System.Linq.Enumerable.Any``1(System.Collections.Generic.IEnumerable{``0},System.Func{``0,System.Boolean})"/>
<remarks>
<format type="text/markdown">
<![CDATA[
## Remarks
Here's a reference to <xref:MyNamespace.MyGenericType.Select%60%602%28MyNamespace.MyGenericType%7B%60%600%7D%2CSystem.Func%7B%60%600%2C%60%601%7D%29>.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😮

]]>
</format>
</remarks>
</Docs>
</Member>
</Members>
</Type>
13 changes: 13 additions & 0 deletions Tests/PortToTripleSlash/TestData/Generics/SourceExpected.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,17 @@ public class MyGenericType<T>
// Original MyGenericType<T>.Enumerator class comments with information for maintainers, must stay.
public class Enumerator { }
}

/// <summary>This is the MyGenericType static class summary.</summary>
public static class MyGenericType
{
/// <summary>Projects each element into a new form.</summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
/// <typeparam name="TResult">The type of the value returned by <paramref name="selector" />.</typeparam>
/// <param name="source">A sequence of values to invoke a transform function on.</param>
/// <param name="selector">A transform function to apply to each element.</param>
/// <remarks>Here's a reference to <see cref="MyNamespace.MyGenericType.Select{T1,T2}(MyNamespace.MyGenericType{T1},System.Func{T1,T2})" />.</remarks>
/// <altmember cref="System.Linq.Enumerable.Any{T}(System.Collections.Generic.IEnumerable{T},System.Func{T,System.Boolean})"/>
public static MyGenericType<TResult> Select<TSource, TResult>(this MyGenericType<TSource> source, Func<TSource, TResult> selector) => null;
}
}
5 changes: 5 additions & 0 deletions Tests/PortToTripleSlash/TestData/Generics/SourceOriginal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ public class MyGenericType<T>
// Original MyGenericType<T>.Enumerator class comments with information for maintainers, must stay.
public class Enumerator { }
}

public static class MyGenericType
{
public static MyGenericType<TResult> Select<TSource, TResult>(this MyGenericType<TSource> source, Func<TSource, TResult> selector) => null;
}
}