Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 75 additions & 11 deletions src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@ private enum FriendlyOverloadOf

private static ParameterSyntax StripAttributes(ParameterSyntax parameter) => parameter.WithAttributeLists(default);

private static SyntaxTriviaList CopyDocumentationForFriendlyOverload(MethodDeclarationSyntax method, IEnumerable<ParameterSyntax> parameters)
{
HashSet<string> 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<XmlNodeSyntax> content = SyntaxFactory.List(documentation.Content.Where(node =>
node is not XmlElementSyntax { StartTag.Name.LocalName.ValueText: "param" } paramDoc ||
paramDoc.StartTag.Attributes.OfType<XmlNameAttributeSyntax>()
.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,
Expand All @@ -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<QualifiedNameSyntax>())
{
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;
Expand Down Expand Up @@ -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
{
Expand Down
4 changes: 2 additions & 2 deletions src/Microsoft.Windows.CsWin32/templates/PWSTR.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ partial struct PWSTR
{
public static implicit operator PCWSTR(PWSTR value) => new PCWSTR(value.Value);

/// <inheritdoc cref="PCWSTR.Length"/>
/// <inheritdoc cref="P:Windows.Win32.Foundation.PCWSTR.Length"/>
internal int Length => new PCWSTR(this.Value).Length;

/// <inheritdoc cref="PCWSTR.ToString()"/>
/// <inheritdoc cref="M:Windows.Win32.Foundation.PCWSTR.ToString"/>
public override string ToString() => new PCWSTR(this.Value).ToString();

#if canUseSpan
Expand Down
61 changes: 61 additions & 0 deletions test/Microsoft.Windows.CsWin32.Tests/FriendlyOverloadTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,67 @@ public void OutPWSTR_Parameters_AsSpan()
Assert.Equal("Span<char>", friendlyOverload.ParameterList.Parameters[1].Type?.ToString());
}

/// <summary>
/// Verifies that documentation compiles with public ABI types from one assembly and, optionally, an internal copy in another.
/// </summary>
[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<MethodDeclarationSyntax> 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)
Expand Down
Loading