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 @@