diff --git a/src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs b/src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs index 8aea0c61..054af10c 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs @@ -22,6 +22,25 @@ private enum FriendlyOverloadOf private static ParameterSyntax StripAttributes(ParameterSyntax parameter) => parameter.WithAttributeLists(default); + private static SyntaxTriviaList CopyDocumentationForFriendlyOverload(MethodDeclarationSyntax method, IEnumerable parameters) + { + HashSet parameterNames = new(parameters.Select(parameter => parameter.Identifier.ValueText), StringComparer.Ordinal); + SyntaxTriviaList leadingTrivia = method.GetLeadingTrivia(); + foreach (SyntaxTrivia trivia in leadingTrivia) + { + if (trivia.GetStructure() is DocumentationCommentTriviaSyntax documentation) + { + SyntaxList content = SyntaxFactory.List(documentation.Content.Where(node => + node is not XmlElementSyntax { StartTag.Name.LocalName.ValueText: "param" } paramDoc || + paramDoc.StartTag.Attributes.OfType() + .Any(attribute => attribute.Name.LocalName.ValueText == "name" && parameterNames.Contains(attribute.Identifier.Identifier.ValueText)))); + leadingTrivia = leadingTrivia.Replace(trivia, Trivia(documentation.WithContent(content))); + } + } + + return leadingTrivia; + } + private static ExpressionSyntax GetSpanLength(ExpressionSyntax span, bool isRefType) => isRefType ? ParenthesizedExpression(BinaryExpression( SyntaxKind.CoalesceExpression, @@ -35,6 +54,41 @@ private static ExpressionSyntax GetIsSpanEmpty(ExpressionSyntax span, bool isRef LiteralExpression(SyntaxKind.NullLiteralExpression))) : MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, span, IdentifierName(nameof(Span<>.IsEmpty))); + private bool HasAmbiguousCrefParameterTypes(ParameterListSyntax parameterList) + { + if (this.compilation is null) + { + return false; + } + + foreach (ParameterSyntax parameter in parameterList.Parameters) + { + foreach (QualifiedNameSyntax typeName in parameter.Type!.DescendantNodesAndSelf().OfType()) + { + string metadataName = typeName.ToString(); + if (metadataName.StartsWith(GlobalWinmdRootNamespaceAlias + ".", StringComparison.Ordinal)) + { + metadataName = this.MetadataIndex.CommonNamespace + metadataName.Substring(GlobalWinmdRootNamespaceAlias.Length); + } + else if (metadataName.StartsWith(GlobalNamespacePrefix, StringComparison.Ordinal)) + { + metadataName = metadataName.Substring(GlobalNamespacePrefix.Length); + } + else + { + continue; + } + + if (this.compilation.GetTypesByMetadataName(metadataName).Length > 1) + { + return true; + } + } + } + + return false; + } + private ExpressionSyntax GetIntPtrFromTypeDef(ExpressionSyntax typedefValue, TypeHandleInfo typeDefTypeInfo) { ExpressionSyntax intPtrValue = typedefValue; @@ -1405,17 +1459,27 @@ bool TryHandleCountParam(TypeSyntax elementType, bool nullableSource) parameters.RemoveAt(indexToRemove); } - TypeSyntax docRefExternName = overloadOf == FriendlyOverloadOf.InterfaceMethod - ? QualifiedName(declaringTypeName, IdentifierName(externMethodDeclaration.Identifier)) - : IdentifierName(externMethodDeclaration.Identifier); - SyntaxTrivia leadingTrivia = Trivia( - DocumentationCommentTrivia( - SyntaxKind.SingleLineDocumentationCommentTrivia, - [ - XmlText($"/// "), - XmlEmptyElement("inheritdoc", [XmlCrefAttribute(NameMemberCref(docRefExternName, ToCref(externMethodDeclaration.ParameterList)))]), - XmlText(XmlTextNewLine("\n", continueXmlDocumentationComment: false)) - ])); + SyntaxTriviaList leadingTrivia; + if (this.HasAmbiguousCrefParameterTypes(externMethodDeclaration.ParameterList)) + { + // A hidden type with the same name in another reference can break cref binding, even when the method signature compiles. + leadingTrivia = CopyDocumentationForFriendlyOverload(externMethodDeclaration, parameters); + } + else + { + TypeSyntax docRefExternName = overloadOf == FriendlyOverloadOf.InterfaceMethod + ? QualifiedName(declaringTypeName, IdentifierName(externMethodDeclaration.Identifier)) + : IdentifierName(externMethodDeclaration.Identifier); + leadingTrivia = TriviaList(Trivia( + DocumentationCommentTrivia( + SyntaxKind.SingleLineDocumentationCommentTrivia, + [ + XmlText($"/// "), + XmlEmptyElement("inheritdoc", [XmlCrefAttribute(NameMemberCref(docRefExternName, ToCref(externMethodDeclaration.ParameterList)))]), + XmlText(XmlTextNewLine("\n", continueXmlDocumentationComment: false)) + ]))); + } + ExpressionSyntax externInvocation = InvocationExpression( overloadOf switch { diff --git a/src/Microsoft.Windows.CsWin32/templates/PWSTR.cs b/src/Microsoft.Windows.CsWin32/templates/PWSTR.cs index 8d3a669d..1a83916e 100644 --- a/src/Microsoft.Windows.CsWin32/templates/PWSTR.cs +++ b/src/Microsoft.Windows.CsWin32/templates/PWSTR.cs @@ -6,10 +6,10 @@ partial struct PWSTR { public static implicit operator PCWSTR(PWSTR value) => new PCWSTR(value.Value); - /// + /// internal int Length => new PCWSTR(this.Value).Length; - /// + /// public override string ToString() => new PCWSTR(this.Value).ToString(); #if canUseSpan diff --git a/test/Microsoft.Windows.CsWin32.Tests/FriendlyOverloadTests.cs b/test/Microsoft.Windows.CsWin32.Tests/FriendlyOverloadTests.cs index 1b4e4eb8..2f87901a 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/FriendlyOverloadTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/FriendlyOverloadTests.cs @@ -70,6 +70,67 @@ public void OutPWSTR_Parameters_AsSpan() Assert.Equal("Span", friendlyOverload.ParameterList.Parameters[1].Type?.ToString()); } + /// + /// Verifies that documentation compiles with public ABI types from one assembly and, optionally, an internal copy in another. + /// + [Theory] + [InlineData("CreateWindowEx", false, LanguageVersion.CSharp12, true)] + [InlineData("CreateWindowEx", false, LanguageVersion.CSharp12, false)] + [InlineData("CreateWindowEx", false, LanguageVersion.CSharp14, false)] + [InlineData("CreateWindowEx", false, LanguageVersion.CSharp14, true)] + [InlineData("CreateWindowEx", true, LanguageVersion.CSharp14, false)] + [InlineData("CreateWindowEx", true, LanguageVersion.CSharp14, true)] + [InlineData("ConvertStringSecurityDescriptorToSecurityDescriptor", false, LanguageVersion.CSharp12, false)] + [InlineData("ConvertStringSecurityDescriptorToSecurityDescriptor", false, LanguageVersion.CSharp12, true)] + public void FriendlyOverloadDocumentationWithReferencedPCWSTR(string methodName, bool extensionReceiver, LanguageVersion languageVersion, bool includeHiddenPCWSTR) + { + this.parseOptions = this.parseOptions.WithLanguageVersion(languageVersion); + CSharpCompilationOptions options = this.starterCompilations["net8.0"].Options; + this.compilation = this.starterCompilations["net8.0"].WithOptions(options + .WithPlatform(Platform.X64) + .WithSpecificDiagnosticOptions(options.SpecificDiagnosticOptions + .SetItem("CS1574", ReportDiagnostic.Error) + .SetItem("CS1580", ReportDiagnostic.Error))); + CSharpCompilation referencedProject = this.compilation.WithAssemblyName("ReferencedInterop"); + using (var referencedGenerator = this.CreateGenerator(new GeneratorOptions { Public = true }, referencedProject)) + { + Assert.True(referencedGenerator.TryGenerate("FindWindow", TestContext.Current.CancellationToken)); + referencedProject = this.AddGeneratedCode(referencedProject, referencedGenerator); + } + + this.AssertNoDiagnostics(referencedProject, logAllGeneratedCode: false); + using var assemblyStream = new MemoryStream(); + Assert.True(referencedProject.Emit(assemblyStream, cancellationToken: TestContext.Current.CancellationToken).Success); + this.compilation = this.compilation.AddReferences(MetadataReference.CreateFromImage(assemblyStream.ToArray())); + if (includeHiddenPCWSTR) + { + // A dependency unrelated to CsWin32 can embed an internal ABI type with the same name. + CSharpCompilation hiddenProject = this.starterCompilations["net8.0"] + .WithAssemblyName("HiddenInterop") + .AddSyntaxTrees(CSharpSyntaxTree.ParseText( + "namespace Windows.Win32.Foundation { internal struct PCWSTR { } }", + this.parseOptions, + cancellationToken: TestContext.Current.CancellationToken)); + using var hiddenStream = new MemoryStream(); + Assert.True(hiddenProject.Emit(hiddenStream, cancellationToken: TestContext.Current.CancellationToken).Success); + this.compilation = this.compilation.AddReferences(MetadataReference.CreateFromImage(hiddenStream.ToArray())); + Assert.Equal(2, this.compilation.GetTypesByMetadataName("Windows.Win32.Foundation.PCWSTR").Length); + } + + this.generator = this.CreateGenerator( + new GeneratorOptions + { + EmitSingleFile = false, + ClassName = extensionReceiver ? "PInvokeExtensions" : "PInvoke", + ExtensionReceiver = extensionReceiver ? "PInvoke" : null, + }, + includeDocs: true); + this.GenerateApi(methodName); + Assert.Empty(this.FindGeneratedType("PCWSTR")); + IEnumerable friendlyOverloads = this.FindGeneratedMethod(methodName).Where(m => !IsOrContainsExternMethod(m)); + Assert.NotEmpty(friendlyOverloads); + } + [Theory] [InlineData("WSManGetSessionOptionAsString")] // Uses the reserved keyword 'string' as a parameter name [InlineData("RmRegisterResources")] // Parameter with PCWSTR* (an array of native strings)