From 0b550e75b80261aea078ddd2ca69d8c39faa6b64 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sun, 12 Jul 2026 22:35:14 -0700 Subject: [PATCH] Qualify nested type references with their containing types A reference to a nested type (e.g. A::Inner) was reduced to just its last name segment, so referencing it from another scope produced an unqualified name that doesn't resolve in C#. Walk the enclosing record decls and prefix them (A.Inner), while continuing to flatten namespaces as before. Fixes #579 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PInvokeGenerator.TypeResolution.cs | 14 +++++ .../NestedTypeReferenceTest.cs | 54 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/ClangSharp.PInvokeGenerator.UnitTests/NestedTypeReferenceTest.cs diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.TypeResolution.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.TypeResolution.cs index f5e4d03c..0ec17839 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.TypeResolution.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.TypeResolution.cs @@ -388,6 +388,20 @@ private string GetTypeName(Cursor? cursor, Cursor? context, Type rootType, Type { result.typeName = result.typeName.Split(s_doubleColonSeparator, StringSplitOptions.RemoveEmptyEntries).Last(); result.typeName = GetRemappedName(result.typeName, cursor, tryRemapOperatorName: false, out _, skipUsing: true); + + // A nested type needs to be qualified by its containing type(s) so it resolves + // when referenced from another scope (e.g. `A::Inner` -> `A.Inner`). Namespaces + // are flattened away, so only walk the enclosing record decls. + + var qualificationBuilder = new StringBuilder(); + + for (var declContext = tagType.Decl.DeclContext; declContext is RecordDecl parentRecordDecl; declContext = parentRecordDecl.DeclContext) + { + var parentRecordDeclName = GetRemappedCursorName(parentRecordDecl, out _, skipUsing: true); + _ = qualificationBuilder.Insert(0, '.').Insert(0, EscapeName(parentRecordDeclName)); + } + + result.typeName = qualificationBuilder.Append(result.typeName).ToString(); } } else if (type is TemplateSpecializationType templateSpecializationType) diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/NestedTypeReferenceTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/NestedTypeReferenceTest.cs new file mode 100644 index 00000000..10688ab5 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/NestedTypeReferenceTest.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation and Contributors. All Rights Reserved. Licensed under the MIT License (MIT). See License.md in the repository root for more information. + +using System.Threading.Tasks; +using NUnit.Framework; + +namespace ClangSharp.UnitTests; + +/// +/// Regression test for https://github.com/dotnet/ClangSharp/issues/579. +/// A nested type referenced from another scope must be qualified by its containing type(s) +/// (e.g. A::Inner -> A.Inner) so it resolves in the generated C#. +/// +[Platform("win")] +public sealed class NestedTypeReferenceTest : PInvokeGeneratorTest +{ + [Test] + public Task NestedTypeIsQualified() + { + var inputContents = @"struct A +{ + struct Inner + { + int value; + }; +}; + +struct B +{ + A::Inner inner; +}; +"; + + var expectedOutputContents = @"namespace ClangSharp.Test +{ + public partial struct A + { + + public partial struct Inner + { + public int value; + } + } + + public partial struct B + { + [NativeTypeName(""A::Inner"")] + public A.Inner inner; + } +} +"; + + return ValidateGeneratedCSharpLatestWindowsBindingsAsync(inputContents, expectedOutputContents); + } +}