diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 1776b39..46a4b84 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -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; @@ -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 = @"(?[A-Za-z]{1}:)?(?(" + ValidRegexChars + @")+)(?%2[aA])?(?\?(" + ValidRegexChars + @")+=(" + ValidRegexChars + @")+)?"; + private static readonly string RegexDocIdPattern = @"(?[A-Za-z]{1}:)?(?(" + ValidRegexChars + @")+)?(?\?(" + ValidRegexChars + @")+=(" + ValidRegexChars + @")+)?"; private static readonly string RegexXmlCrefPattern = "cref=\"" + RegexDocIdPattern + "\""; private static readonly string RegexMarkdownXrefPattern = @"(?)"; @@ -645,6 +646,7 @@ private static SyntaxTriviaList GetSeeAlsos(List 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(attribute)); return GetXmlTrivia(emptyElement, leadingWhitespace); @@ -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) diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index a93b747..527f538 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -107,8 +107,8 @@ public int MyIntMethod(int param1, int param2) /// These are the MyVoidMethod remarks. /// Multiple lines. /// Mentions the . - /// Also mentions an overloaded method DocID: . - /// And also mentions an overloaded method DocID with displayProperty which should be ignored when porting: . + /// Also mentions an overloaded method DocID: . + /// And also mentions an overloaded method DocID with displayProperty which should be ignored when porting: . public void MyVoidMethod() { } diff --git a/Tests/PortToTripleSlash/TestData/Generics/MyGenericType.xml b/Tests/PortToTripleSlash/TestData/Generics/MyGenericType.xml new file mode 100644 index 0000000..dff699f --- /dev/null +++ b/Tests/PortToTripleSlash/TestData/Generics/MyGenericType.xml @@ -0,0 +1,30 @@ + + + + MyAssembly + + + This is the MyGenericType static class summary. + + + + + + Projects each element into a new form. + The type of the elements of . + The type of the value returned by . + A sequence of values to invoke a transform function on. + A transform function to apply to each element. + + + + . + ]]> + + + + + + \ No newline at end of file diff --git a/Tests/PortToTripleSlash/TestData/Generics/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Generics/SourceExpected.cs index ae85c0f..a6c1dec 100644 --- a/Tests/PortToTripleSlash/TestData/Generics/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Generics/SourceExpected.cs @@ -11,4 +11,17 @@ public class MyGenericType // Original MyGenericType.Enumerator class comments with information for maintainers, must stay. public class Enumerator { } } + + /// This is the MyGenericType static class summary. + public static class MyGenericType + { + /// Projects each element into a new form. + /// The type of the elements of . + /// The type of the value returned by . + /// A sequence of values to invoke a transform function on. + /// A transform function to apply to each element. + /// Here's a reference to . + /// + public static MyGenericType Select(this MyGenericType source, Func selector) => null; + } } diff --git a/Tests/PortToTripleSlash/TestData/Generics/SourceOriginal.cs b/Tests/PortToTripleSlash/TestData/Generics/SourceOriginal.cs index 3d91be3..6e8d502 100644 --- a/Tests/PortToTripleSlash/TestData/Generics/SourceOriginal.cs +++ b/Tests/PortToTripleSlash/TestData/Generics/SourceOriginal.cs @@ -8,4 +8,9 @@ public class MyGenericType // Original MyGenericType.Enumerator class comments with information for maintainers, must stay. public class Enumerator { } } + + public static class MyGenericType + { + public static MyGenericType Select(this MyGenericType source, Func selector) => null; + } }