From c3009fdef840bb43021ab63eb194a669e2529613 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Mon, 13 Jul 2026 00:05:46 -0700 Subject: [PATCH] Disambiguate nested record names that clash with a sibling field A named nested record is legal in C even when a sibling field shares its name, but the two map to a nested type and a field of the same name in C#, which is a CS0102 clash. Auto-rename the type using the anonymous-record naming (e.g. '_Name_e__Struct') so the declaration and every reference stay consistent, and emit a warning once per rename pointing at the new options. Add '--remap-type' and '--remap-field' so a type and a field that share a name can be disambiguated explicitly; these take precedence over the general '--remap' and are keyed by cursor kind so resolving a field's type no longer picks up a field remapping. The clash is detected via the lexical parent so it also fires in C, where a nested record is semantically promoted to the enclosing scope yet still emitted nested. Fixes #606 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 2 + .../PInvokeGenerator.Naming.cs | 111 +++++++++++++-- .../PInvokeGenerator.TypeResolution.cs | 5 + .../PInvokeGenerator.cs | 2 + .../PInvokeGeneratorConfiguration.cs | 32 +++++ .../Program.Options.cs | 28 ++++ sources/ClangSharpPInvokeGenerator/Program.cs | 6 + .../PInvokeGeneratorTest.cs | 8 +- .../UnionFieldTypeNameClashTest.cs | 130 ++++++++++++++++++ 9 files changed, 311 insertions(+), 13 deletions(-) create mode 100644 tests/ClangSharp.PInvokeGenerator.UnitTests/UnionFieldTypeNameClashTest.cs diff --git a/README.md b/README.md index bc5be01f2..be44db44f 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,8 @@ Options: -p, --prefixStrip The prefix to strip from the generated method bindings. [] --nativeTypeNamesToStrip The contents to strip from the generated NativeTypeName attributes. [] -r, --remap A declaration name to be remapped to another name during binding generation. [] + -rt, --remap-type A type (record or enum) declaration name to be remapped to another name during binding generation. Takes precedence over --remap and is useful when a type and field share a name. [] + -rf, --remap-field A field declaration name to be remapped to another name during binding generation. Takes precedence over --remap and is useful when a type and field share a name. [] -std Language standard to compile for. [] -to, --test-output The output location to write the generated tests to. [] -t, --traverse A file name included either directly or indirectly by -f that should be traversed during binding generation. [] diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs index 339ea4ce2..02a53f98e 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs @@ -439,6 +439,26 @@ private string GetRemappedCursorName(NamedDecl namedDecl, out string nativeTypeN { nativeTypeName = GetCursorQualifiedName(namedDecl); + // A `--remap-type` / `--remap-field` entry takes precedence over the general `--remap` so a + // type and a field that share a name (legal in C, but not C#) can be disambiguated. These + // are only consulted here, on the decl's own name, so that resolving the *type* referenced + // by a field (which reuses the field cursor) doesn't pick up a `--remap-field` entry. + var kindSpecificRemappedNames = namedDecl switch { + FieldDecl => _config._remappedFieldNames, + TypeDecl => _config._remappedTypeNames, + _ => null, + }; + + if ((kindSpecificRemappedNames is not null) && (kindSpecificRemappedNames.Count != 0)) + { + var kindSpecificLookup = kindSpecificRemappedNames.GetAlternateLookup>(); + + if (kindSpecificLookup.TryGetValue(GetCursorName(namedDecl), out var kindSpecificRemappedName)) + { + return AddUsingDirectiveIfNeeded(_outputBuilder, kindSpecificRemappedName, skipUsing); + } + } + var name = nativeTypeName; var remappedName = GetRemappedName(name, namedDecl, tryRemapOperatorName: true, out var wasRemapped, skipUsing); @@ -503,10 +523,81 @@ private string GetRemappedCursorName(NamedDecl namedDecl, out string nativeTypeN { remappedName = GetRemappedNameForAnonymousRecord(recordDecl); } + else if ((namedDecl is RecordDecl clashingRecordDecl) && TryDeclashRecordName(clashingRecordDecl, remappedName, out var declashedName)) + { + remappedName = declashedName; + } return remappedName; } + private string ApplyTagTypeNameOverrides(TagType tagType, string leafName) + { + // Keep a `--remap-type` override or a de-clashed nested record name (see + // GetRemappedCursorName) consistent between the type declaration and any reference to it, + // so both resolve to the same C# type. + + if (_config._remappedTypeNames.Count != 0) + { + var remappedTypeNamesLookup = _config._remappedTypeNames.GetAlternateLookup>(); + + if (remappedTypeNamesLookup.TryGetValue(leafName, out var remappedTypeName)) + { + return remappedTypeName; + } + } + + if ((tagType.Decl is RecordDecl recordDecl) && TryDeclashRecordName(recordDecl, leafName, out var declashedName)) + { + return declashedName; + } + + return leafName; + } + + private bool TryDeclashRecordName(RecordDecl recordDecl, string name, out string declashedName) + { + declashedName = name; + + // A named nested record is legal in C even when a sibling field shares its name, but the + // two would map to a nested type and a field of the same name in C#, which is a CS0102 + // clash. Rename the type using the anonymous-record naming so both the declaration and any + // field type references resolve consistently through this method. + // + // The lexical parent is used (not the semantic one) since C promotes a nested record to the + // enclosing scope, yet it is still emitted nested based on where it lexically appears. + + if (recordDecl.LexicalDeclContext is not RecordDecl parentRecordDecl) + { + return false; + } + + var hasClash = false; + + foreach (var fieldDecl in parentRecordDecl.Fields) + { + if (GetRemappedCursorName(fieldDecl) == name) + { + hasClash = true; + break; + } + } + + if (!hasClash) + { + return false; + } + + declashedName = $"_{name}{AnonymousTypeKindTag}{(recordDecl.IsUnion ? "Union" : "Struct")}"; + + if (_declashedRecordNames.Add(recordDecl)) + { + AddDiagnostic(DiagnosticLevel.Warning, $"Renamed nested type '{name}' to '{declashedName}' to avoid a name clash with a field of the same name in '{GetRemappedCursorName(parentRecordDecl)}'. Use '--remap-type {name}=NewName' or '--remap-field {name}=NewName' to control the naming explicitly.", recordDecl); + } + + return true; + } + private static int GetAnonymousRecordIndex(RecordDecl recordDecl, RecordDecl parentRecordDecl) { var index = -1; @@ -648,22 +739,22 @@ private string GetRemappedName(string name, Cursor? cursor, bool tryRemapOperato wasRemapped = false; return AddUsingDirectiveIfNeeded(_outputBuilder, remappedName, skipUsingIfNotRemapped); + } - string AddUsingDirectiveIfNeeded(IOutputBuilder? outputBuilder, string remappedName, bool skipUsing) + private string AddUsingDirectiveIfNeeded(IOutputBuilder? outputBuilder, string remappedName, bool skipUsing) + { + if (!skipUsing) { - if (!skipUsing) + if (NeedsSystemSupportRegex().IsMatch(remappedName)) { - if (NeedsSystemSupportRegex().IsMatch(remappedName)) - { - outputBuilder?.EmitSystemSupport(); - } - - var namespaceName = GetNamespace(remappedName); - AddUsingDirective(outputBuilder, namespaceName); + outputBuilder?.EmitSystemSupport(); } - return remappedName; + var namespaceName = GetNamespace(remappedName); + AddUsingDirective(outputBuilder, namespaceName); } + + return remappedName; } private string GetRemappedTypeName(Cursor? cursor, Cursor? context, Type type, out string nativeTypeName, bool skipUsing = false, bool ignoreTransparentStructsWhereRequired = false, bool isTemplate = false) diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.TypeResolution.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.TypeResolution.cs index 29e48c47d..54ff9ea98 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.TypeResolution.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.TypeResolution.cs @@ -388,6 +388,7 @@ 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); + result.typeName = ApplyTagTypeNameOverrides(tagType, result.typeName); // 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 @@ -403,6 +404,10 @@ private string GetTypeName(Cursor? cursor, Cursor? context, Type rootType, Type result.typeName = qualificationBuilder.Append(result.typeName).ToString(); } + else + { + result.typeName = ApplyTagTypeNameOverrides(tagType, result.typeName); + } } else if (type is TemplateSpecializationType templateSpecializationType) { diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs index 56191fd83..da28b2e9c 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs @@ -75,6 +75,7 @@ public sealed partial class PInvokeGenerator : IDisposable private readonly Dictionary _fileNames; private readonly HashSet _topLevelClassNames; private readonly HashSet _usedRemappings; + private readonly HashSet _declashedRecordNames; private readonly string _placeholderMacroType; private string _filePath; @@ -174,6 +175,7 @@ public PInvokeGenerator(PInvokeGeneratorConfiguration config, Func>(StringComparer.Ordinal); _usedRemappings = new HashSet(StringComparer.Ordinal); + _declashedRecordNames = []; _filePath = ""; _clangCommandLineArgs = []; _placeholderMacroType = GetPlaceholderMacroType(); diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs index c3a10e029..039de7858 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs @@ -38,6 +38,8 @@ public sealed class PInvokeGeneratorConfiguration internal readonly HashSet _withSuppressGCTransitions; internal readonly Dictionary _remappedNames; + internal readonly Dictionary _remappedTypeNames; + internal readonly Dictionary _remappedFieldNames; internal readonly Dictionary _withAccessSpecifiers; internal readonly Dictionary> _withAttributes; internal readonly Dictionary _withCallConvs; @@ -93,6 +95,8 @@ public PInvokeGeneratorConfiguration(string language, string languageStandard, s _withSuppressGCTransitions = new HashSet(QualifiedNameComparer.Default); _remappedNames = new Dictionary(QualifiedNameComparer.Default); + _remappedTypeNames = new Dictionary(QualifiedNameComparer.Default); + _remappedFieldNames = new Dictionary(QualifiedNameComparer.Default); _withAccessSpecifiers = new Dictionary(QualifiedNameComparer.Default); _withAttributes = new Dictionary>(QualifiedNameComparer.Default); _withCallConvs = new Dictionary(QualifiedNameComparer.Default); @@ -363,6 +367,34 @@ public IReadOnlyDictionary RemappedNames public IReadOnlyCollection ForceRemappedNames => _forceRemappedNames; + [AllowNull] + public IReadOnlyDictionary RemappedTypeNames + { + get + { + return _remappedTypeNames; + } + + init + { + AddRange(_remappedTypeNames, value); + } + } + + [AllowNull] + public IReadOnlyDictionary RemappedFieldNames + { + get + { + return _remappedFieldNames; + } + + init + { + AddRange(_remappedFieldNames, value); + } + } + [AllowNull] public string TestOutputLocation { diff --git a/sources/ClangSharpPInvokeGenerator/Program.Options.cs b/sources/ClangSharpPInvokeGenerator/Program.Options.cs index 69073b09e..652008bdd 100644 --- a/sources/ClangSharpPInvokeGenerator/Program.Options.cs +++ b/sources/ClangSharpPInvokeGenerator/Program.Options.cs @@ -27,6 +27,8 @@ internal static partial class Program private static readonly string[] s_outputOptionAliases = ["--output", "-o"]; private static readonly string[] s_prefixStripOptionAliases = ["--prefixStrip", "-p"]; private static readonly string[] s_remapOptionAliases = ["--remap", "-r"]; + private static readonly string[] s_remapTypeOptionAliases = ["--remap-type", "-rt"]; + private static readonly string[] s_remapFieldOptionAliases = ["--remap-field", "-rf"]; private static readonly string[] s_stdOptionAliases = ["--std", "-std"]; private static readonly string[] s_testOutputOptionAliases = ["--test-output", "-to"]; private static readonly string[] s_traverseOptionAliases = ["--traverse", "-t"]; @@ -66,6 +68,8 @@ internal static partial class Program private static readonly Option s_outputLocation = GetOutputOption(); private static readonly Option s_outputMode = GetOutputModeOption(); private static readonly Option s_remappedNameValuePairs = GetRemapOption(); + private static readonly Option s_remappedTypeNameValuePairs = GetRemapTypeOption(); + private static readonly Option s_remappedFieldNameValuePairs = GetRemapFieldOption(); private static readonly Option s_std = GetStdOption(); private static readonly Option s_testOutputLocation = GetTestOutputOption(); private static readonly Option s_traversalNames = GetTraverseOption(); @@ -361,6 +365,28 @@ private static Option GetRemapOption() }; } + private static Option GetRemapTypeOption() + { + return new Option( + aliases: s_remapTypeOptionAliases, + description: "A type (record or enum) declaration name to be remapped to another name during binding generation. Takes precedence over --remap and is useful when a type and field share a name.", + getDefaultValue: Array.Empty + ) { + AllowMultipleArgumentsPerToken = true + }; + } + + private static Option GetRemapFieldOption() + { + return new Option( + aliases: s_remapFieldOptionAliases, + description: "A field declaration name to be remapped to another name during binding generation. Takes precedence over --remap and is useful when a type and field share a name.", + getDefaultValue: Array.Empty + ) { + AllowMultipleArgumentsPerToken = true + }; + } + private static RootCommand GetRootCommand() { var rootCommand = new RootCommand("ClangSharp P/Invoke Binding Generator") @@ -383,6 +409,8 @@ private static RootCommand GetRootCommand() s_methodPrefixToStrip, s_nativeTypeNamesToStrip, s_remappedNameValuePairs, + s_remappedTypeNameValuePairs, + s_remappedFieldNameValuePairs, s_std, s_testOutputLocation, s_traversalNames, diff --git a/sources/ClangSharpPInvokeGenerator/Program.cs b/sources/ClangSharpPInvokeGenerator/Program.cs index 879acf25f..83bbe47a7 100644 --- a/sources/ClangSharpPInvokeGenerator/Program.cs +++ b/sources/ClangSharpPInvokeGenerator/Program.cs @@ -73,6 +73,8 @@ public static void Run(InvocationContext context) var outputLocation = context.ParseResult.GetValueForOption(s_outputLocation) ?? ""; var outputMode = context.ParseResult.GetValueForOption(s_outputMode); var remappedNameValuePairs = context.ParseResult.GetValueForOption(s_remappedNameValuePairs) ?? []; + var remappedTypeNameValuePairs = context.ParseResult.GetValueForOption(s_remappedTypeNameValuePairs) ?? []; + var remappedFieldNameValuePairs = context.ParseResult.GetValueForOption(s_remappedFieldNameValuePairs) ?? []; var std = context.ParseResult.GetValueForOption(s_std) ?? ""; var testOutputLocation = context.ParseResult.GetValueForOption(s_testOutputLocation) ?? ""; var traversalNames = context.ParseResult.GetValueForOption(s_traversalNames) ?? []; @@ -122,6 +124,8 @@ public static void Run(InvocationContext context) } ParseKeyValuePairs(remappedNameValuePairs, errorList, out Dictionary remappedNames); + ParseKeyValuePairs(remappedTypeNameValuePairs, errorList, out Dictionary remappedTypeNames); + ParseKeyValuePairs(remappedFieldNameValuePairs, errorList, out Dictionary remappedFieldNames); ParseKeyValuePairs(withAccessSpecifierNameValuePairs, errorList, out Dictionary withAccessSpecifiers); ParseKeyValuePairs(withAttributeNameValuePairs, errorList, out Dictionary> withAttributes); ParseKeyValuePairs(withCallConvNameValuePairs, errorList, out Dictionary withCallConvs); @@ -549,6 +553,8 @@ public static void Run(InvocationContext context) MethodPrefixToStrip = methodPrefixToStrip, NativeTypeNamesToStrip = nativeTypeNamesToStrip, RemappedNames = remappedNames, + RemappedTypeNames = remappedTypeNames, + RemappedFieldNames = remappedFieldNames, TraversalNames = traversalNames, TestOutputLocation = testOutputLocation, WithAccessSpecifiers = withAccessSpecifiers, diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/PInvokeGeneratorTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/PInvokeGeneratorTest.cs index 34fb1b260..bbe1019b7 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/PInvokeGeneratorTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/PInvokeGeneratorTest.cs @@ -45,8 +45,8 @@ protected static Task ValidateGeneratedCSharpPreviewWindowsBindingsAsync(string protected static Task ValidateGeneratedCSharpPreviewUnixBindingsAsync(string inputContents, string expectedOutputContents, PInvokeGeneratorConfigurationOptions additionalConfigOptions = PInvokeGeneratorConfigurationOptions.None, string[]? excludedNames = null, IReadOnlyDictionary? remappedNames = null, IReadOnlyDictionary? withAccessSpecifiers = null, IReadOnlyDictionary>? withAttributes = null, IReadOnlyDictionary? withCallConvs = null, IReadOnlyDictionary? withClasses = null, IReadOnlyDictionary? withLibraryPaths = null, IReadOnlyDictionary? withNamespaces = null, string[]? withSetLastErrors = null, IReadOnlyDictionary? withTransparentStructs = null, IReadOnlyDictionary? withTypes = null, IReadOnlyDictionary>? withUsings = null, IReadOnlyDictionary? withPackings = null, IEnumerable? expectedDiagnostics = null, string libraryPath = DefaultLibraryPath, string[]? commandLineArgs = null, string language = "c++", string languageStandard = DefaultCppStandard) => ValidateGeneratedBindingsAsync(inputContents, expectedOutputContents, PInvokeGeneratorOutputMode.CSharp, PInvokeGeneratorConfigurationOptions.GeneratePreviewCode | PInvokeGeneratorConfigurationOptions.GenerateUnixTypes | additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard); - protected static Task ValidateGeneratedCSharpLatestWindowsBindingsAsync(string inputContents, string expectedOutputContents, PInvokeGeneratorConfigurationOptions additionalConfigOptions = PInvokeGeneratorConfigurationOptions.None, string[]? excludedNames = null, IReadOnlyDictionary? remappedNames = null, IReadOnlyDictionary? withAccessSpecifiers = null, IReadOnlyDictionary>? withAttributes = null, IReadOnlyDictionary? withCallConvs = null, IReadOnlyDictionary? withClasses = null, IReadOnlyDictionary? withLibraryPaths = null, IReadOnlyDictionary? withNamespaces = null, string[]? withSetLastErrors = null, IReadOnlyDictionary? withTransparentStructs = null, IReadOnlyDictionary? withTypes = null, IReadOnlyDictionary>? withUsings = null, IReadOnlyDictionary? withPackings = null, IEnumerable? expectedDiagnostics = null, string libraryPath = DefaultLibraryPath, string[]? commandLineArgs = null, string language = "c++", string languageStandard = DefaultCppStandard) - => ValidateGeneratedBindingsAsync(inputContents, expectedOutputContents, PInvokeGeneratorOutputMode.CSharp, PInvokeGeneratorConfigurationOptions.GenerateLatestCode | PInvokeGeneratorConfigurationOptions.None | additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard); + protected static Task ValidateGeneratedCSharpLatestWindowsBindingsAsync(string inputContents, string expectedOutputContents, PInvokeGeneratorConfigurationOptions additionalConfigOptions = PInvokeGeneratorConfigurationOptions.None, string[]? excludedNames = null, IReadOnlyDictionary? remappedNames = null, IReadOnlyDictionary? withAccessSpecifiers = null, IReadOnlyDictionary>? withAttributes = null, IReadOnlyDictionary? withCallConvs = null, IReadOnlyDictionary? withClasses = null, IReadOnlyDictionary? withLibraryPaths = null, IReadOnlyDictionary? withNamespaces = null, string[]? withSetLastErrors = null, IReadOnlyDictionary? withTransparentStructs = null, IReadOnlyDictionary? withTypes = null, IReadOnlyDictionary>? withUsings = null, IReadOnlyDictionary? withPackings = null, IEnumerable? expectedDiagnostics = null, string libraryPath = DefaultLibraryPath, string[]? commandLineArgs = null, string language = "c++", string languageStandard = DefaultCppStandard, IReadOnlyDictionary? remappedTypeNames = null, IReadOnlyDictionary? remappedFieldNames = null) + => ValidateGeneratedBindingsAsync(inputContents, expectedOutputContents, PInvokeGeneratorOutputMode.CSharp, PInvokeGeneratorConfigurationOptions.GenerateLatestCode | PInvokeGeneratorConfigurationOptions.None | additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames); protected static Task ValidateGeneratedCSharpLatestUnixBindingsAsync(string inputContents, string expectedOutputContents, PInvokeGeneratorConfigurationOptions additionalConfigOptions = PInvokeGeneratorConfigurationOptions.None, string[]? excludedNames = null, IReadOnlyDictionary? remappedNames = null, IReadOnlyDictionary? withAccessSpecifiers = null, IReadOnlyDictionary>? withAttributes = null, IReadOnlyDictionary? withCallConvs = null, IReadOnlyDictionary? withClasses = null, IReadOnlyDictionary? withLibraryPaths = null, IReadOnlyDictionary? withNamespaces = null, string[]? withSetLastErrors = null, IReadOnlyDictionary? withTransparentStructs = null, IReadOnlyDictionary? withTypes = null, IReadOnlyDictionary>? withUsings = null, IReadOnlyDictionary? withPackings = null, IEnumerable? expectedDiagnostics = null, string libraryPath = DefaultLibraryPath, string[]? commandLineArgs = null, string language = "c++", string languageStandard = DefaultCppStandard) => ValidateGeneratedBindingsAsync(inputContents, expectedOutputContents, PInvokeGeneratorOutputMode.CSharp, PInvokeGeneratorConfigurationOptions.GenerateLatestCode | PInvokeGeneratorConfigurationOptions.GenerateUnixTypes | additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard); @@ -87,7 +87,7 @@ protected static Task ValidateGeneratedXmlCompatibleWindowsBindingsAsync(string protected static Task ValidateGeneratedXmlCompatibleUnixBindingsAsync(string inputContents, string expectedOutputContents, PInvokeGeneratorConfigurationOptions additionalConfigOptions = PInvokeGeneratorConfigurationOptions.None, string[]? excludedNames = null, IReadOnlyDictionary? remappedNames = null, IReadOnlyDictionary? withAccessSpecifiers = null, IReadOnlyDictionary>? withAttributes = null, IReadOnlyDictionary? withCallConvs = null, IReadOnlyDictionary? withClasses = null, IReadOnlyDictionary? withLibraryPaths = null, IReadOnlyDictionary? withNamespaces = null, string[]? withSetLastErrors = null, IReadOnlyDictionary? withTransparentStructs = null, IReadOnlyDictionary? withTypes = null, IReadOnlyDictionary>? withUsings = null, IReadOnlyDictionary? withPackings = null, IEnumerable? expectedDiagnostics = null, string libraryPath = DefaultLibraryPath, string[]? commandLineArgs = null, string language = "c++", string languageStandard = DefaultCppStandard) => ValidateGeneratedBindingsAsync(inputContents, expectedOutputContents, PInvokeGeneratorOutputMode.Xml, PInvokeGeneratorConfigurationOptions.GenerateCompatibleCode | PInvokeGeneratorConfigurationOptions.GenerateUnixTypes | additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard); - private static async Task ValidateGeneratedBindingsAsync(string inputContents, string expectedOutputContents, PInvokeGeneratorOutputMode outputMode, PInvokeGeneratorConfigurationOptions configOptions, string[]? excludedNames, IReadOnlyDictionary? remappedNames, IReadOnlyDictionary? withAccessSpecifiers, IReadOnlyDictionary>? withAttributes, IReadOnlyDictionary? withCallConvs, IReadOnlyDictionary? withClasses, IReadOnlyDictionary? withLibraryPaths, IReadOnlyDictionary? withNamespaces, string[]? withSetLastErrors, IReadOnlyDictionary? withTransparentStructs, IReadOnlyDictionary? withTypes, IReadOnlyDictionary>? withUsings, IReadOnlyDictionary? withPackings, IEnumerable? expectedDiagnostics, string libraryPath, string[]? commandLineArgs, string language, string languageStandard) + private static async Task ValidateGeneratedBindingsAsync(string inputContents, string expectedOutputContents, PInvokeGeneratorOutputMode outputMode, PInvokeGeneratorConfigurationOptions configOptions, string[]? excludedNames, IReadOnlyDictionary? remappedNames, IReadOnlyDictionary? withAccessSpecifiers, IReadOnlyDictionary>? withAttributes, IReadOnlyDictionary? withCallConvs, IReadOnlyDictionary? withClasses, IReadOnlyDictionary? withLibraryPaths, IReadOnlyDictionary? withNamespaces, string[]? withSetLastErrors, IReadOnlyDictionary? withTransparentStructs, IReadOnlyDictionary? withTypes, IReadOnlyDictionary>? withUsings, IReadOnlyDictionary? withPackings, IEnumerable? expectedDiagnostics, string libraryPath, string[]? commandLineArgs, string language, string languageStandard, IReadOnlyDictionary? remappedTypeNames = null, IReadOnlyDictionary? remappedFieldNames = null) { Assert.That(DefaultInputFileName, Does.Exist); commandLineArgs ??= DefaultCppClangCommandLineArgs; @@ -105,6 +105,8 @@ private static async Task ValidateGeneratedBindingsAsync(string inputContents, s LibraryPath = libraryPath, MethodPrefixToStrip = null, RemappedNames = remappedNames, + RemappedTypeNames = remappedTypeNames, + RemappedFieldNames = remappedFieldNames, TraversalNames = null, TestOutputLocation = null, WithAccessSpecifiers = withAccessSpecifiers, diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/UnionFieldTypeNameClashTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/UnionFieldTypeNameClashTest.cs new file mode 100644 index 000000000..2bd11f9e9 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/UnionFieldTypeNameClashTest.cs @@ -0,0 +1,130 @@ +// 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.Collections.Generic; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace ClangSharp.UnitTests; + +/// +/// Regression test for https://github.com/dotnet/ClangSharp/issues/606. +/// A named nested record whose name matches a sibling field would emit a nested type and a field +/// of the same name in the same parent, which is a CS0102 clash in C#. The type is auto-renamed +/// (with a warning) and can be controlled explicitly via --remap-type / --remap-field. +/// +[Platform("win")] +public sealed class UnionFieldTypeNameClashTest : PInvokeGeneratorTest +{ + private const string InputContents = @"union GpuCaptureUnion +{ + struct GpuCaptureParameters + { + int a; + int b; + } GpuCaptureParameters; + int other; +}; +"; + + [Test] + public Task AutoRenamesClashingTypeAndWarns() + { + var expectedOutputContents = @"using System.Runtime.InteropServices; + +namespace ClangSharp.Test +{ + [StructLayout(LayoutKind.Explicit)] + public partial struct GpuCaptureUnion + { + [FieldOffset(0)] + [NativeTypeName(""struct GpuCaptureParameters"")] + public _GpuCaptureParameters_e__Struct GpuCaptureParameters; + + [FieldOffset(0)] + public int other; + + public partial struct _GpuCaptureParameters_e__Struct + { + public int a; + + public int b; + } + } +} +"; + + var expectedDiagnostics = new[] { + new Diagnostic(DiagnosticLevel.Warning, "Renamed nested type 'GpuCaptureParameters' to '_GpuCaptureParameters_e__Struct' to avoid a name clash with a field of the same name in 'GpuCaptureUnion'. Use '--remap-type GpuCaptureParameters=NewName' or '--remap-field GpuCaptureParameters=NewName' to control the naming explicitly.", "Line 3, Column 12 in ClangUnsavedFile.h") + }; + + return ValidateGeneratedCSharpLatestWindowsBindingsAsync(InputContents, expectedOutputContents, expectedDiagnostics: expectedDiagnostics, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard); + } + + [Test] + public Task RemapTypeControlsClashingTypeName() + { + var expectedOutputContents = @"using System.Runtime.InteropServices; + +namespace ClangSharp.Test +{ + [StructLayout(LayoutKind.Explicit)] + public partial struct GpuCaptureUnion + { + [FieldOffset(0)] + [NativeTypeName(""struct GpuCaptureParameters"")] + public GpuCaptureParametersData GpuCaptureParameters; + + [FieldOffset(0)] + public int other; + + public partial struct GpuCaptureParametersData + { + public int a; + + public int b; + } + } +} +"; + + var remappedTypeNames = new Dictionary { + ["GpuCaptureParameters"] = "GpuCaptureParametersData" + }; + + return ValidateGeneratedCSharpLatestWindowsBindingsAsync(InputContents, expectedOutputContents, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard, remappedTypeNames: remappedTypeNames); + } + + [Test] + public Task RemapFieldControlsClashingFieldName() + { + var expectedOutputContents = @"using System.Runtime.InteropServices; + +namespace ClangSharp.Test +{ + [StructLayout(LayoutKind.Explicit)] + public partial struct GpuCaptureUnion + { + [FieldOffset(0)] + [NativeTypeName(""struct GpuCaptureParameters"")] + public GpuCaptureParameters GpuCaptureParametersField; + + [FieldOffset(0)] + public int other; + + public partial struct GpuCaptureParameters + { + public int a; + + public int b; + } + } +} +"; + + var remappedFieldNames = new Dictionary { + ["GpuCaptureParameters"] = "GpuCaptureParametersField" + }; + + return ValidateGeneratedCSharpLatestWindowsBindingsAsync(InputContents, expectedOutputContents, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard, remappedFieldNames: remappedFieldNames); + } +}