From 5d452d73cf11896b567ff2c88a2f03ca247213fe Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 24 Jun 2021 06:52:05 -0600 Subject: [PATCH] Consume API docs from win32metadata This removes our own production of the API docs data file and replaces it with a consumption of what the win32metadata repo started producing in https://github.com/microsoft/win32metadata/pull/548. --- .gitmodules | 4 - Directory.Build.props | 2 + Microsoft.Windows.CsWin32.sln | 7 - ext/sdk-api | 1 - global.json | 2 +- src/Microsoft.Windows.CsWin32/ApiDetails.cs | 52 -- src/Microsoft.Windows.CsWin32/Docs.cs | 35 +- src/Microsoft.Windows.CsWin32/Generator.cs | 456 +++++++------ .../Microsoft.Windows.CsWin32.csproj | 10 +- .../Microsoft.Windows.CsWin32.nuspec | 4 + .../Microsoft.Windows.CsWin32.targets | 13 +- .../SourceGenerator.cs | 4 +- src/ScrapeDocs/.editorconfig | 4 - src/ScrapeDocs/Directory.Build.props | 6 - src/ScrapeDocs/DocEnum.cs | 136 ---- src/ScrapeDocs/Program.cs | 642 ------------------ src/ScrapeDocs/Properties/launchSettings.json | 9 - src/ScrapeDocs/ScrapeDocs.csproj | 18 - src/Win32MetaGeneration/Program.cs | 2 + .../Win32MetaGeneration.csproj | 3 + .../GenerationSandbox.Tests.csproj | 1 + .../GeneratorTests.cs | 3 +- .../Microsoft.Windows.CsWin32.Tests.csproj | 3 + test/SpellChecker/SpellChecker.csproj | 1 + 24 files changed, 278 insertions(+), 1140 deletions(-) delete mode 100644 .gitmodules delete mode 160000 ext/sdk-api delete mode 100644 src/Microsoft.Windows.CsWin32/ApiDetails.cs delete mode 100644 src/ScrapeDocs/.editorconfig delete mode 100644 src/ScrapeDocs/Directory.Build.props delete mode 100644 src/ScrapeDocs/DocEnum.cs delete mode 100644 src/ScrapeDocs/Program.cs delete mode 100644 src/ScrapeDocs/Properties/launchSettings.json delete mode 100644 src/ScrapeDocs/ScrapeDocs.csproj diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 189e376e..00000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "docs"] - path = ext/sdk-api - url = https://github.com/MicrosoftDocs/sdk-api - shallow = true diff --git a/Directory.Build.props b/Directory.Build.props index f8aec500..00a30982 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,10 +26,12 @@ snupkg 10.2.84-preview + 0.1.4-alpha + diff --git a/Microsoft.Windows.CsWin32.sln b/Microsoft.Windows.CsWin32.sln index 971d3b6b..51a7d444 100644 --- a/Microsoft.Windows.CsWin32.sln +++ b/Microsoft.Windows.CsWin32.sln @@ -33,8 +33,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Windows.CsWin32", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Windows.CsWin32.Tests", "test\Microsoft.Windows.CsWin32.Tests\Microsoft.Windows.CsWin32.Tests.csproj", "{0129FE6E-3480-408A-BF40-9E6343CDB06C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScrapeDocs", "src\ScrapeDocs\ScrapeDocs.csproj", "{EB7D0834-4236-408F-B172-64FB45FF643A}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Win32MetaGeneration", "src\Win32MetaGeneration\Win32MetaGeneration.csproj", "{6638957D-09ED-47C1-86B9-5D2DFD0FE625}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenerationSandbox.Tests", "test\GenerationSandbox.Tests\GenerationSandbox.Tests.csproj", "{7E8A5179-F94C-410F-8BBE-FDAAA95A19C3}" @@ -55,10 +53,6 @@ Global {0129FE6E-3480-408A-BF40-9E6343CDB06C}.Debug|Any CPU.Build.0 = Debug|Any CPU {0129FE6E-3480-408A-BF40-9E6343CDB06C}.Release|Any CPU.ActiveCfg = Release|Any CPU {0129FE6E-3480-408A-BF40-9E6343CDB06C}.Release|Any CPU.Build.0 = Release|Any CPU - {EB7D0834-4236-408F-B172-64FB45FF643A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EB7D0834-4236-408F-B172-64FB45FF643A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EB7D0834-4236-408F-B172-64FB45FF643A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EB7D0834-4236-408F-B172-64FB45FF643A}.Release|Any CPU.Build.0 = Release|Any CPU {6638957D-09ED-47C1-86B9-5D2DFD0FE625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6638957D-09ED-47C1-86B9-5D2DFD0FE625}.Debug|Any CPU.Build.0 = Debug|Any CPU {6638957D-09ED-47C1-86B9-5D2DFD0FE625}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -78,7 +72,6 @@ Global GlobalSection(NestedProjects) = preSolution {E3E96466-44B6-41AF-BBC8-9D30183ED8A9} = {9E154A29-1796-4B85-BD81-B6A385D8FF71} {0129FE6E-3480-408A-BF40-9E6343CDB06C} = {36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A} - {EB7D0834-4236-408F-B172-64FB45FF643A} = {9E154A29-1796-4B85-BD81-B6A385D8FF71} {6638957D-09ED-47C1-86B9-5D2DFD0FE625} = {9E154A29-1796-4B85-BD81-B6A385D8FF71} {7E8A5179-F94C-410F-8BBE-FDAAA95A19C3} = {36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A} {744BE74F-8C4A-49E8-9683-52D987224285} = {36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A} diff --git a/ext/sdk-api b/ext/sdk-api deleted file mode 160000 index 4c1b7b25..00000000 --- a/ext/sdk-api +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4c1b7b257c9749195e6f0319238dc83efb02d2f0 diff --git a/global.json b/global.json index 728ab674..34eb419f 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "5.0.202", + "version": "5.0.301", "rollForward": "patch", "allowPrerelease": false } diff --git a/src/Microsoft.Windows.CsWin32/ApiDetails.cs b/src/Microsoft.Windows.CsWin32/ApiDetails.cs deleted file mode 100644 index 6f034d3b..00000000 --- a/src/Microsoft.Windows.CsWin32/ApiDetails.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace ScrapeDocs -{ - using System; - using System.Collections.Generic; - using MessagePack; - - /// - /// Captures all the documentation we have available for an API. - /// - [MessagePackObject] - public class ApiDetails - { - /// - /// Gets or sets the URL that provides more complete documentation for this API. - /// - [Key(0)] - public Uri? HelpLink { get; set; } - - /// - /// Gets or sets a summary of what the API is for. - /// - [Key(1)] - public string? Description { get; set; } - - /// - /// Gets or sets the remarks section of the documentation. - /// - [Key(2)] - public string? Remarks { get; set; } - - /// - /// Gets a collection of parameter docs, keyed by their names. - /// - [Key(3)] - public Dictionary Parameters { get; } = new(); - - /// - /// Gets a collection of field docs, keyed by their names. - /// - [Key(4)] - public Dictionary Fields { get; } = new(); - - /// - /// Gets or sets the documentation of the return value of the API, if applicable. - /// - [Key(5)] - public string? ReturnValue { get; set; } - } -} diff --git a/src/Microsoft.Windows.CsWin32/Docs.cs b/src/Microsoft.Windows.CsWin32/Docs.cs index 2e1165e3..3d2fdcb6 100644 --- a/src/Microsoft.Windows.CsWin32/Docs.cs +++ b/src/Microsoft.Windows.CsWin32/Docs.cs @@ -9,10 +9,12 @@ namespace Microsoft.Windows.CsWin32 using System.IO; using System.Reflection; using MessagePack; - using ScrapeDocs; + using Microsoft.Windows.SDK.Win32Docs; internal class Docs { + private static readonly Dictionary DocsByPath = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary apisAndDocs; private Docs(Dictionary apisAndDocs) @@ -20,21 +22,32 @@ private Docs(Dictionary apisAndDocs) this.apisAndDocs = apisAndDocs; } - internal static Docs Instance { get; } = Create(); - - internal bool TryGetApiDocs(string apiName, [NotNullWhen(true)] out ApiDetails? docs) => this.apisAndDocs.TryGetValue(apiName, out docs); - - private static Docs Create() + internal static Docs Get(string docsPath) { - using Stream? docsStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(ThisAssembly.RootNamespace + ".apidocs.msgpack"); - if (docsStream is null) + lock (DocsByPath) { - ////return new Docs(new Dictionary()); - throw new Exception("Documentation not found."); + if (DocsByPath.TryGetValue(docsPath, out Docs? existing)) + { + return existing; + } } + using FileStream docsStream = File.OpenRead(docsPath); var data = MessagePackSerializer.Deserialize>(docsStream); - return new Docs(data); + var docs = new Docs(data); + + lock (DocsByPath) + { + if (DocsByPath.TryGetValue(docsPath, out Docs? existing)) + { + return existing; + } + + DocsByPath.Add(docsPath, docs); + return docs; + } } + + internal bool TryGetApiDocs(string apiName, [NotNullWhen(true)] out ApiDetails? docs) => this.apisAndDocs.TryGetValue(apiName, out docs); } } diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs index 255886d0..93129962 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.cs @@ -22,7 +22,7 @@ namespace Microsoft.Windows.CsWin32 using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; - using ScrapeDocs; + using Microsoft.Windows.SDK.Win32Docs; using static FastSyntaxFactory; /// @@ -284,12 +284,14 @@ public class Generator : IDisposable /// Initializes a new instance of the class. /// /// The path to the winmd metadata to generate APIs from. + /// The path to the API docs file. /// Options that influence the result of generation. /// The compilation that the generated code will be added to. /// The parse options that will be used for the generated code. - public Generator(string metadataLibraryPath, GeneratorOptions? options = null, CSharpCompilation? compilation = null, CSharpParseOptions? parseOptions = null) + public Generator(string metadataLibraryPath, string? apiDocsPath, GeneratorOptions? options = null, CSharpCompilation? compilation = null, CSharpParseOptions? parseOptions = null) { this.MetadataIndex = MetadataIndex.Get(metadataLibraryPath, compilation?.Options.Platform); + this.ApiDocs = apiDocsPath is object ? Docs.Get(apiDocsPath) : null; this.options = options ??= new GeneratorOptions(); this.options.Validate(); @@ -348,6 +350,8 @@ private enum FriendlyOverloadOf internal MetadataIndex MetadataIndex { get; } + internal Docs? ApiDocs { get; } + internal ReadOnlyCollection Apis => this.MetadataIndex.Apis; internal MetadataReader Reader => this.MetadataIndex.Reader; @@ -1202,7 +1206,7 @@ internal void RequestConstant(FieldDefinitionHandle fieldDefHandle) this.volatileCode.GenerateConstant(fieldDefHandle, delegate { FieldDeclarationSyntax constantDeclaration = this.DeclareConstant(fieldDefHandle); - constantDeclaration = AddApiDocumentation(constantDeclaration.Declaration.Variables[0].Identifier.ValueText, constantDeclaration); + constantDeclaration = this.AddApiDocumentation(constantDeclaration.Declaration.Variables[0].Identifier.ValueText, constantDeclaration); this.volatileCode.AddConstant(fieldDefHandle, constantDeclaration); }); } @@ -1677,222 +1681,6 @@ protected virtual void Dispose(bool disposing) private static SyntaxToken TokenWithLineFeed(SyntaxKind syntaxKind) => SyntaxFactory.Token(TriviaList(), syntaxKind, TriviaList(LineFeed)); - private static T AddApiDocumentation(string api, T memberDeclaration) - where T : MemberDeclarationSyntax - { - if (Docs.Instance.TryGetApiDocs(api, out var docs)) - { - var docCommentsBuilder = new StringBuilder(); - if (docs.Description is object) - { - docCommentsBuilder.Append($@"/// "); - EmitDoc(docs.Description, docCommentsBuilder, docs, string.Empty); - docCommentsBuilder.AppendLine(""); - } - - if (docs.Parameters is object) - { - if (memberDeclaration is BaseMethodDeclarationSyntax methodDecl) - { - foreach (var entry in docs.Parameters) - { - if (!methodDecl.ParameterList.Parameters.Any(p => string.Equals(p.Identifier.ValueText, entry.Key, StringComparison.Ordinal))) - { - // Skip documentation for parameters that do not actually exist on the method. - continue; - } - - docCommentsBuilder.Append($@"/// "); - EmitDoc(entry.Value, docCommentsBuilder, docs, "parameters"); - docCommentsBuilder.AppendLine(""); - } - } - } - - if (docs.Fields is object) - { - var fieldsDocBuilder = new StringBuilder(); - switch (memberDeclaration) - { - case StructDeclarationSyntax structDeclaration: - memberDeclaration = memberDeclaration.ReplaceNodes( - structDeclaration.Members.OfType(), - (_, field) => - { - var variable = field.Declaration.Variables.Single(); - if (docs.Fields.TryGetValue(variable.Identifier.ValueText, out string? fieldDoc)) - { - fieldsDocBuilder.Append("/// "); - EmitDoc(fieldDoc, fieldsDocBuilder, docs, "members"); - fieldsDocBuilder.AppendLine(""); - if (field.Declaration.Type.HasAnnotations(OriginalDelegateAnnotation)) - { - fieldsDocBuilder.AppendLine(@$"/// See the delegate for more about this function."); - } - - field = field.WithLeadingTrivia(ParseLeadingTrivia(fieldsDocBuilder.ToString())); - fieldsDocBuilder.Clear(); - } - - return field; - }); - break; - case EnumDeclarationSyntax enumDeclaration: - memberDeclaration = memberDeclaration.ReplaceNodes( - enumDeclaration.Members, - (_, field) => - { - if (docs.Fields.TryGetValue(field.Identifier.ValueText, out string? fieldDoc)) - { - fieldsDocBuilder.Append($@"/// "); - EmitDoc(fieldDoc, fieldsDocBuilder, docs, "members"); - fieldsDocBuilder.AppendLine(""); - field = field.WithLeadingTrivia(ParseLeadingTrivia(fieldsDocBuilder.ToString())); - fieldsDocBuilder.Clear(); - } - - return field; - }); - break; - } - } - - if (docs.ReturnValue is object) - { - docCommentsBuilder.Append("/// "); - EmitDoc(docs.ReturnValue, docCommentsBuilder, docs: null, string.Empty); - docCommentsBuilder.AppendLine(""); - } - - if (docs.Remarks is object || docs.HelpLink is object) - { - docCommentsBuilder.Append($"/// "); - if (docs.Remarks is object) - { - EmitDoc(docs.Remarks, docCommentsBuilder, docs, string.Empty); - } - else if (docs.HelpLink is object) - { - docCommentsBuilder.AppendLine(); - docCommentsBuilder.AppendLine($@"/// Learn more about this API from docs.microsoft.com."); - docCommentsBuilder.Append("/// "); - } - - docCommentsBuilder.AppendLine($""); - } - - memberDeclaration = memberDeclaration.WithLeadingTrivia( - ParseLeadingTrivia(docCommentsBuilder.ToString())); - } - - return memberDeclaration; - - static void EmitLine(StringBuilder stringBuilder, string yamlDocSrc) - { - stringBuilder.Append(yamlDocSrc.Trim()); - } - - static void EmitDoc(string yamlDocSrc, StringBuilder docCommentsBuilder, ApiDetails? docs, string docsAnchor) - { - if (yamlDocSrc.Contains('\n')) - { - docCommentsBuilder.AppendLine(); - var docReader = new StringReader(yamlDocSrc); - string? paramDocLine; - - bool inParagraph = false; - bool inComment = false; - int blankLineCounter = 0; - while ((paramDocLine = docReader.ReadLine()) is object) - { - if (string.IsNullOrWhiteSpace(paramDocLine)) - { - if (++blankLineCounter >= 2 && inParagraph) - { - docCommentsBuilder.AppendLine(""); - inParagraph = false; - inComment = false; - } - - continue; - } - else if (blankLineCounter > 0) - { - blankLineCounter = 0; - } - else - { - docCommentsBuilder.Append(' '); - } - - if (inParagraph) - { - if (docCommentsBuilder.Length > 0 && docCommentsBuilder[docCommentsBuilder.Length - 1] != ' ') - { - docCommentsBuilder.Append(' '); - } - } - else - { - docCommentsBuilder.Append("/// "); - inParagraph = true; - inComment = true; - } - - if (!inComment) - { - docCommentsBuilder.Append("/// "); - } - - if (paramDocLine.IndexOf("= 0 || - paramDocLine.IndexOf("= 0 || - paramDocLine.IndexOf("= 0 || - paramDocLine.IndexOf("= 0 || - paramDocLine.IndexOf("```", StringComparison.OrdinalIgnoreCase) >= 0 || - paramDocLine.IndexOf("<<", StringComparison.OrdinalIgnoreCase) >= 0) - { - // We don't try to format tables, so truncate at this point. - if (inParagraph) - { - docCommentsBuilder.AppendLine(""); - inParagraph = false; - inComment = false; - } - - docCommentsBuilder.AppendLine($@"/// This doc was truncated."); - - break; // is this the right way? - } - - EmitLine(docCommentsBuilder, paramDocLine); - } - - if (inParagraph) - { - if (!inComment) - { - docCommentsBuilder.Append("/// "); - } - - docCommentsBuilder.AppendLine(""); - inParagraph = false; - inComment = false; - } - - if (docs is object) - { - docCommentsBuilder.AppendLine($@"/// Read more on docs.microsoft.com."); - } - - docCommentsBuilder.Append("/// "); - } - else - { - EmitLine(docCommentsBuilder, yamlDocSrc); - } - } - } - private static bool RequiresUnsafe(TypeSyntax? typeSyntax) => typeSyntax is PointerTypeSyntax || typeSyntax is FunctionPointerTypeSyntax; private static string GetClassNameForModule(string moduleName) => @@ -2131,6 +1919,222 @@ private static bool TrySplitPossiblyQualifiedName(string possiblyQualifiedName, return @namespace is object; } + private T AddApiDocumentation(string api, T memberDeclaration) + where T : MemberDeclarationSyntax + { + if (this.ApiDocs is object && this.ApiDocs.TryGetApiDocs(api, out var docs)) + { + var docCommentsBuilder = new StringBuilder(); + if (docs.Description is object) + { + docCommentsBuilder.Append($@"/// "); + EmitDoc(docs.Description, docCommentsBuilder, docs, string.Empty); + docCommentsBuilder.AppendLine(""); + } + + if (docs.Parameters is object) + { + if (memberDeclaration is BaseMethodDeclarationSyntax methodDecl) + { + foreach (var entry in docs.Parameters) + { + if (!methodDecl.ParameterList.Parameters.Any(p => string.Equals(p.Identifier.ValueText, entry.Key, StringComparison.Ordinal))) + { + // Skip documentation for parameters that do not actually exist on the method. + continue; + } + + docCommentsBuilder.Append($@"/// "); + EmitDoc(entry.Value, docCommentsBuilder, docs, "parameters"); + docCommentsBuilder.AppendLine(""); + } + } + } + + if (docs.Fields is object) + { + var fieldsDocBuilder = new StringBuilder(); + switch (memberDeclaration) + { + case StructDeclarationSyntax structDeclaration: + memberDeclaration = memberDeclaration.ReplaceNodes( + structDeclaration.Members.OfType(), + (_, field) => + { + var variable = field.Declaration.Variables.Single(); + if (docs.Fields.TryGetValue(variable.Identifier.ValueText, out string? fieldDoc)) + { + fieldsDocBuilder.Append("/// "); + EmitDoc(fieldDoc, fieldsDocBuilder, docs, "members"); + fieldsDocBuilder.AppendLine(""); + if (field.Declaration.Type.HasAnnotations(OriginalDelegateAnnotation)) + { + fieldsDocBuilder.AppendLine(@$"/// See the delegate for more about this function."); + } + + field = field.WithLeadingTrivia(ParseLeadingTrivia(fieldsDocBuilder.ToString())); + fieldsDocBuilder.Clear(); + } + + return field; + }); + break; + case EnumDeclarationSyntax enumDeclaration: + memberDeclaration = memberDeclaration.ReplaceNodes( + enumDeclaration.Members, + (_, field) => + { + if (docs.Fields.TryGetValue(field.Identifier.ValueText, out string? fieldDoc)) + { + fieldsDocBuilder.Append($@"/// "); + EmitDoc(fieldDoc, fieldsDocBuilder, docs, "members"); + fieldsDocBuilder.AppendLine(""); + field = field.WithLeadingTrivia(ParseLeadingTrivia(fieldsDocBuilder.ToString())); + fieldsDocBuilder.Clear(); + } + + return field; + }); + break; + } + } + + if (docs.ReturnValue is object) + { + docCommentsBuilder.Append("/// "); + EmitDoc(docs.ReturnValue, docCommentsBuilder, docs: null, string.Empty); + docCommentsBuilder.AppendLine(""); + } + + if (docs.Remarks is object || docs.HelpLink is object) + { + docCommentsBuilder.Append($"/// "); + if (docs.Remarks is object) + { + EmitDoc(docs.Remarks, docCommentsBuilder, docs, string.Empty); + } + else if (docs.HelpLink is object) + { + docCommentsBuilder.AppendLine(); + docCommentsBuilder.AppendLine($@"/// Learn more about this API from docs.microsoft.com."); + docCommentsBuilder.Append("/// "); + } + + docCommentsBuilder.AppendLine($""); + } + + memberDeclaration = memberDeclaration.WithLeadingTrivia( + ParseLeadingTrivia(docCommentsBuilder.ToString())); + } + + return memberDeclaration; + + static void EmitLine(StringBuilder stringBuilder, string yamlDocSrc) + { + stringBuilder.Append(yamlDocSrc.Trim()); + } + + static void EmitDoc(string yamlDocSrc, StringBuilder docCommentsBuilder, ApiDetails? docs, string docsAnchor) + { + if (yamlDocSrc.Contains('\n')) + { + docCommentsBuilder.AppendLine(); + var docReader = new StringReader(yamlDocSrc); + string? paramDocLine; + + bool inParagraph = false; + bool inComment = false; + int blankLineCounter = 0; + while ((paramDocLine = docReader.ReadLine()) is object) + { + if (string.IsNullOrWhiteSpace(paramDocLine)) + { + if (++blankLineCounter >= 2 && inParagraph) + { + docCommentsBuilder.AppendLine(""); + inParagraph = false; + inComment = false; + } + + continue; + } + else if (blankLineCounter > 0) + { + blankLineCounter = 0; + } + else + { + docCommentsBuilder.Append(' '); + } + + if (inParagraph) + { + if (docCommentsBuilder.Length > 0 && docCommentsBuilder[docCommentsBuilder.Length - 1] != ' ') + { + docCommentsBuilder.Append(' '); + } + } + else + { + docCommentsBuilder.Append("/// "); + inParagraph = true; + inComment = true; + } + + if (!inComment) + { + docCommentsBuilder.Append("/// "); + } + + if (paramDocLine.IndexOf("= 0 || + paramDocLine.IndexOf("= 0 || + paramDocLine.IndexOf("= 0 || + paramDocLine.IndexOf("= 0 || + paramDocLine.IndexOf("```", StringComparison.OrdinalIgnoreCase) >= 0 || + paramDocLine.IndexOf("<<", StringComparison.OrdinalIgnoreCase) >= 0) + { + // We don't try to format tables, so truncate at this point. + if (inParagraph) + { + docCommentsBuilder.AppendLine(""); + inParagraph = false; + inComment = false; + } + + docCommentsBuilder.AppendLine($@"/// This doc was truncated."); + + break; // is this the right way? + } + + EmitLine(docCommentsBuilder, paramDocLine); + } + + if (inParagraph) + { + if (!inComment) + { + docCommentsBuilder.Append("/// "); + } + + docCommentsBuilder.AppendLine(""); + inParagraph = false; + inComment = false; + } + + if (docs is object) + { + docCommentsBuilder.AppendLine($@"/// Read more on docs.microsoft.com."); + } + + docCommentsBuilder.Append("/// "); + } + else + { + EmitLine(docCommentsBuilder, yamlDocSrc); + } + } + } + private MemberDeclarationSyntax FetchTemplate(string name) { if (!this.TryFetchTemplate(name, out MemberDeclarationSyntax? result)) @@ -2410,7 +2414,7 @@ private void DeclareExternMethod(MethodDefinitionHandle methodDefinitionHandle) } // Add documentation if we can find it. - methodDeclaration = AddApiDocumentation(entrypoint ?? methodName, methodDeclaration); + methodDeclaration = this.AddApiDocumentation(entrypoint ?? methodName, methodDeclaration); if (RequiresUnsafe(methodDeclaration.ReturnType) || methodDeclaration.ParameterList.Parameters.Any(p => RequiresUnsafe(p.Type))) { @@ -2731,7 +2735,7 @@ private TypeDeclarationSyntax DeclareInterfaceAsStruct(TypeDefinition typeDef, S } // Add documentation if we can find it. - methodDeclaration = AddApiDocumentation($"{ifaceName}.{methodName}", methodDeclaration); + methodDeclaration = this.AddApiDocumentation($"{ifaceName}.{methodName}", methodDeclaration); members.AddRange(this.DeclareFriendlyOverloads(methodDefinition, methodDeclaration, IdentifierName(ifaceName.Identifier.ValueText), FriendlyOverloadOf.StructMethod)); members.Add(methodDeclaration); @@ -2884,7 +2888,7 @@ private TypeDeclarationSyntax DeclareInterfaceAsStruct(TypeDefinition typeDef, S } // Add documentation if we can find it. - methodDeclaration = AddApiDocumentation($"{ifaceName}.{methodName}", methodDeclaration); + methodDeclaration = this.AddApiDocumentation($"{ifaceName}.{methodName}", methodDeclaration); members.Add(methodDeclaration); NameSyntax declaringTypeName = HandleTypeHandleInfo.GetNestingQualifiedName(this.Reader, typeDef); @@ -3087,7 +3091,7 @@ private StructDeclarationSyntax DeclareStruct(TypeDefinition typeDef) result = result.AddAttributeLists(AttributeList().AddAttributes(GUID(guid))); } - result = AddApiDocumentation(name.Identifier.ValueText, result); + result = this.AddApiDocumentation(name.Identifier.ValueText, result); return result; } @@ -3319,7 +3323,7 @@ private StructDeclarationSyntax DeclareTypeDefStruct(TypeDefinition typeDef) .WithModifiers(structModifiers) .AddAttributeLists(AttributeList().AddAttributes(DebuggerDisplay("{" + fieldName + "}"))); - result = AddApiDocumentation(name.Identifier.ValueText, result); + result = this.AddApiDocumentation(name.Identifier.ValueText, result); return result; } @@ -3519,7 +3523,7 @@ private StructDeclarationSyntax DeclareTypeDefBOOLStruct(TypeDefinition typeDef) .WithMembers(members) .WithModifiers(TokenList(TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.ReadOnlyKeyword), TokenWithSpace(SyntaxKind.PartialKeyword))); - result = AddApiDocumentation(name.Identifier.ValueText, result); + result = this.AddApiDocumentation(name.Identifier.ValueText, result); return result; } @@ -3578,7 +3582,7 @@ private EnumDeclarationSyntax DeclareEnum(TypeDefinition typeDef) AttributeList().AddAttributes(FlagsAttributeSyntax)); } - result = AddApiDocumentation(name, result); + result = this.AddApiDocumentation(name, result); return result; } diff --git a/src/Microsoft.Windows.CsWin32/Microsoft.Windows.CsWin32.csproj b/src/Microsoft.Windows.CsWin32/Microsoft.Windows.CsWin32.csproj index e6e0e92d..1e04762a 100644 --- a/src/Microsoft.Windows.CsWin32/Microsoft.Windows.CsWin32.csproj +++ b/src/Microsoft.Windows.CsWin32/Microsoft.Windows.CsWin32.csproj @@ -13,10 +13,6 @@ Microsoft.Windows.CsWin32.nuspec - - - - @@ -37,7 +33,6 @@ - @@ -51,15 +46,12 @@ + - - - - diff --git a/src/Microsoft.Windows.CsWin32/Microsoft.Windows.CsWin32.nuspec b/src/Microsoft.Windows.CsWin32/Microsoft.Windows.CsWin32.nuspec index 36065224..c17eaeae 100644 --- a/src/Microsoft.Windows.CsWin32/Microsoft.Windows.CsWin32.nuspec +++ b/src/Microsoft.Windows.CsWin32/Microsoft.Windows.CsWin32.nuspec @@ -16,18 +16,22 @@ + + + + diff --git a/src/Microsoft.Windows.CsWin32/Microsoft.Windows.CsWin32.targets b/src/Microsoft.Windows.CsWin32/Microsoft.Windows.CsWin32.targets index e2c2a837..bb04f231 100644 --- a/src/Microsoft.Windows.CsWin32/Microsoft.Windows.CsWin32.targets +++ b/src/Microsoft.Windows.CsWin32/Microsoft.Windows.CsWin32.targets @@ -3,20 +3,9 @@ $(TargetsForTfmSpecificContentInPackage);PackBuildOutputs - - - - - - $(NuspecProperties);Version=$(Version);BaseOutputPath=$(OutputPath);MetadataVersion=$(MetadataVersion);PackageReleaseNotes=$(PackageReleaseNotes);commit=$(GitCommitId); + $(NuspecProperties);Version=$(Version);BaseOutputPath=$(OutputPath);MetadataVersion=$(MetadataVersion);ApiDocsVersion=$(ApiDocsVersion);PackageReleaseNotes=$(PackageReleaseNotes);commit=$(GitCommitId); diff --git a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs index aa313aa1..9d045dfa 100644 --- a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs +++ b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs @@ -109,6 +109,8 @@ public void Execute(GeneratorExecutionContext context) return; } + context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.MicrosoftWindowsSdkApiDocsPath", out string? apiDocsPath); + GeneratorOptions? options = null; AdditionalText? nativeMethodsJsonFile = context.AdditionalFiles .FirstOrDefault(af => string.Equals(Path.GetFileName(af.Path), NativeMethodsJsonAdditionalFileName, StringComparison.OrdinalIgnoreCase)); @@ -139,7 +141,7 @@ public void Execute(GeneratorExecutionContext context) context.ReportDiagnostic(Diagnostic.Create(UnsafeCodeRequired, location: null)); } - using var generator = new Generator(metadataPath, options, compilation, parseOptions); + using var generator = new Generator(metadataPath, apiDocsPath, options, compilation, parseOptions); SourceText? nativeMethodsTxt = nativeMethodsTxtFile.GetText(context.CancellationToken); if (nativeMethodsTxt is null) diff --git a/src/ScrapeDocs/.editorconfig b/src/ScrapeDocs/.editorconfig deleted file mode 100644 index d40fea65..00000000 --- a/src/ScrapeDocs/.editorconfig +++ /dev/null @@ -1,4 +0,0 @@ -[*.cs] - -# CA1303: Do not pass literals as localized parameters -dotnet_diagnostic.CA1303.severity = none diff --git a/src/ScrapeDocs/Directory.Build.props b/src/ScrapeDocs/Directory.Build.props deleted file mode 100644 index a3b22a14..00000000 --- a/src/ScrapeDocs/Directory.Build.props +++ /dev/null @@ -1,6 +0,0 @@ - - - true - - - diff --git a/src/ScrapeDocs/DocEnum.cs b/src/ScrapeDocs/DocEnum.cs deleted file mode 100644 index 83946966..00000000 --- a/src/ScrapeDocs/DocEnum.cs +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace ScrapeDocs -{ - using System; - using System.Collections.Generic; - using System.Linq; - - internal class DocEnum - { - internal DocEnum(bool isFlags, IReadOnlyDictionary members) - { - this.IsFlags = isFlags; - this.Members = members; - } - - internal bool IsFlags { get; } - - internal IReadOnlyDictionary Members { get; } - - public override bool Equals(object? obj) => this.Equals(obj as DocEnum); - - public override int GetHashCode() - { - unchecked - { - int hash = this.IsFlags ? 1 : 0; - foreach (KeyValuePair entry in this.Members) - { - hash += entry.Key.GetHashCode(); - hash += (int)(entry.Value.Value ?? 0u); - } - - return hash; - } - } - - public bool Equals(DocEnum? other) - { - if (other is null) - { - return false; - } - - if (this.IsFlags != other.IsFlags) - { - return false; - } - - if (this.Members.Count != other.Members.Count) - { - return false; - } - - foreach (KeyValuePair entry in this.Members) - { - if (!other.Members.TryGetValue(entry.Key, out (ulong? Value, string? Doc) value)) - { - return false; - } - - if (entry.Value.Value != value.Value) - { - return false; - } - } - - return true; - } - - internal string? GetRecommendedName(List<(string MethodName, string ParameterName, string HelpLink, bool IsMethod)> uses) - { - string? enumName = null; - if (uses.Count == 1) - { - var oneValue = uses[0]; - if (oneValue.ParameterName.Contains("flags", StringComparison.OrdinalIgnoreCase)) - { - // Only appears in one method, on a parameter named something like "flags". - enumName = $"{oneValue.MethodName}Flags"; - } - else - { - enumName = $"{oneValue.MethodName}_{oneValue.ParameterName}Flags"; - } - } - else - { - string firstName = this.Members.Keys.First(); - int commonPrefixLength = firstName.Length; - foreach (string key in this.Members.Keys) - { - commonPrefixLength = Math.Min(commonPrefixLength, GetCommonPrefixLength(key, firstName)); - } - - if (commonPrefixLength > 1) - { - int last_ = firstName.LastIndexOf('_', commonPrefixLength - 1); - if (last_ != -1 && last_ != commonPrefixLength - 1) - { - // Trim down to last underscore - commonPrefixLength = last_; - } - - if (commonPrefixLength > 1 && firstName[commonPrefixLength - 1] == '_') - { - // The enum values share a common prefix suitable to imply a name for the enum. - enumName = firstName.Substring(0, commonPrefixLength - 1); - } - } - } - - return enumName; - } - - private static int GetCommonPrefixLength(ReadOnlySpan first, ReadOnlySpan second) - { - int count = 0; - int minLength = Math.Min(first.Length, second.Length); - for (int i = 0; i < minLength; i++) - { - if (first[i] == second[i]) - { - count++; - } - else - { - break; - } - } - - return count; - } - } -} diff --git a/src/ScrapeDocs/Program.cs b/src/ScrapeDocs/Program.cs deleted file mode 100644 index 17a897f9..00000000 --- a/src/ScrapeDocs/Program.cs +++ /dev/null @@ -1,642 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace ScrapeDocs -{ - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Globalization; - using System.IO; - using System.Linq; - using System.Reflection; - using System.Text; - using System.Text.Json; - using System.Text.RegularExpressions; - using System.Threading; - using MessagePack; - using YamlDotNet; - using YamlDotNet.RepresentationModel; - - /// - /// Program entrypoint class. - /// - internal class Program - { - private static readonly Regex FileNamePattern = new Regex(@"^\w\w-\w+-([\w\-]+)$", RegexOptions.Compiled); - private static readonly Regex ParameterHeaderPattern = new Regex(@"^### -param (\w+)", RegexOptions.Compiled); - private static readonly Regex FieldHeaderPattern = new Regex(@"^### -field (?:\w+\.)*(\w+)", RegexOptions.Compiled); - private static readonly Regex ReturnHeaderPattern = new Regex(@"^## -returns", RegexOptions.Compiled); - private static readonly Regex RemarksHeaderPattern = new Regex(@"^## -remarks", RegexOptions.Compiled); - private static readonly Regex InlineCodeTag = new Regex(@"\(.*)\", RegexOptions.Compiled); - private static readonly Regex EnumNameCell = new Regex(@"\]*\>\([\dxa-f]+)\<\/dt\>", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private readonly string contentBasePath; - private readonly string outputPath; - - private Program(string contentBasePath, string outputPath) - { - this.contentBasePath = contentBasePath; - this.outputPath = outputPath; - } - - private bool EmitEnums { get; set; } - - private static int Main(string[] args) - { - using var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (s, e) => - { - Console.WriteLine("Canceling..."); - cts.Cancel(); - e.Cancel = true; - }; - - if (args.Length < 2) - { - Console.Error.WriteLine("USAGE: {0} [enums]"); - return 1; - } - - string contentBasePath = args[0]; - string outputPath = args[1]; - bool emitEnums = args.Length > 2 ? args[2] == "enums" : false; - - try - { - new Program(contentBasePath, outputPath) { EmitEnums = true }.Worker(cts.Token); - } - catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token) - { - return 2; - } - - return 0; - } - - private static void Expect(string? expected, string? actual) - { - if (expected != actual) - { - throw new InvalidOperationException($"Expected: \"{expected}\" but read: \"{actual}\"."); - } - } - - private int AnalyzeEnums(ConcurrentDictionary results, ConcurrentDictionary<(string MethodName, string ParameterName, string HelpLink), DocEnum> parameterEnums, ConcurrentDictionary<(string MethodName, string ParameterName, string HelpLink), DocEnum> fieldEnums) - { - var uniqueEnums = new Dictionary>(); - var constantsDocs = new Dictionary>(); - - void Collect(ConcurrentDictionary<(string MethodName, string ParameterName, string HelpLink), DocEnum> enums, bool isMethod) - { - foreach (var item in enums) - { - if (!uniqueEnums.TryGetValue(item.Value, out List<(string MethodName, string ParameterName, string HelpLink, bool IsMethod)>? list)) - { - uniqueEnums.Add(item.Value, list = new()); - } - - list.Add((item.Key.MethodName, item.Key.ParameterName, item.Key.HelpLink, isMethod)); - - foreach (KeyValuePair enumValue in item.Value.Members) - { - if (enumValue.Value.Doc is object) - { - if (!constantsDocs.TryGetValue(enumValue.Key, out List<(string MethodName, string HelpLink, string Doc)>? values)) - { - constantsDocs.Add(enumValue.Key, values = new()); - } - - values.Add((item.Key.MethodName, item.Key.HelpLink, enumValue.Value.Doc)); - } - } - } - } - - Collect(parameterEnums, isMethod: true); - Collect(fieldEnums, isMethod: false); - - foreach (var item in constantsDocs) - { - var docNode = new ApiDetails(); - docNode.Description = item.Value[0].Doc; - - // If the documentation varies across methods, just link to each document. - bool differenceDetected = false; - for (int i = 1; i < item.Value.Count; i++) - { - if (item.Value[i].Doc != docNode.Description) - { - differenceDetected = true; - break; - } - } - - if (differenceDetected) - { - docNode.Description = "Documentation varies per use. Refer to each: " + string.Join(", ", item.Value.Select(v => @$"{v.MethodOrStructName}")) + "."; - } - else - { - // Just point to any arbitrary method that documents it. - docNode.HelpLink = new Uri(item.Value[0].HelpLink); - } - - results.TryAdd(item.Key, docNode); - } - - if (this.EmitEnums) - { - string enumDirectory = Path.GetDirectoryName(this.outputPath) ?? throw new InvalidOperationException("Unable to determine where to write enums."); - Directory.CreateDirectory(enumDirectory); - using var enumsJsonStream = File.OpenWrite(Path.Combine(enumDirectory, "enums.json")); - using var writer = new Utf8JsonWriter(enumsJsonStream, new JsonWriterOptions { Indented = true }); - writer.WriteStartArray(); - - foreach (KeyValuePair> item in uniqueEnums) - { - writer.WriteStartObject(); - - if (item.Key.GetRecommendedName(item.Value) is string enumName) - { - writer.WriteString("name", enumName); - } - - writer.WriteBoolean("flags", item.Key.IsFlags); - - writer.WritePropertyName("members"); - writer.WriteStartArray(); - foreach (var member in item.Key.Members) - { - writer.WriteStartObject(); - writer.WriteString("name", member.Key); - if (member.Value.Value is ulong value) - { - writer.WriteString("value", value.ToString(CultureInfo.InvariantCulture)); - } - - writer.WriteEndObject(); - } - - writer.WriteEndArray(); - - writer.WritePropertyName("uses"); - writer.WriteStartArray(); - foreach (var uses in item.Value) - { - writer.WriteStartObject(); - - int periodIndex = uses.MethodName.IndexOf('.', StringComparison.Ordinal); - string? iface = periodIndex >= 0 ? uses.MethodName.Substring(0, periodIndex) : null; - string name = periodIndex >= 0 ? uses.MethodName.Substring(periodIndex + 1) : uses.MethodName; - - if (iface is string) - { - writer.WriteString("interface", iface); - } - - writer.WriteString(uses.IsMethod ? "method" : "struct", name); - writer.WriteString(uses.IsMethod ? "parameter" : "field", uses.ParameterName); - - writer.WriteEndObject(); - } - - writer.WriteEndArray(); - writer.WriteEndObject(); - } - - writer.WriteEndArray(); - } - - return constantsDocs.Count; - } - - private void Worker(CancellationToken cancellationToken) - { - Console.WriteLine("Enumerating documents to be parsed..."); - string[] paths = Directory.GetFiles(this.contentBasePath, "??-*-*.md", SearchOption.AllDirectories) - ////.Where(p => p.Contains(@"ns-winsock2-blob", StringComparison.OrdinalIgnoreCase)).ToArray() - ; - - Console.WriteLine("Parsing documents..."); - var timer = Stopwatch.StartNew(); - var parsedNodes = from path in paths.AsParallel() - let result = this.ParseDocFile(path) - where result is not null - select (Path: path, result.Value.ApiName, result.Value.Docs, result.Value.EnumsByParameter, result.Value.EnumsByField); - var results = new ConcurrentDictionary(); - var parameterEnums = new ConcurrentDictionary<(string MethodName, string ParameterName, string HelpLink), DocEnum>(); - var fieldEnums = new ConcurrentDictionary<(string StructName, string FieldName, string HelpLink), DocEnum>(); - if (Debugger.IsAttached) - { - parsedNodes = parsedNodes.WithDegreeOfParallelism(1); // improve debuggability - } - - parsedNodes - .WithCancellation<(string Path, string ApiName, ApiDetails Docs, IReadOnlyDictionary EnumsByParameter, IReadOnlyDictionary EnumsByField)>(cancellationToken) - .ForAll(result => - { - results.TryAdd(result.ApiName, result.Docs); - foreach (var e in result.EnumsByParameter) - { - if (result.Docs.HelpLink is object) - { - parameterEnums.TryAdd((result.ApiName, e.Key, result.Docs.HelpLink.AbsoluteUri), e.Value); - } - } - - foreach (var e in result.EnumsByField) - { - if (result.Docs.HelpLink is object) - { - fieldEnums.TryAdd((result.ApiName, e.Key, result.Docs.HelpLink.AbsoluteUri), e.Value); - } - } - }); - if (paths.Length == 0) - { - Console.Error.WriteLine("No documents found to parse."); - } - else - { - Console.WriteLine("Parsed {2} documents in {0} ({1} per document)", timer.Elapsed, timer.Elapsed / paths.Length, paths.Length); - Console.WriteLine($"Found {parameterEnums.Count + fieldEnums.Count} enums."); - } - - Console.WriteLine("Analyzing and naming enums and collecting docs on their members..."); - int constantsCount = this.AnalyzeEnums(results, parameterEnums, fieldEnums); - Console.WriteLine($"Found docs for {constantsCount} constants."); - - Console.WriteLine("Writing results to \"{0}\"", this.outputPath); - Directory.CreateDirectory(Path.GetDirectoryName(this.outputPath)!); - using var outputFileStream = File.OpenWrite(this.outputPath); - MessagePackSerializer.Serialize(outputFileStream, results.ToDictionary(kv => kv.Key, kv => kv.Value), MessagePackSerializerOptions.Standard); - } - - private (string ApiName, ApiDetails Docs, IReadOnlyDictionary EnumsByParameter, IReadOnlyDictionary EnumsByField)? ParseDocFile(string filePath) - { - try - { - var enumsByParameter = new Dictionary(); - var enumsByField = new Dictionary(); - var yaml = new YamlStream(); - using StreamReader mdFileReader = File.OpenText(filePath); - using var markdownToYamlReader = new YamlSectionReader(mdFileReader); - var yamlBuilder = new StringBuilder(); - ApiDetails docs = new(); - string? line; - while ((line = markdownToYamlReader.ReadLine()) is object) - { - yamlBuilder.AppendLine(line); - } - - try - { - yaml.Load(new StringReader(yamlBuilder.ToString())); - } - catch (YamlDotNet.Core.YamlException ex) - { - Debug.WriteLine("YAML parsing error in \"{0}\": {1}", filePath, ex.Message); - return null; - } - - YamlSequenceNode methodNames = (YamlSequenceNode)yaml.Documents[0].RootNode["api_name"]; - bool TryGetProperName(string searchFor, string? suffix, [NotNullWhen(true)] out string? match) - { - if (suffix is string) - { - if (searchFor.EndsWith(suffix, StringComparison.Ordinal)) - { - searchFor = searchFor.Substring(0, searchFor.Length - suffix.Length); - } - else - { - match = null; - return false; - } - } - - match = methodNames.Children.Cast().FirstOrDefault(c => string.Equals(c.Value?.Replace('.', '-'), searchFor, StringComparison.OrdinalIgnoreCase))?.Value; - - if (suffix is string && match is object) - { - match += suffix.ToUpper(CultureInfo.InvariantCulture); - } - - return match is object; - } - - string presumedMethodName = FileNamePattern.Match(Path.GetFileNameWithoutExtension(filePath)).Groups[1].Value; - - // Some structures have filenames that include the W or A suffix when the content doesn't. So try some fuzzy matching. - if (!TryGetProperName(presumedMethodName, null, out string? properName) && - !TryGetProperName(presumedMethodName, "a", out properName) && - !TryGetProperName(presumedMethodName, "w", out properName) && - !TryGetProperName(presumedMethodName, "32", out properName) && - !TryGetProperName(presumedMethodName, "64", out properName)) - { - Debug.WriteLine("WARNING: Could not find proper API name in: {0}", filePath); - return null; - } - - Uri helpLink = new Uri("https://docs.microsoft.com/windows/win32/api/" + filePath.Substring(this.contentBasePath.Length, filePath.Length - 3 - this.contentBasePath.Length).Replace('\\', '/')); - docs.HelpLink = helpLink; - - var description = ((YamlMappingNode)yaml.Documents[0].RootNode).Children.FirstOrDefault(n => n.Key is YamlScalarNode { Value: "description" }).Value as YamlScalarNode; - docs.Description = description?.Value; - - // Search for parameter/field docs - var parametersMap = new YamlMappingNode(); - var fieldsMap = new YamlMappingNode(); - StringBuilder docBuilder = new StringBuilder(); - line = mdFileReader.ReadLine(); - - static string FixupLine(string line) - { - line = line.Replace("href=\"/", "href=\"https://docs.microsoft.com/"); - line = InlineCodeTag.Replace(line, match => $"{match.Groups[1].Value}"); - return line; - } - - void ParseTextSection(out string text) - { - while ((line = mdFileReader.ReadLine()) is object) - { - if (line.StartsWith('#')) - { - break; - } - - line = FixupLine(line); - docBuilder.AppendLine(line); - } - - text = docBuilder.ToString(); - - docBuilder.Clear(); - } - - IReadOnlyDictionary ParseEnumTable() - { - var enums = new Dictionary(); - int state = 0; - const int StateReadingHeader = 0; - const int StateReadingName = 1; - const int StateLookingForDetail = 2; - const int StateReadingDocColumn = 3; - string? enumName = null; - ulong? enumValue = null; - var docsBuilder = new StringBuilder(); - while ((line = mdFileReader.ReadLine()) is object) - { - if (line == "") - { - break; - } - - switch (state) - { - case StateReadingHeader: - // Reading TR header - if (line == "") - { - state = StateReadingName; - } - - break; - - case StateReadingName: - // Reading an enum row's name column. - Match m = EnumNameCell.Match(line); - if (m.Success) - { - enumName = m.Groups[1].Value; - if (enumName == "0") - { - enumName = "None"; - enumValue = 0; - } - - state = StateLookingForDetail; - } - - break; - - case StateLookingForDetail: - // Looking for an enum row's doc column. - m = EnumOrdinalValue.Match(line); - if (m.Success) - { - string value = m.Groups[1].Value; - bool hex = value.StartsWith("0x", StringComparison.OrdinalIgnoreCase); - if (hex) - { - value = value.Substring(2); - } - - enumValue = ulong.Parse(value, hex ? NumberStyles.HexNumber : NumberStyles.Integer, CultureInfo.InvariantCulture); - } - else if (line.StartsWith("", StringComparison.OrdinalIgnoreCase)) - { - // The row ended before we found the doc column. - state = StateReadingName; - enums.Add(enumName!, (enumValue, null)); - enumName = null; - enumValue = null; - } - - break; - - case StateReadingDocColumn: - // Reading the enum row's doc column. - if (line.StartsWith("", StringComparison.OrdinalIgnoreCase)) - { - state = StateReadingName; - - // Some docs are invalid in documenting the same enum multiple times. - if (!enums.ContainsKey(enumName!)) - { - enums.Add(enumName!, (enumValue, docsBuilder.ToString().Trim())); - } - - enumName = null; - enumValue = null; - docsBuilder.Clear(); - break; - } - - docsBuilder.AppendLine(FixupLine(line)); - break; - } - } - - return enums; - } - - void ParseSection(Match match, IDictionary receivingMap, bool lookForParameterEnums = false, bool lookForFieldEnums = false) - { - string sectionName = match.Groups[1].Value; - bool foundEnum = false; - bool foundEnumIsFlags = false; - while ((line = mdFileReader.ReadLine()) is object) - { - if (line.StartsWith('#')) - { - break; - } - - if (lookForParameterEnums || lookForFieldEnums) - { - if (foundEnum) - { - if (line == "") - { - IReadOnlyDictionary enumNamesAndDocs = ParseEnumTable(); - if (enumNamesAndDocs.Count > 0) - { - var enums = lookForParameterEnums ? enumsByParameter : enumsByField; - if (!enums.ContainsKey(sectionName)) - { - enums.Add(sectionName, new DocEnum(foundEnumIsFlags, enumNamesAndDocs)); - } - } - - lookForParameterEnums = false; - lookForFieldEnums = false; - } - } - else - { - foundEnum = line.Contains("of the following values", StringComparison.OrdinalIgnoreCase); - foundEnumIsFlags = line.Contains("combination of", StringComparison.OrdinalIgnoreCase) - || line.Contains("zero or more of", StringComparison.OrdinalIgnoreCase) - || line.Contains("one or both of", StringComparison.OrdinalIgnoreCase) - || line.Contains("one or more of", StringComparison.OrdinalIgnoreCase); - } - } - - if (!foundEnum) - { - line = FixupLine(line); - docBuilder.AppendLine(line); - } - } - - receivingMap.TryAdd(sectionName, docBuilder.ToString().Trim()); - docBuilder.Clear(); - } - - while (line is object) - { - if (ParameterHeaderPattern.Match(line) is Match { Success: true } parameterMatch) - { - ParseSection(parameterMatch, docs.Parameters, lookForParameterEnums: true); - } - else if (FieldHeaderPattern.Match(line) is Match { Success: true } fieldMatch) - { - ParseSection(fieldMatch, docs.Fields, lookForFieldEnums: true); - } - else if (RemarksHeaderPattern.Match(line) is Match { Success: true } remarksMatch) - { - string remarks; - ParseTextSection(out remarks); - docs.Remarks = remarks; - } - else - { - // TODO: don't break out of this loop so soon... remarks sometimes follows return value docs. - if (line is object && ReturnHeaderPattern.IsMatch(line)) - { - break; - } - - line = mdFileReader.ReadLine(); - } - } - - // Search for return value documentation - while (line is object) - { - Match m = ReturnHeaderPattern.Match(line); - if (m.Success) - { - while ((line = mdFileReader.ReadLine()) is object) - { - if (line.StartsWith('#')) - { - break; - } - - docBuilder.AppendLine(line); - } - - docs.ReturnValue = docBuilder.ToString().Trim(); - docBuilder.Clear(); - break; - } - else - { - line = mdFileReader.ReadLine(); - } - } - - return (properName, docs, enumsByParameter, enumsByField); - } - catch (Exception ex) - { - throw new ApplicationException($"Failed parsing \"{filePath}\".", ex); - } - } - - private class YamlSectionReader : TextReader - { - private readonly StreamReader fileReader; - private bool firstLineRead; - private bool lastLineRead; - - internal YamlSectionReader(StreamReader fileReader) - { - this.fileReader = fileReader; - } - - public override string? ReadLine() - { - if (this.lastLineRead) - { - return null; - } - - if (!this.firstLineRead) - { - Expect("---", this.fileReader.ReadLine()); - this.firstLineRead = true; - } - - string? line = this.fileReader.ReadLine(); - if (line == "---") - { - this.lastLineRead = true; - return null; - } - - return line; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - this.fileReader.Dispose(); - } - - base.Dispose(disposing); - } - } - } -} diff --git a/src/ScrapeDocs/Properties/launchSettings.json b/src/ScrapeDocs/Properties/launchSettings.json deleted file mode 100644 index 1e0b3df6..00000000 --- a/src/ScrapeDocs/Properties/launchSettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "profiles": { - "ScrapeDocs": { - "commandName": "Project", - "commandLineArgs": "ext\\sdk-api\\sdk-api-src\\content obj/Debug/apidocs.yml enums", - "workingDirectory": "C:\\git\\CsWin32" - } - } -} \ No newline at end of file diff --git a/src/ScrapeDocs/ScrapeDocs.csproj b/src/ScrapeDocs/ScrapeDocs.csproj deleted file mode 100644 index 6df9e9cf..00000000 --- a/src/ScrapeDocs/ScrapeDocs.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - Exe - net5.0 - false - - - - - - - - - - - - diff --git a/src/Win32MetaGeneration/Program.cs b/src/Win32MetaGeneration/Program.cs index 127a602c..b4918d28 100644 --- a/src/Win32MetaGeneration/Program.cs +++ b/src/Win32MetaGeneration/Program.cs @@ -44,8 +44,10 @@ private static void Main(string[] args) var sw = Stopwatch.StartNew(); string metadataPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "Windows.Win32.winmd"); + string apiDocsPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "apidocs.msgpack"); using var generator = new Generator( metadataPath, + apiDocsPath, new GeneratorOptions { WideCharOnly = true, diff --git a/src/Win32MetaGeneration/Win32MetaGeneration.csproj b/src/Win32MetaGeneration/Win32MetaGeneration.csproj index 50a637ee..2923782e 100644 --- a/src/Win32MetaGeneration/Win32MetaGeneration.csproj +++ b/src/Win32MetaGeneration/Win32MetaGeneration.csproj @@ -16,6 +16,9 @@ PreserveNewest + + PreserveNewest + diff --git a/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj b/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj index 45908ff8..0505800e 100644 --- a/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj +++ b/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs index 09968421..cea95d55 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs @@ -24,6 +24,7 @@ public class GeneratorTests : IDisposable, IAsyncLifetime private static readonly GeneratorOptions DefaultTestGeneratorOptions = new GeneratorOptions { EmitSingleFile = true }; private static readonly string FileSeparator = new string('=', 140); private static readonly string MetadataPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "Windows.Win32.winmd"); + private static readonly string ApiDocsPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "apidocs.msgpack"); private readonly ITestOutputHelper logger; private readonly Dictionary starterCompilations = new(); private CSharpCompilation compilation; @@ -959,7 +960,7 @@ private async Task CreateCompilationAsync(ReferenceAssemblies return compilation; } - private Generator CreateGenerator(GeneratorOptions? options = null, CSharpCompilation? compilation = null) => new Generator(MetadataPath, options ?? DefaultTestGeneratorOptions, compilation ?? this.compilation, this.parseOptions); + private Generator CreateGenerator(GeneratorOptions? options = null, CSharpCompilation? compilation = null) => new Generator(MetadataPath, ApiDocsPath, options ?? DefaultTestGeneratorOptions, compilation ?? this.compilation, this.parseOptions); private static class MyReferenceAssemblies { diff --git a/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj b/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj index e74bc661..1acd027a 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj +++ b/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj @@ -9,6 +9,9 @@ PreserveNewest + + PreserveNewest + diff --git a/test/SpellChecker/SpellChecker.csproj b/test/SpellChecker/SpellChecker.csproj index e6a63d01..b606617a 100644 --- a/test/SpellChecker/SpellChecker.csproj +++ b/test/SpellChecker/SpellChecker.csproj @@ -11,6 +11,7 @@ +