lijunle / Vsxmd

VS XML documentation -> Markdown syntax.
MIT License
215 stars 51 forks source link

Generic types are not generated correctly in the markdown #85

Open astrohart opened 3 years ago

astrohart commented 3 years ago

I have a generic interface

public interface IFloatyThing<T> where T : Balloon
{
   /* ... */
}

and another generic interface

public interface IRockCandy<S, T> where S : Flavor, new() where T : Shape, new()
{
   /* ... */
}

These are generated in the Markdown with a 1 for the `IFloatyThing` interface, and a2 for the IRockCandy interface. The output is

# IFloatyThing`1
# IRockCandy`2

The desired result is

# IFloatyThing<T>
# IRockCandy<S, T>

To reproduce, make interfaces such as above in a new C# class library project with this NuGet package, and then build it, and then look at the generated Markdown.

luttje commented 3 years ago

Having generics formatted like that would sure be nice.

I tried to have a look to see if it's a simple fix I could quickly create a PR for but nope. I'm already side-tracked from my main project so I'll just leave what I found for reference.

The effect @astrohart described is caused by how the xml docs represent generics:

Arguments that represent generic types have an appended ` (backtick) character followed by the number of type parameters
Arguments having the out or ref modifier have an @ following their type name. Arguments passed by value or via params have no special notation.
Arguments that are arrays are represented as [lowerbound:size, ... , lowerbound:size] where the number of commas is the rank less one, and the lower bounds and size of each dimension, if known, are represented in decimal. If a lower bound or size is not specified, it is omitted. If the lower bound and size for a particular dimension are omitted, the : is omitted as well. Jagged arrays are represented by one [] per level.
Arguments that have pointer types other than void are represented using a * following the type name. A void pointer is represented using a type name of System.Void.
Arguments that refer to generic type parameters defined on types are encoded using the ` (backtick) character followed by the zero-based index of the type parameter.
Arguments that use generic type parameters defined in methods use a double-backtick `` instead of the ` used for types.
Arguments that refer to constructed generic types are encoded using the generic type, followed by {, followed by a comma-separated list of type arguments, followed by }.

Source: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/documentation-comments#id-string-format

This is where it goes wrong:

  1. https://github.com/lijunle/Vsxmd/blob/master/Vsxmd/Units/MemberName.cs#L93
  2. It uses FriendlyName for all methods which takes the last segment from of NameSegments from the appropriate XElement
  3. It does not handle the logic quoted above

It looks to me that the in fixing this; you want to know the name of the generic types in the method, you'd somehow need to get information from all the relevant TypeparamUnits

I guess just sending them as the third parameter to the MemberName constructor would work, just like params are passed along: https://github.com/lijunle/Vsxmd/blob/master/Vsxmd/Units/MemberUnit.cs#L34

JanneLouw commented 2 weeks ago

There is another problem with generic type parameters. When generating links, the backticks often don't get escaped properly, leading to invalid markdown. Once generic type parameters are rendered as desccribed by OP, this problem should also be fixed, so I don't think it's worth a separate issue.

I have made a little minimum effort fix that will automatically escape the backticks inside the links, so it will at least generate valid markdown. It's a little bit clunky and slow, but it does the job. I thought I'd share it here.

Define a task in your project file (or Directory.Build.targets):

<UsingTask TaskName="FixGeneratedXmlDocMarkdown" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
        <MarkdownFile ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
        <Using Namespace="System" />
        <Using Namespace="System.IO" />
        <Using Namespace="System.Text.RegularExpressions" />
        <Code Type="Fragment" Language="cs">
            <![CDATA[
                Regex regex = new Regex(@"(\]\([^\)]*[^\\])`");
                string text = File.ReadAllText(MarkdownFile);
                while (regex.IsMatch(text))
                {
                    text = regex.Replace(text, "$1\\`");
                }
                File.WriteAllText(MarkdownFile, text);
            ]]>
        </Code>
    </Task>
</UsingTask>

and add a build target to run this after build:

<Target Name="AfterBuildStep" AfterTargets="Build">
    <FixGeneratedXmlDocMarkdown MarkdownFile="$(DocumentationMarkdown)" />
</Target>