From c1807a5b6823845d24ce3ce1f6b96e1e96d20d90 Mon Sep 17 00:00:00 2001 From: Nanook Date: Sat, 6 Jun 2026 16:18:01 +0000 Subject: [PATCH] fix: document inherited COM interface methods --- .../Generator.Com.cs | 6 +++- .../COMTests.cs | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs index 951ec2f7..7ccd1ae1 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs @@ -1014,13 +1014,17 @@ static ExpressionSyntax ThisPointer(PointerTypeSyntax? typedPointer = null) propertyOrMethod = methodDeclaration; } + string apiDocsKey = $"{ifaceName}.{methodName}"; if (inheritedMethods >= 0) { propertyOrMethod = propertyOrMethod.AddModifiers(TokenWithSpace(SyntaxKind.NewKeyword)); + TypeDefinition declaringType = methodDefinition.Reader.GetTypeDefinition(methodDefinition.Method.GetDeclaringType()); + string declaringIfaceName = methodDefinition.Reader.GetString(declaringType.Name); + apiDocsKey = $"{declaringIfaceName}.{methodName}"; } // Add documentation if we can find it. - propertyOrMethod = this.AddApiDocumentation($"{ifaceName}.{methodName}", propertyOrMethod); + propertyOrMethod = this.AddApiDocumentation(apiDocsKey, propertyOrMethod); // In source-generator mode, GeneratedComInterface handles inheritance automatically, so inherited // methods are NOT re-declared on the derived interface. We still need to emit the friendly overloads diff --git a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs index ebcd3bd1..8722c005 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs @@ -34,6 +34,28 @@ public void IDispatchInterfaceIsDual(string ifaceName) Assert.Equal(nameof(ComInterfaceType.InterfaceIsDual), arg.Name.Identifier.ValueText); } + [Fact] + public void InheritedCOMInterfaceMethodsUseBaseMethodDocs() + { + this.generator = this.CreateGenerator(includeDocs: true); + this.GenerateApi("IShellDispatch2"); + + InterfaceDeclarationSyntax baseInterface = Assert.Single(this.FindGeneratedType("IShellDispatch").OfType()); + MethodDeclarationSyntax baseFileRun = Assert.Single(baseInterface.Members.OfType(), m => m.Identifier.ValueText == "FileRun"); + + InterfaceDeclarationSyntax derivedInterface = Assert.Single(this.FindGeneratedType("IShellDispatch2").OfType()); + MethodDeclarationSyntax derivedFileRun = Assert.Single(derivedInterface.Members.OfType(), m => m.Identifier.ValueText == "FileRun"); + MethodDeclarationSyntax derivedIsRestricted = Assert.Single(derivedInterface.Members.OfType(), m => m.Identifier.ValueText == "IsRestricted"); + + Assert.Equal("void", derivedFileRun.ReturnType.ToString()); + Assert.Empty(derivedFileRun.ParameterList.Parameters); + Assert.Contains(derivedFileRun.Modifiers, m => m.IsKind(SyntaxKind.NewKeyword)); + string? baseFileRunDocs = GetDocumentationComment(baseFileRun); + Assert.NotNull(baseFileRunDocs); + Assert.Equal(baseFileRunDocs, GetDocumentationComment(derivedFileRun)); + Assert.NotNull(GetDocumentationComment(derivedIsRestricted)); + } + [Fact] public void CreateDispatcherQueueController_CreatesWinRTCustomMarshaler() { @@ -1073,4 +1095,11 @@ public void COMStructWrappers_ClsPragmaSuppressionsAreLoadBearing_Issue1703() private static string StripWarningDisablePragmas(string code) => string.Join("\n", code.Split('\n').Where(line => !line.TrimStart().StartsWith("#pragma warning disable", StringComparison.Ordinal))); + + private static string? GetDocumentationComment(MemberDeclarationSyntax member) => + member.GetLeadingTrivia() + .Select(t => t.GetStructure()) + .OfType() + .SingleOrDefault() + ?.ToFullString(); }