From 9023a6b430eebaa65f6b5b1b9cee1c2b102a2b8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:59:34 +0000 Subject: [PATCH 1/2] Support generating PNSE stubs from implementation sources with docs preserved Co-authored-by: ericstj <8918108+ericstj@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/arcade/sessions/636beb1a-1195-4d41-b167-5ba7a1877d6e --- .../NotSupportedAssemblyGenerator.cs | 64 +++++++++++++++---- ...oft.DotNet.GenFacades.NotSupported.targets | 15 ++++- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs b/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs index 467858088e8..e1651ae0576 100644 --- a/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs +++ b/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs @@ -14,7 +14,7 @@ namespace Microsoft.DotNet.GenFacades { /// - /// The class generates an NotSupportedAssembly from the reference sources. + /// The class generates a NotSupportedAssembly from the reference or implementation sources. /// public class NotSupportedAssemblyGenerator : RoslynBuildTask { @@ -32,7 +32,7 @@ public override bool ExecuteCore() { if (SourceFiles == null || SourceFiles.Length == 0) { - Log.LogError("There are no ref source files."); + Log.LogError("There are no source files."); return false; } @@ -112,7 +112,8 @@ public NotSupportedAssemblyRewriter(string message, string[] exclusionApis) public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) { - if (node.Body == null) + // Abstract/extern methods and interface members have neither body nor expression body. + if (node.Body == null && node.ExpressionBody == null) return node; if (_exclusionApis != null && _exclusionApis.Contains(GetMethodDefinition(node))) @@ -127,7 +128,7 @@ public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) { block = (BlockSyntax)SyntaxFactory.ParseStatement(GetDefaultMessage()); } - return node.WithBody(block); + return node.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default); } public override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax node) @@ -135,6 +136,26 @@ public override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax no if (_exclusionApis != null && _exclusionApis.Contains(GetPropertyDefinition(node))) return null; + // Handle expression-bodied properties (e.g., `public int X => _x;`). + // Convert them to a getter-only property that throws. + if (node.ExpressionBody != null) + { + var getterBlock = (BlockSyntax)SyntaxFactory.ParseStatement(GetDefaultMessage()); + var getterAccessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithBody(getterBlock); + var accessorList = SyntaxFactory.AccessorList(SyntaxFactory.SingletonList(getterAccessor)); + return node.WithExpressionBody(null) + .WithSemicolonToken(default) + .WithInitializer(null) + .WithAccessorList(accessorList); + } + + // Strip property initializers (e.g., `public int X { get; } = 5;`). + if (node.Initializer != null) + { + node = node.WithInitializer(null).WithSemicolonToken(default); + } + return base.VisitPropertyDeclaration(node); } @@ -157,42 +178,57 @@ public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) public override SyntaxNode VisitConstructorDeclaration(ConstructorDeclarationSyntax node) { BlockSyntax block = (BlockSyntax)SyntaxFactory.ParseStatement(GetDefaultMessage()); - return node.WithBody(block); + return node.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default); } public override SyntaxNode VisitDestructorDeclaration(DestructorDeclarationSyntax node) { BlockSyntax block = (BlockSyntax)SyntaxFactory.ParseStatement(emptyBody); - return node.WithBody(block); + return node.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default); } public override SyntaxNode VisitAccessorDeclaration(AccessorDeclarationSyntax node) { - if (node.Body == null) + // Auto-accessors and interface accessors have neither body nor expression body. + if (node.Body == null && node.ExpressionBody == null) return node; - string message = "{ throw new System.PlatformNotSupportedException(" + $"{ _message }); "+ " } "; - BlockSyntax block = (BlockSyntax)SyntaxFactory.ParseStatement(message); + BlockSyntax block = (BlockSyntax)SyntaxFactory.ParseStatement(GetDefaultMessage()); - return node.WithBody(block); + return node.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default); } public override SyntaxNode VisitOperatorDeclaration(OperatorDeclarationSyntax node) { - if (node.Body == null) + if (node.Body == null && node.ExpressionBody == null) return node; BlockSyntax block = (BlockSyntax)SyntaxFactory.ParseStatement(GetDefaultMessage()); - return node.WithBody(block); + return node.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default); } public override SyntaxNode VisitConversionOperatorDeclaration(ConversionOperatorDeclarationSyntax node) { - if (node.Body == null) + if (node.Body == null && node.ExpressionBody == null) return node; BlockSyntax block = (BlockSyntax)SyntaxFactory.ParseStatement(GetDefaultMessage()); - return node.WithBody(block); + return node.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default); + } + + public override SyntaxNode VisitVariableDeclarator(VariableDeclaratorSyntax node) + { + // Strip non-const field initializers so that implementation sources with runtime + // initializers (e.g., `private static readonly X s_x = new X();`) compile correctly. + if (node.Initializer != null && + node.Parent is VariableDeclarationSyntax && + node.Parent.Parent is FieldDeclarationSyntax fieldDecl && + !fieldDecl.Modifiers.Any(SyntaxKind.ConstKeyword)) + { + return node.WithInitializer(null); + } + + return base.VisitVariableDeclarator(node); } private string GetFullyQualifiedName(TypeDeclarationSyntax node) diff --git a/src/Microsoft.DotNet.GenFacades/build/Microsoft.DotNet.GenFacades.NotSupported.targets b/src/Microsoft.DotNet.GenFacades/build/Microsoft.DotNet.GenFacades.NotSupported.targets index 46a061c0f5a..e0cebc8783e 100644 --- a/src/Microsoft.DotNet.GenFacades/build/Microsoft.DotNet.GenFacades.NotSupported.targets +++ b/src/Microsoft.DotNet.GenFacades/build/Microsoft.DotNet.GenFacades.NotSupported.targets @@ -5,7 +5,7 @@ AddGenFacadeNotSupportedCompileItem;$(CoreCompileDependsOn) - + false $(NoWarn);CA1823;CA1821;CS0169 @@ -19,7 +19,16 @@ - + + SourceFilesProjectOutputGroup + false + _contractSourceFilesGroup + + false + + SourceFilesProjectOutputGroup false _contractSourceFilesGroup @@ -29,7 +38,7 @@ - + Date: Wed, 25 Mar 2026 22:20:11 +0000 Subject: [PATCH 2/2] GenFacades: enrich PNSE stubs with XML docs from implementation sources via **/*.cs glob Co-authored-by: ericstj <8918108+ericstj@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/arcade/sessions/4e4d36a1-1980-4a25-bd9f-b03f132195f0 --- .../NotSupportedAssemblyGenerator.cs | 424 ++++++++++++++---- ...oft.DotNet.GenFacades.NotSupported.targets | 24 +- 2 files changed, 361 insertions(+), 87 deletions(-) diff --git a/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs b/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs index e1651ae0576..898caaaeb94 100644 --- a/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs +++ b/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs @@ -14,7 +14,8 @@ namespace Microsoft.DotNet.GenFacades { /// - /// The class generates a NotSupportedAssembly from the reference or implementation sources. + /// Generates a not-supported assembly from reference sources, optionally enriching + /// XML documentation comments from implementation sources in the same project. /// public class NotSupportedAssemblyGenerator : RoslynBuildTask { @@ -28,6 +29,13 @@ public class NotSupportedAssemblyGenerator : RoslynBuildTask public string ApiExclusionListPath { get; set; } + /// + /// Optional implementation source files (e.g. **/*.cs for the current project) + /// used to provide XML doc comments for the generated not-supported stubs. + /// Warnings are emitted for public APIs that cannot be located or lack documentation. + /// + public ITaskItem[] ImplementationSourceFiles { get; set; } + public override bool ExecuteCore() { if (SourceFiles == null || SourceFiles.Length == 0) @@ -49,6 +57,15 @@ private void GenerateNotSupportedAssemblyFiles(IEnumerable sourceFile apiExclusions = File.ReadAllLines(ApiExclusionListPath); } + LanguageVersion languageVersion = LanguageVersion.Default; + if (!string.IsNullOrEmpty(LangVersion) && !LanguageVersionFacts.TryParse(LangVersion, out languageVersion)) + { + Log.LogError($"Invalid LangVersion value '{LangVersion}'"); + return; + } + + DocCommentIndex docIndex = BuildDocCommentIndex(languageVersion); + foreach (ITaskItem item in sourceFiles) { string sourceFile = item.ItemSpec; @@ -60,54 +77,285 @@ private void GenerateNotSupportedAssemblyFiles(IEnumerable sourceFile continue; } - GenerateNotSupportedAssemblyForSourceFile(sourceFile, outputPath, apiExclusions); + GenerateNotSupportedAssemblyForSourceFile(sourceFile, outputPath, apiExclusions, languageVersion, docIndex); } } - private void GenerateNotSupportedAssemblyForSourceFile(string sourceFile, string outputPath, string[] apiExclusions) + private DocCommentIndex BuildDocCommentIndex(LanguageVersion languageVersion) { - SyntaxTree syntaxTree; + if (ImplementationSourceFiles == null || ImplementationSourceFiles.Length == 0) + return null; - try + var parseOptions = new CSharpParseOptions(languageVersion); + var index = new DocCommentIndex(); + + foreach (ITaskItem item in ImplementationSourceFiles) { - LanguageVersion languageVersion = LanguageVersion.Default; - if (!String.IsNullOrEmpty(LangVersion) && !LanguageVersionFacts.TryParse(LangVersion, out languageVersion)) + string path = item.ItemSpec; + if (!File.Exists(path)) + continue; + + try { - Log.LogError($"Invalid LangVersion value '{LangVersion}'"); - return; + SyntaxTree tree = CSharpSyntaxTree.ParseText(File.ReadAllText(path), parseOptions); + index.AddSourceTree(tree); } + catch (Exception ex) + { + Log.LogWarning($"Failed to parse implementation source '{path}': {ex.Message}"); + } + } + + return index; + } + + private void GenerateNotSupportedAssemblyForSourceFile( + string sourceFile, + string outputPath, + string[] apiExclusions, + LanguageVersion languageVersion, + DocCommentIndex docIndex) + { + SyntaxTree syntaxTree; + + try + { syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText(sourceFile), new CSharpParseOptions(languageVersion)); } - catch(Exception ex) + catch (Exception ex) { Log.LogErrorFromException(ex, false); return; } - var rewriter = new NotSupportedAssemblyRewriter(Message, apiExclusions); + var rewriter = new NotSupportedAssemblyRewriter(Message, apiExclusions, docIndex); SyntaxNode root = rewriter.Visit(syntaxTree.GetRoot()); string text = root.GetText().ToString(); File.WriteAllText(outputPath, text); + + if (docIndex != null) + { + foreach (string api in rewriter.ApisNotFoundInImplementation) + Log.LogWarning($"Public API '{api}' could not be located in implementation sources."); + + foreach (string api in rewriter.ApisMissingDocumentation) + Log.LogWarning($"Public API '{api}' is missing documentation in implementation sources."); + } } } - internal class NotSupportedAssemblyRewriter : CSharpSyntaxRewriter + /// + /// Builds a lookup from public API keys to their XML doc comment trivia, + /// collected from one or more implementation source files. First occurrence wins. + /// + internal sealed class DocCommentIndex { - private const string emptyBody = "{ }\n"; - private string _message; - private IEnumerable _exclusionApis; + // All APIs seen in implementation sources (with or without docs). + private readonly HashSet _seenApis = new(StringComparer.Ordinal); + // APIs that have at least one XML doc comment (first occurrence wins). + private readonly Dictionary _docTrivia = new(StringComparer.Ordinal); + + public void AddSourceTree(SyntaxTree tree) + { + var walker = new DocCommentWalker(this); + walker.Visit(tree.GetRoot()); + } - public NotSupportedAssemblyRewriter(string message, string[] exclusionApis) + internal void RecordMember(string key, SyntaxTriviaList leading) { - if (message != null && message.StartsWith("SR.")) + _seenApis.Add(key); + + if (!_docTrivia.ContainsKey(key)) { - _message = "System." + message; + var docTrivia = leading + .Where(t => t.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) || + t.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia)) + .ToList(); + + if (docTrivia.Count > 0) + _docTrivia[key] = SyntaxFactory.TriviaList(docTrivia); } - else + } + + public bool HasMember(string key) => _seenApis.Contains(key); + + public bool TryGetDocComment(string key, out SyntaxTriviaList docTrivia) + => _docTrivia.TryGetValue(key, out docTrivia); + } + + /// + /// Walks an implementation source tree and records doc comment trivia for all declared members. + /// + internal sealed class DocCommentWalker : CSharpSyntaxWalker + { + private readonly DocCommentIndex _index; + + public DocCommentWalker(DocCommentIndex index) { _index = index; } + + public override void VisitClassDeclaration(ClassDeclarationSyntax node) + { + _index.RecordMember(ApiKey.GetTypeKey(node), node.GetLeadingTrivia()); + base.VisitClassDeclaration(node); + } + + public override void VisitStructDeclaration(StructDeclarationSyntax node) + { + _index.RecordMember(ApiKey.GetTypeKey(node), node.GetLeadingTrivia()); + base.VisitStructDeclaration(node); + } + + public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) + { + _index.RecordMember(ApiKey.GetTypeKey(node), node.GetLeadingTrivia()); + base.VisitInterfaceDeclaration(node); + } + + public override void VisitRecordDeclaration(RecordDeclarationSyntax node) + { + _index.RecordMember(ApiKey.GetTypeKey(node), node.GetLeadingTrivia()); + base.VisitRecordDeclaration(node); + } + + public override void VisitEnumDeclaration(EnumDeclarationSyntax node) + { + _index.RecordMember(ApiKey.GetEnumKey(node), node.GetLeadingTrivia()); + base.VisitEnumDeclaration(node); + } + + public override void VisitDelegateDeclaration(DelegateDeclarationSyntax node) + { + _index.RecordMember(ApiKey.GetDelegateKey(node), node.GetLeadingTrivia()); + } + + public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) + { + if (node.Parent is TypeDeclarationSyntax parent) + _index.RecordMember(ApiKey.GetMemberKey(parent, ".ctor"), node.GetLeadingTrivia()); + } + + public override void VisitDestructorDeclaration(DestructorDeclarationSyntax node) + { + if (node.Parent is TypeDeclarationSyntax parent) + _index.RecordMember(ApiKey.GetMemberKey(parent, "Finalize"), node.GetLeadingTrivia()); + } + + public override void VisitMethodDeclaration(MethodDeclarationSyntax node) + { + if (node.Parent is TypeDeclarationSyntax parent) + _index.RecordMember(ApiKey.GetMemberKey(parent, node.Identifier.ValueText), node.GetLeadingTrivia()); + } + + public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + if (node.Parent is TypeDeclarationSyntax parent) + _index.RecordMember(ApiKey.GetMemberKey(parent, node.Identifier.ValueText), node.GetLeadingTrivia()); + } + + public override void VisitEventDeclaration(EventDeclarationSyntax node) + { + if (node.Parent is TypeDeclarationSyntax parent) + _index.RecordMember(ApiKey.GetMemberKey(parent, node.Identifier.ValueText), node.GetLeadingTrivia()); + } + + public override void VisitEventFieldDeclaration(EventFieldDeclarationSyntax node) + { + if (node.Parent is TypeDeclarationSyntax parent) { - _message = message; + foreach (VariableDeclaratorSyntax v in node.Declaration.Variables) + _index.RecordMember(ApiKey.GetMemberKey(parent, v.Identifier.ValueText), node.GetLeadingTrivia()); } + } + + public override void VisitFieldDeclaration(FieldDeclarationSyntax node) + { + if (node.Parent is TypeDeclarationSyntax parent) + { + foreach (VariableDeclaratorSyntax v in node.Declaration.Variables) + _index.RecordMember(ApiKey.GetMemberKey(parent, v.Identifier.ValueText), node.GetLeadingTrivia()); + } + } + + public override void VisitOperatorDeclaration(OperatorDeclarationSyntax node) + { + if (node.Parent is TypeDeclarationSyntax parent) + _index.RecordMember(ApiKey.GetOperatorKey(parent, node.OperatorToken.ValueText), node.GetLeadingTrivia()); + } + + public override void VisitConversionOperatorDeclaration(ConversionOperatorDeclarationSyntax node) + { + if (node.Parent is TypeDeclarationSyntax parent) + _index.RecordMember(ApiKey.GetConversionOperatorKey(parent, node.ImplicitOrExplicitKeyword.ValueText), node.GetLeadingTrivia()); + } + + public override void VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) + { + if (node.Parent is EnumDeclarationSyntax parentEnum) + _index.RecordMember(ApiKey.GetEnumKey(parentEnum) + "." + node.Identifier.ValueText, node.GetLeadingTrivia()); + } + } + + /// + /// Helpers for computing consistent, fully-qualified API keys from syntax nodes. + /// Handles regular namespaces, file-scoped namespaces, nested types, and top-level types. + /// + internal static class ApiKey + { + public static string GetTypeKey(TypeDeclarationSyntax node) + => Qualify(GetParentPrefix(node.Parent), node.Identifier.ValueText.Trim()); + + public static string GetEnumKey(EnumDeclarationSyntax node) + => Qualify(GetParentPrefix(node.Parent), node.Identifier.ValueText.Trim()); + + public static string GetDelegateKey(DelegateDeclarationSyntax node) + => Qualify(GetParentPrefix(node.Parent), node.Identifier.ValueText.Trim()); + + public static string GetMemberKey(TypeDeclarationSyntax parent, string memberName) + => GetTypeKey(parent) + "." + memberName; + + public static string GetOperatorKey(TypeDeclarationSyntax parent, string operatorToken) + => GetTypeKey(parent) + ".op_" + operatorToken; + + public static string GetConversionOperatorKey(TypeDeclarationSyntax parent, string implicitOrExplicit) + => GetTypeKey(parent) + "." + implicitOrExplicit + " operator"; + + private static string GetParentPrefix(SyntaxNode parent) => parent switch + { + NamespaceDeclarationSyntax ns => ns.Name.ToFullString().Trim(), + FileScopedNamespaceDeclarationSyntax fns => fns.Name.ToFullString().Trim(), + TypeDeclarationSyntax type => GetTypeKey(type), + _ => string.Empty, + }; + + private static string Qualify(string prefix, string name) + => string.IsNullOrEmpty(prefix) ? name : prefix + "." + name; + } + + internal class NotSupportedAssemblyRewriter : CSharpSyntaxRewriter + { + private readonly string _message; + private readonly IEnumerable _exclusionApis; + private readonly DocCommentIndex _docIndex; + private readonly List _apisNotFoundInImplementation = new(); + private readonly List _apisMissingDocs = new(); + + public IReadOnlyList ApisNotFoundInImplementation => _apisNotFoundInImplementation; + public IReadOnlyList ApisMissingDocumentation => _apisMissingDocs; + + public NotSupportedAssemblyRewriter(string message, string[] exclusionApis, DocCommentIndex docIndex = null) + { + _message = message != null && message.StartsWith("SR.") ? "System." + message : message; _exclusionApis = exclusionApis?.Select(t => t.Substring(t.IndexOf(':') + 1)); + _docIndex = docIndex; + } + + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + string key = ApiKey.GetTypeKey(node); + if (_exclusionApis != null && _exclusionApis.Contains(key)) + return null; + + SyntaxNode result = base.VisitClassDeclaration(node); + return ApplyDocComment(result, key); } public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) @@ -116,75 +364,69 @@ public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) if (node.Body == null && node.ExpressionBody == null) return node; - if (_exclusionApis != null && _exclusionApis.Contains(GetMethodDefinition(node))) + string key = ApiKey.GetMemberKey((TypeDeclarationSyntax)node.Parent, node.Identifier.ValueText); + if (_exclusionApis != null && _exclusionApis.Contains(key)) return null; - BlockSyntax block; - if (node.Identifier.ValueText == "Dispose" || node.Identifier.ValueText == "Finalize") - { - block = (BlockSyntax)SyntaxFactory.ParseStatement(emptyBody); - } - else - { - block = (BlockSyntax)SyntaxFactory.ParseStatement(GetDefaultMessage()); - } - return node.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default); + BlockSyntax block = node.Identifier.ValueText == "Dispose" || node.Identifier.ValueText == "Finalize" + ? CreateEmptyBlock() + : CreateThrowBlock(); + SyntaxNode result = node.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default); + return ApplyDocComment(result, key); } public override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax node) { - if (_exclusionApis != null && _exclusionApis.Contains(GetPropertyDefinition(node))) + string key = ApiKey.GetMemberKey((TypeDeclarationSyntax)node.Parent, node.Identifier.ValueText); + if (_exclusionApis != null && _exclusionApis.Contains(key)) return null; + SyntaxNode result; + // Handle expression-bodied properties (e.g., `public int X => _x;`). // Convert them to a getter-only property that throws. if (node.ExpressionBody != null) { - var getterBlock = (BlockSyntax)SyntaxFactory.ParseStatement(GetDefaultMessage()); var getterAccessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) - .WithBody(getterBlock); + .WithBody(CreateThrowBlock()); var accessorList = SyntaxFactory.AccessorList(SyntaxFactory.SingletonList(getterAccessor)); - return node.WithExpressionBody(null) - .WithSemicolonToken(default) - .WithInitializer(null) - .WithAccessorList(accessorList); + result = node.WithExpressionBody(null) + .WithSemicolonToken(default) + .WithInitializer(null) + .WithAccessorList(accessorList); } - - // Strip property initializers (e.g., `public int X { get; } = 5;`). - if (node.Initializer != null) + else { - node = node.WithInitializer(null).WithSemicolonToken(default); + // Strip property initializers (e.g., `public int X { get; } = 5;`). + if (node.Initializer != null) + node = node.WithInitializer(null).WithSemicolonToken(default); + + result = base.VisitPropertyDeclaration(node); } - return base.VisitPropertyDeclaration(node); + return ApplyDocComment(result, key); } public override SyntaxNode VisitEventDeclaration(EventDeclarationSyntax node) { - if (_exclusionApis != null && _exclusionApis.Contains(GetEventDefinition(node))) + string key = ApiKey.GetMemberKey((TypeDeclarationSyntax)node.Parent, node.Identifier.ValueText); + if (_exclusionApis != null && _exclusionApis.Contains(key)) return null; - return base.VisitEventDeclaration(node); - } - - public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) - { - if (_exclusionApis != null && _exclusionApis.Contains(GetFullyQualifiedName(node))) - return null; - - return base.VisitClassDeclaration(node); + SyntaxNode result = base.VisitEventDeclaration(node); + return ApplyDocComment(result, key); } public override SyntaxNode VisitConstructorDeclaration(ConstructorDeclarationSyntax node) { - BlockSyntax block = (BlockSyntax)SyntaxFactory.ParseStatement(GetDefaultMessage()); - return node.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default); + string key = ApiKey.GetMemberKey((TypeDeclarationSyntax)node.Parent, ".ctor"); + SyntaxNode result = node.WithBody(CreateThrowBlock()).WithExpressionBody(null).WithSemicolonToken(default); + return ApplyDocComment(result, key); } public override SyntaxNode VisitDestructorDeclaration(DestructorDeclarationSyntax node) { - BlockSyntax block = (BlockSyntax)SyntaxFactory.ParseStatement(emptyBody); - return node.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default); + return node.WithBody(CreateEmptyBlock()).WithExpressionBody(null).WithSemicolonToken(default); } public override SyntaxNode VisitAccessorDeclaration(AccessorDeclarationSyntax node) @@ -193,9 +435,7 @@ public override SyntaxNode VisitAccessorDeclaration(AccessorDeclarationSyntax no if (node.Body == null && node.ExpressionBody == null) return node; - BlockSyntax block = (BlockSyntax)SyntaxFactory.ParseStatement(GetDefaultMessage()); - - return node.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default); + return node.WithBody(CreateThrowBlock()).WithExpressionBody(null).WithSemicolonToken(default); } public override SyntaxNode VisitOperatorDeclaration(OperatorDeclarationSyntax node) @@ -203,8 +443,9 @@ public override SyntaxNode VisitOperatorDeclaration(OperatorDeclarationSyntax no if (node.Body == null && node.ExpressionBody == null) return node; - BlockSyntax block = (BlockSyntax)SyntaxFactory.ParseStatement(GetDefaultMessage()); - return node.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default); + string key = ApiKey.GetOperatorKey((TypeDeclarationSyntax)node.Parent, node.OperatorToken.ValueText); + SyntaxNode result = node.WithBody(CreateThrowBlock()).WithExpressionBody(null).WithSemicolonToken(default); + return ApplyDocComment(result, key); } public override SyntaxNode VisitConversionOperatorDeclaration(ConversionOperatorDeclarationSyntax node) @@ -212,8 +453,9 @@ public override SyntaxNode VisitConversionOperatorDeclaration(ConversionOperator if (node.Body == null && node.ExpressionBody == null) return node; - BlockSyntax block = (BlockSyntax)SyntaxFactory.ParseStatement(GetDefaultMessage()); - return node.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default); + string key = ApiKey.GetConversionOperatorKey((TypeDeclarationSyntax)node.Parent, node.ImplicitOrExplicitKeyword.ValueText); + SyntaxNode result = node.WithBody(CreateThrowBlock()).WithExpressionBody(null).WithSemicolonToken(default); + return ApplyDocComment(result, key); } public override SyntaxNode VisitVariableDeclarator(VariableDeclaratorSyntax node) @@ -231,29 +473,55 @@ node.Parent.Parent is FieldDeclarationSyntax fieldDecl && return base.VisitVariableDeclarator(node); } - private string GetFullyQualifiedName(TypeDeclarationSyntax node) + /// + /// If implementation sources are available, replaces the node's XML doc comment trivia + /// with the implementation's doc comment. Records a warning-level diagnostic if the API + /// could not be found or has no documentation. + /// + private SyntaxNode ApplyDocComment(SyntaxNode node, string key) { - string parent; - if (node.Parent is NamespaceDeclarationSyntax parentNamespace) + if (_docIndex == null || node == null) + return node; + + if (_docIndex.TryGetDocComment(key, out SyntaxTriviaList implDocTrivia)) { - parent = GetFullyQualifiedName(parentNamespace); + return node.WithLeadingTrivia(ReplaceDocTrivia(node.GetLeadingTrivia(), implDocTrivia)); } + + if (!_docIndex.HasMember(key)) + _apisNotFoundInImplementation.Add(key); else - { - parent = GetFullyQualifiedName((TypeDeclarationSyntax)node.Parent); - } + _apisMissingDocs.Add(key); - return parent + "." + node.Identifier.ValueText.Trim(); + return node; } - private string GetFullyQualifiedName(NamespaceDeclarationSyntax node) => node.Name.ToFullString().Trim(); - - private string GetMethodDefinition(MethodDeclarationSyntax node) => GetFullyQualifiedName((TypeDeclarationSyntax)node.Parent) + "." + node.Identifier.ValueText; + /// + /// Replaces any XML doc comment trivia in with + /// , preserving surrounding whitespace/indentation. + /// + private static SyntaxTriviaList ReplaceDocTrivia(SyntaxTriviaList existing, SyntaxTriviaList implDocs) + { + // Remove existing doc-comment trivia. + var withoutDocs = existing + .Where(t => !t.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) && + !t.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia)) + .ToList(); + + // Insert implementation docs just before the trailing indentation whitespace that + // immediately precedes the declaration keyword. + int insertAt = withoutDocs.Count; + while (insertAt > 0 && withoutDocs[insertAt - 1].IsKind(SyntaxKind.WhitespaceTrivia)) + insertAt--; + + withoutDocs.InsertRange(insertAt, implDocs); + return SyntaxFactory.TriviaList(withoutDocs); + } - private string GetPropertyDefinition(PropertyDeclarationSyntax node) => GetFullyQualifiedName((TypeDeclarationSyntax)node.Parent) + "." + node.Identifier.ValueText; + private string GetDefaultMessage() => "{ throw new System.PlatformNotSupportedException(" + $"{ _message }); " + " }\n"; - private string GetEventDefinition(EventDeclarationSyntax node) => GetFullyQualifiedName((TypeDeclarationSyntax)node.Parent) + "." + node.Identifier.ValueText; + private BlockSyntax CreateThrowBlock() => (BlockSyntax)SyntaxFactory.ParseStatement(GetDefaultMessage()); - private string GetDefaultMessage() => "{ throw new System.PlatformNotSupportedException(" + $"{ _message }); " + " }\n"; + private static BlockSyntax CreateEmptyBlock() => (BlockSyntax)SyntaxFactory.ParseStatement("{ }\n"); } } diff --git a/src/Microsoft.DotNet.GenFacades/build/Microsoft.DotNet.GenFacades.NotSupported.targets b/src/Microsoft.DotNet.GenFacades/build/Microsoft.DotNet.GenFacades.NotSupported.targets index e0cebc8783e..7d5c8865e2c 100644 --- a/src/Microsoft.DotNet.GenFacades/build/Microsoft.DotNet.GenFacades.NotSupported.targets +++ b/src/Microsoft.DotNet.GenFacades/build/Microsoft.DotNet.GenFacades.NotSupported.targets @@ -5,7 +5,7 @@ AddGenFacadeNotSupportedCompileItem;$(CoreCompileDependsOn) - + false $(NoWarn);CA1823;CA1821;CS0169 @@ -18,6 +18,19 @@ + + + + + @@ -27,14 +40,6 @@ false - - SourceFilesProjectOutputGroup - false - _contractSourceFilesGroup - - false - @@ -64,6 +69,7 @@