diff --git a/README.md b/README.md index 90edc95a..a41f3d05 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ Options: -wa, --with-attribute An attribute to be added to the given remapped declaration name during binding generation. Supports wildcards. [] -wcc, --with-callconv A calling convention to be used for the given declaration during binding generation. Supports wildcards. [] -wc, --with-class A class to be used for the given remapped constant or function declaration name during binding generation. Supports wildcards. [] + -wems, --with-enum-member-strip How to strip a prefix or suffix from the members of the given remapped enum name during binding generation. Mode is one of `none`, `common-prefix`, `common-suffix`, `type-name`, `prefix:`, or `suffix:`. Supports wildcards. [] -wg, --with-guid A GUID to be used for the given declaration during binding generation. Supports wildcards. [] -wl, --with-length A length to be used for the given declaration during binding generation. Supports wildcards. [] -wlb, --with-librarypath A library path to be used for the given declaration during binding generation. Supports wildcards. [] diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitDecl.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitDecl.cs index 4dd8775e..a90a8b64 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitDecl.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitDecl.cs @@ -309,7 +309,7 @@ private void VisitEnumConstantDecl(EnumConstantDecl enumConstantDecl) parentName = _outputBuilder.Name; } - var escapedName = EscapeAndStripEnumMemberName(name, parentName); + var escapedName = EscapeAndStripEnumMemberName(name, parentName, enumConstantDecl.DeclContext as EnumDecl); var kind = isAnonymousEnum ? ValueKind.Primitive : ValueKind.Enumerator; var flags = ValueFlags.Constant; diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs index 749f0c49..4aa67f2a 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs @@ -1094,7 +1094,7 @@ private void VisitDeclRefExpr(DeclRefExpr declRefExpr) if (!IsAnonymousEnum(enumTypeName)) { - escapedName = EscapeAndStripEnumMemberName(name, enumTypeName); + escapedName = EscapeAndStripEnumMemberName(name, enumTypeName, enumConstantDecl.DeclContext as EnumDecl); } } diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs index f0cfa1c5..e9db0b61 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs @@ -83,6 +83,7 @@ public sealed partial class PInvokeGenerator : IDisposable private readonly HashSet _topLevelClassNames; private readonly HashSet _usedRemappings; private readonly HashSet _declashedRecordNames; + private readonly Dictionary _enumMemberStrips; private readonly string _placeholderMacroType; private string _filePath; @@ -199,6 +200,7 @@ public PInvokeGenerator(PInvokeGeneratorConfiguration config, Func>(StringComparer.Ordinal); _usedRemappings = new HashSet(StringComparer.Ordinal); _declashedRecordNames = []; + _enumMemberStrips = []; _filePath = ""; _clangCommandLineArgs = []; _placeholderMacroType = GetPlaceholderMacroType(); @@ -1027,8 +1029,43 @@ private string EscapeAndStripMethodName(string name) return EscapeName(name); } - private string EscapeAndStripEnumMemberName(string name, string enumTypeName) + private string EscapeAndStripEnumMemberName(string name, string enumTypeName, EnumDecl? enumDecl = null) { + if ((enumDecl is not null) && + (_config.WithEnumMemberStrip.TryGetValue(enumTypeName, out var mode) || _config.WithEnumMemberStrip.TryGetValue("*", out mode))) + { + // An explicit mode was configured for this enum (or via the `*` default) and fully determines + // how the member is stripped, taking precedence over the legacy type-name/type-prefix behavior. + switch (mode) + { + case "none": + { + return EscapeName(name); + } + + case "type-name": + { + return StripEnumMemberTypeName(name, enumTypeName); + } + + default: + { + var (kind, text) = GetEnumMemberStrip(enumDecl, mode); + + if (kind == EnumMemberStripKind.None) + { + return EscapeName(name); + } + + var strippedName = (kind == EnumMemberStripKind.Suffix) + ? StripSuffix(name, text) + : PrefixAndStrip(name, text); + + return GuardLeadingDigit(strippedName); + } + } + } + var typePrefixToStrip = _config.TypePrefixToStrip; if (Config.StripEnumMemberTypeName || (typePrefixToStrip.Length != 0)) @@ -1042,15 +1079,170 @@ private string EscapeAndStripEnumMemberName(string name, string enumTypeName) strippedName = PrefixAndStrip(strippedName, enumTypeName, trimChar: '_'); } - if (strippedName.Length > 0 && char.IsAsciiDigit(strippedName[0])) + return GuardLeadingDigit(strippedName); + } + return EscapeName(name); + } + + private static string StripEnumMemberTypeName(string name, string enumTypeName) + { + var strippedName = PrefixAndStrip(name, enumTypeName, trimChar: '_'); + return GuardLeadingDigit(strippedName); + } + + private static string GuardLeadingDigit(string name) + { + // A leading digit is not a valid C# identifier start, so guard it with an underscore. + if (name.Length > 0 && char.IsAsciiDigit(name[0])) + { + name = '_' + name; + } + return name; + } + + private static string StripSuffix(string name, string suffix) + { + var nameSpan = name.AsSpan(); + if (nameSpan.EndsWith(suffix, StringComparison.Ordinal)) + { + return nameSpan[..^suffix.Length].ToString(); + } + return name; + } + + // Resolves, and caches per enum, the prefix/suffix that should be stripped from the members of an enum + // for the `common-prefix`, `common-suffix`, `prefix:`, and `suffix:` modes. The result is + // all-or-nothing: `EnumMemberStripKind.None` is returned when no valid strip exists so the whole enum + // keeps its original member names and stays internally consistent. + private (EnumMemberStripKind Kind, string Text) GetEnumMemberStrip(EnumDecl enumDecl, string mode) + { + if (_enumMemberStrips.TryGetValue(enumDecl, out var cached)) + { + return cached; + } + + var result = ComputeEnumMemberStrip(enumDecl, mode); + _enumMemberStrips.Add(enumDecl, result); + return result; + } + + private (EnumMemberStripKind Kind, string Text) ComputeEnumMemberStrip(EnumDecl enumDecl, string mode) + { + EnumMemberStripKind kind; + string text; + + if (mode.StartsWith("prefix:", StringComparison.Ordinal)) + { + kind = EnumMemberStripKind.Prefix; + text = mode["prefix:".Length..]; + } + else if (mode.StartsWith("suffix:", StringComparison.Ordinal)) + { + kind = EnumMemberStripKind.Suffix; + text = mode["suffix:".Length..]; + } + else if (mode == "common-prefix") + { + kind = EnumMemberStripKind.Prefix; + text = ComputeEnumMemberCommonAffix(enumDecl, suffix: false); + } + else if (mode == "common-suffix") + { + kind = EnumMemberStripKind.Suffix; + text = ComputeEnumMemberCommonAffix(enumDecl, suffix: true); + } + else + { + AddDiagnostic(DiagnosticLevel.Warning, $"Unrecognized enum member strip mode '{mode}' for enum '{GetCursorName(enumDecl)}'.", enumDecl); + return (EnumMemberStripKind.None, ""); + } + + if (text.Length == 0) + { + return (EnumMemberStripKind.None, ""); + } + + // Validate all-or-nothing: every member must remain a distinct, non-empty identifier after stripping. + var strippedNames = new HashSet(StringComparer.Ordinal); + + foreach (var enumerator in enumDecl.Enumerators) + { + var name = GetRemappedCursorName(enumerator); + var strippedName = (kind == EnumMemberStripKind.Suffix) ? StripSuffix(name, text) : PrefixAndStrip(name, text); + strippedName = GuardLeadingDigit(strippedName); + + if (strippedName.Length == 0) + { + AddDiagnostic(DiagnosticLevel.Info, $"Not stripping {(kind == EnumMemberStripKind.Suffix ? "suffix" : "prefix")} '{text}' from enum '{GetCursorName(enumDecl)}' because it would leave an empty member name.", enumDecl); + return (EnumMemberStripKind.None, ""); + } + + if (!strippedNames.Add(strippedName)) { - strippedName = '_' + strippedName; + AddDiagnostic(DiagnosticLevel.Info, $"Not stripping {(kind == EnumMemberStripKind.Suffix ? "suffix" : "prefix")} '{text}' from enum '{GetCursorName(enumDecl)}' because it would cause member name collisions.", enumDecl); + return (EnumMemberStripKind.None, ""); } - return strippedName; } - return EscapeName(name); + + return (kind, text); + } + + // Auto-detects the longest common prefix (or suffix) shared by an enum's member names, trimmed back to + // the nearest `_` token boundary so a partial token is never cut. Returns "" when no valid affix exists + // (fewer than two members, no shared characters, or no `_` boundary within the shared characters). + private string ComputeEnumMemberCommonAffix(EnumDecl enumDecl, bool suffix) + { + var enumerators = enumDecl.Enumerators; + + // The longest common affix of a single name is the whole name, which is useless, so require at least two. + if (enumerators.Count < 2) + { + return ""; + } + + var commonAffix = GetRemappedCursorName(enumerators[0]).AsSpan(); + + for (var i = 1; i < enumerators.Count; i++) + { + var name = GetRemappedCursorName(enumerators[i]).AsSpan(); + var length = Math.Min(commonAffix.Length, name.Length); + var matched = 0; + + while ((matched < length) && (suffix ? (commonAffix[^(matched + 1)] == name[^(matched + 1)]) : (commonAffix[matched] == name[matched]))) + { + matched++; + } + + commonAffix = suffix ? commonAffix[^matched..] : commonAffix[..matched]; + + if (commonAffix.Length == 0) + { + return ""; + } + } + + // Trim the affix back to the nearest `_` (inclusive) so we strip whole tokens only. Without this, + // `abc_some_enum_key1`/`key2` would share the prefix `abc_some_enum_key` and strip to `1`/`2`. + if (suffix) + { + var firstSeparator = commonAffix.IndexOf('_'); + return (firstSeparator < 0) ? "" : commonAffix[firstSeparator..].ToString(); + } + else + { + var lastSeparator = commonAffix.LastIndexOf('_'); + return (lastSeparator < 0) ? "" : commonAffix[..(lastSeparator + 1)].ToString(); + } + } + + private enum EnumMemberStripKind + { + None, + Prefix, + Suffix, } + internal static string EscapeCharacter(char value) => value switch { '\0' => @"\0", '\\' => @"\\", diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs index abca8155..120d4759 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs @@ -46,6 +46,7 @@ public sealed class PInvokeGeneratorConfiguration internal readonly Dictionary> _withAttributes; internal readonly Dictionary _withCallConvs; internal readonly Dictionary _withClasses; + internal readonly Dictionary _withEnumMemberStrip; internal readonly Dictionary _withGuids; internal readonly Dictionary _withLengths; private readonly Dictionary _withLibraryPaths; @@ -104,6 +105,7 @@ public PInvokeGeneratorConfiguration(string language, string languageStandard, s _withAttributes = new Dictionary>(QualifiedNameComparer.Default); _withCallConvs = new Dictionary(QualifiedNameComparer.Default); _withClasses = new Dictionary(StringComparer.Ordinal); + _withEnumMemberStrip = new Dictionary(QualifiedNameComparer.Default); _withGuids = new Dictionary(QualifiedNameComparer.Default); _withLengths = new Dictionary(QualifiedNameComparer.Default); _withLibraryPaths = new Dictionary(StringComparer.Ordinal); @@ -577,6 +579,20 @@ public IReadOnlyDictionary WithClasses } } + [AllowNull] + public IReadOnlyDictionary WithEnumMemberStrip + { + get + { + return _withEnumMemberStrip; + } + + init + { + AddRange(_withEnumMemberStrip, value); + } + } + [AllowNull] public IReadOnlyDictionary WithGuids { diff --git a/sources/ClangSharpPInvokeGenerator/Program.Options.cs b/sources/ClangSharpPInvokeGenerator/Program.Options.cs index 93543c1f..f1b06bb6 100644 --- a/sources/ClangSharpPInvokeGenerator/Program.Options.cs +++ b/sources/ClangSharpPInvokeGenerator/Program.Options.cs @@ -35,6 +35,7 @@ internal static partial class Program private static readonly string[] s_withAttributeOptionAliases = ["--with-attribute", "-wa"]; private static readonly string[] s_withCallConvOptionAliases = ["--with-callconv", "-wcc"]; private static readonly string[] s_withClassOptionAliases = ["--with-class", "-wc"]; + private static readonly string[] s_withEnumMemberStripOptionAliases = ["--with-enum-member-strip", "-wems"]; private static readonly string[] s_withGuidOptionAliases = ["--with-guid", "-wg"]; private static readonly string[] s_withLengthOptionAliases = ["--with-length", "-wl"]; private static readonly string[] s_withLibraryPathOptionAliases = ["--with-librarypath", "-wlb"]; @@ -80,6 +81,7 @@ internal static partial class Program private static readonly CommandLineOption s_withAttributeNameValuePairs = Multi(s_withAttributeOptionAliases, "An attribute to be added to the given remapped declaration name during binding generation. Supports wildcards."); private static readonly CommandLineOption s_withCallConvNameValuePairs = Multi(s_withCallConvOptionAliases, "A calling convention to be used for the given declaration during binding generation. Supports wildcards."); private static readonly CommandLineOption s_withClassNameValuePairs = Multi(s_withClassOptionAliases, "A class to be used for the given remapped constant or function declaration name during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withEnumMemberStripNameValuePairs = Multi(s_withEnumMemberStripOptionAliases, "How to strip a prefix or suffix from the members of the given remapped enum name during binding generation. Mode is one of `none`, `common-prefix`, `common-suffix`, `type-name`, `prefix:`, or `suffix:`. Supports wildcards."); private static readonly CommandLineOption s_withGuidNameValuePairs = Multi(s_withGuidOptionAliases, "A GUID to be used for the given declaration during binding generation. Supports wildcards."); private static readonly CommandLineOption s_withLengthNameValuePairs = Multi(s_withLengthOptionAliases, "A length to be used for the given declaration during binding generation. Supports wildcards."); private static readonly CommandLineOption s_withLibraryPathNameValuePairs = Multi(s_withLibraryPathOptionAliases, "A library path to be used for the given declaration during binding generation. Supports wildcards."); @@ -127,6 +129,7 @@ internal static partial class Program s_withAttributeNameValuePairs, s_withCallConvNameValuePairs, s_withClassNameValuePairs, + s_withEnumMemberStripNameValuePairs, s_withGuidNameValuePairs, s_withLengthNameValuePairs, s_withLibraryPathNameValuePairs, diff --git a/sources/ClangSharpPInvokeGenerator/Program.cs b/sources/ClangSharpPInvokeGenerator/Program.cs index 9316e230..f6efadea 100644 --- a/sources/ClangSharpPInvokeGenerator/Program.cs +++ b/sources/ClangSharpPInvokeGenerator/Program.cs @@ -87,6 +87,7 @@ public static int Run() var withAttributeNameValuePairs = s_withAttributeNameValuePairs.GetValues(); var withCallConvNameValuePairs = s_withCallConvNameValuePairs.GetValues(); var withClassNameValuePairs = s_withClassNameValuePairs.GetValues(); + var withEnumMemberStripNameValuePairs = s_withEnumMemberStripNameValuePairs.GetValues(); var withGuidNameValuePairs = s_withGuidNameValuePairs.GetValues(); var withLengthNameValuePairs = s_withLengthNameValuePairs.GetValues(); var withLibraryPathNameValuePairs = s_withLibraryPathNameValuePairs.GetValues(); @@ -127,6 +128,7 @@ public static int Run() ParseKeyValuePairs(withAttributeNameValuePairs, errorList, out Dictionary> withAttributes); ParseKeyValuePairs(withCallConvNameValuePairs, errorList, out Dictionary withCallConvs); ParseKeyValuePairs(withClassNameValuePairs, errorList, out Dictionary withClasses); + ParseKeyValuePairs(withEnumMemberStripNameValuePairs, errorList, out Dictionary withEnumMemberStrip); ParseKeyValuePairs(withGuidNameValuePairs, errorList, out Dictionary withGuids); ParseKeyValuePairs(withLengthNameValuePairs, errorList, out Dictionary withLengths); ParseKeyValuePairs(withLibraryPathNameValuePairs, errorList, out Dictionary withLibraryPaths); @@ -598,6 +600,7 @@ public static int Run() WithAttributes = withAttributes, WithCallConvs = withCallConvs, WithClasses = withClasses, + WithEnumMemberStrip = withEnumMemberStrip, WithGuids = withGuids, WithLengths = withLengths, WithLibraryPaths = withLibraryPaths, diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/CollisionAbandonsStripping.CSharp.Latest.Windows.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/CollisionAbandonsStripping.CSharp.Latest.Windows.cs new file mode 100644 index 00000000..97c290d4 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/CollisionAbandonsStripping.CSharp.Latest.Windows.cs @@ -0,0 +1,8 @@ +namespace ClangSharp.Test +{ + public enum abc_flags + { + abc_flags_1, + abc_flags__1, + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/CommonPrefixIsStripped.CSharp.Latest.Windows.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/CommonPrefixIsStripped.CSharp.Latest.Windows.cs new file mode 100644 index 00000000..b21dbd86 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/CommonPrefixIsStripped.CSharp.Latest.Windows.cs @@ -0,0 +1,9 @@ +namespace ClangSharp.Test +{ + public enum abc_some_enum + { + first, + second, + third, + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/CommonSuffixIsStripped.CSharp.Latest.Windows.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/CommonSuffixIsStripped.CSharp.Latest.Windows.cs new file mode 100644 index 00000000..db81ba5f --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/CommonSuffixIsStripped.CSharp.Latest.Windows.cs @@ -0,0 +1,9 @@ +namespace ClangSharp.Test +{ + public enum render_modes + { + fast, + slow, + idle, + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/DigitAfterPrefixIsGuarded.CSharp.Latest.Windows.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/DigitAfterPrefixIsGuarded.CSharp.Latest.Windows.cs new file mode 100644 index 00000000..0d86a3a0 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/DigitAfterPrefixIsGuarded.CSharp.Latest.Windows.cs @@ -0,0 +1,8 @@ +namespace ClangSharp.Test +{ + public enum abc_version + { + _1_0, + _2_0, + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/ExplicitPrefixIsStripped.CSharp.Latest.Windows.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/ExplicitPrefixIsStripped.CSharp.Latest.Windows.cs new file mode 100644 index 00000000..2c8fcce9 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/ExplicitPrefixIsStripped.CSharp.Latest.Windows.cs @@ -0,0 +1,9 @@ +namespace ClangSharp.Test +{ + public enum colors + { + RED, + GREEN, + BLUE, + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/ExplicitSuffixIsStripped.CSharp.Latest.Windows.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/ExplicitSuffixIsStripped.CSharp.Latest.Windows.cs new file mode 100644 index 00000000..5c89962a --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/ExplicitSuffixIsStripped.CSharp.Latest.Windows.cs @@ -0,0 +1,9 @@ +namespace ClangSharp.Test +{ + public enum access + { + READ, + WRITE, + EXECUTE, + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/SiblingReferencesAreStripped.CSharp.Latest.Windows.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/SiblingReferencesAreStripped.CSharp.Latest.Windows.cs new file mode 100644 index 00000000..ac9399aa --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/SiblingReferencesAreStripped.CSharp.Latest.Windows.cs @@ -0,0 +1,11 @@ +namespace ClangSharp.Test +{ + public enum abc_backend + { + vulkan = 1 << 0, + gl = 1 << 1, + metal = 1 << 2, + primary = vulkan | metal, + secondary = gl, + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/SingleMemberIsNotStripped.CSharp.Latest.Windows.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/SingleMemberIsNotStripped.CSharp.Latest.Windows.cs new file mode 100644 index 00000000..6dc5adf7 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/SingleMemberIsNotStripped.CSharp.Latest.Windows.cs @@ -0,0 +1,7 @@ +namespace ClangSharp.Test +{ + public enum abc_solo + { + abc_solo_only, + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/StarDefaultWithPerEnumOverride.CSharp.Latest.Windows.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/StarDefaultWithPerEnumOverride.CSharp.Latest.Windows.cs new file mode 100644 index 00000000..66ed5d83 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/StarDefaultWithPerEnumOverride.CSharp.Latest.Windows.cs @@ -0,0 +1,14 @@ +namespace ClangSharp.Test +{ + public enum first_group + { + alpha, + beta, + } + + public enum second_group + { + second_group_gamma, + second_group_delta, + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/TypeNameModeMatchesLegacy.CSharp.Latest.Windows.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/TypeNameModeMatchesLegacy.CSharp.Latest.Windows.cs new file mode 100644 index 00000000..4e5dbb6c --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/WithEnumMemberStrip/TypeNameModeMatchesLegacy.CSharp.Latest.Windows.cs @@ -0,0 +1,8 @@ +namespace ClangSharp.Test +{ + public enum abc_some_enum + { + key1, + key2, + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StandaloneBaselineTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StandaloneBaselineTest.cs index d7d1e6e4..c663c3bf 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StandaloneBaselineTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StandaloneBaselineTest.cs @@ -18,11 +18,11 @@ public abstract class StandaloneBaselineTest : PInvokeGeneratorTest { protected abstract string Area { get; } - protected Task ValidateGeneratedCSharpLatestWindowsBaselineAsync(string inputContents, 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, string? typePrefixToStrip = null, [CallerMemberName] string testMethod = "") - => ValidateVariantAsync(new BaselineVariant(PInvokeGeneratorOutputMode.CSharp, BaselineConfig.Latest, BaselineOs.Windows), testMethod, inputContents, additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames, typePrefixToStrip); + protected Task ValidateGeneratedCSharpLatestWindowsBaselineAsync(string inputContents, 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, string? typePrefixToStrip = null, IReadOnlyDictionary? withEnumMemberStrip = null, [CallerMemberName] string testMethod = "") + => ValidateVariantAsync(new BaselineVariant(PInvokeGeneratorOutputMode.CSharp, BaselineConfig.Latest, BaselineOs.Windows), testMethod, inputContents, additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames, typePrefixToStrip, withEnumMemberStrip); - protected Task ValidateGeneratedCSharpLatestUnixBaselineAsync(string inputContents, 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, string? typePrefixToStrip = null, [CallerMemberName] string testMethod = "") - => ValidateVariantAsync(new BaselineVariant(PInvokeGeneratorOutputMode.CSharp, BaselineConfig.Latest, BaselineOs.Unix), testMethod, inputContents, additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames, typePrefixToStrip); + protected Task ValidateGeneratedCSharpLatestUnixBaselineAsync(string inputContents, 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, string? typePrefixToStrip = null, IReadOnlyDictionary? withEnumMemberStrip = null, [CallerMemberName] string testMethod = "") + => ValidateVariantAsync(new BaselineVariant(PInvokeGeneratorOutputMode.CSharp, BaselineConfig.Latest, BaselineOs.Unix), testMethod, inputContents, additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames, typePrefixToStrip, withEnumMemberStrip); protected Task ValidateGeneratedCSharpCompatibleWindowsBaselineAsync(string inputContents, 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, [CallerMemberName] string testMethod = "") => ValidateVariantAsync(new BaselineVariant(PInvokeGeneratorOutputMode.CSharp, BaselineConfig.Compatible, BaselineOs.Windows), testMethod, inputContents, additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames); @@ -41,14 +41,14 @@ protected Task ValidateGeneratedCSharpLatestHostBaselineAsync(string inputConten return ValidateVariantAsync(variant, testMethod, inputContents, additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames, useHostConfigOnly: true); } - private async Task ValidateVariantAsync(BaselineVariant variant, string caseName, string inputContents, PInvokeGeneratorConfigurationOptions additionalConfigOptions, 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, IReadOnlyDictionary? remappedFieldNames, string? typePrefixToStrip = null, bool useHostConfigOnly = false) + private async Task ValidateVariantAsync(BaselineVariant variant, string caseName, string inputContents, PInvokeGeneratorConfigurationOptions additionalConfigOptions, 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, IReadOnlyDictionary? remappedFieldNames, string? typePrefixToStrip = null, IReadOnlyDictionary? withEnumMemberStrip = null, bool useHostConfigOnly = false) { // Host-keyed cases must not add GenerateUnixTypes even when running on a Unix host; the host triple // already drives the layout difference. All other cases fold the variant's OS into the config exactly // as the legacy ValidateGenerated{Config}{OS} wrappers did. var configOptions = (useHostConfigOnly ? BaselineConfig.Latest.ToConfigOptions() : variant.ConfigOptions) | additionalConfigOptions; - var actual = await GenerateBindingsAsync(inputContents, variant.Mode, configOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames, typePrefixToStrip).ConfigureAwait(false); + var actual = await GenerateBindingsAsync(inputContents, variant.Mode, configOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames, typePrefixToStrip, withEnumMemberStrip).ConfigureAwait(false); await BaselineAssertions.AssertOrUpdateAsync(Area, caseName, variant, actual).ConfigureAwait(false); } } diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/PInvokeGeneratorTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/PInvokeGeneratorTest.cs index 2f623f95..fcfda672 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/PInvokeGeneratorTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/PInvokeGeneratorTest.cs @@ -95,7 +95,7 @@ private static async Task ValidateGeneratedBindingsAsync(string inputContents, s // Shared generator core: produces the actual generated bindings text without asserting against an // expected value, so both the inline-string harness and the checked-in baseline harness can reuse it. - internal static async Task GenerateBindingsAsync(string inputContents, 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, string? typePrefixToStrip = null) + internal static async Task GenerateBindingsAsync(string inputContents, 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, string? typePrefixToStrip = null, IReadOnlyDictionary? withEnumMemberStrip = null) { Assert.That(DefaultInputFileName, Does.Exist); commandLineArgs ??= DefaultCppClangCommandLineArgs; @@ -122,6 +122,7 @@ internal static async Task GenerateBindingsAsync(string inputContents, P WithAttributes = withAttributes, WithCallConvs = withCallConvs, WithClasses = withClasses, + WithEnumMemberStrip = withEnumMemberStrip, WithLibraryPaths = withLibraryPaths, WithManualImports = null, WithNamespaces = withNamespaces, diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/WithEnumMemberStripTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/WithEnumMemberStripTest.cs new file mode 100644 index 00000000..b4f83fe1 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/WithEnumMemberStripTest.cs @@ -0,0 +1,180 @@ +// 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 ClangSharp.UnitTests.Baseline; +using NUnit.Framework; + +namespace ClangSharp.UnitTests; + +/// +/// Tests for the --with-enum-member-strip option, which strips a prefix or suffix from an enum's +/// member names using a per-enum mode (with a * global default). See +/// https://github.com/dotnet/ClangSharp/issues/461. +/// +[Platform("win")] +public sealed class WithEnumMemberStripTest : StandaloneBaselineTest +{ + protected override string Area => "WithEnumMemberStrip"; + + // `common-prefix` auto-detects the longest common prefix (trimmed to the last `_` token boundary) and + // strips it from every member: `abc_some_enum_first` -> `first`. + [Test] + public Task CommonPrefixIsStripped() + { + var inputContents = @"enum abc_some_enum +{ + abc_some_enum_first, + abc_some_enum_second, + abc_some_enum_third, +}; +"; + + return ValidateGeneratedCSharpLatestWindowsBaselineAsync(inputContents, withEnumMemberStrip: new Dictionary { ["*"] = "common-prefix" }); + } + + // `common-suffix` mirrors `common-prefix` for the "common POSTfix" case: `fast_mode` -> `fast`. + [Test] + public Task CommonSuffixIsStripped() + { + var inputContents = @"enum render_modes +{ + fast_mode, + slow_mode, + idle_mode, +}; +"; + + return ValidateGeneratedCSharpLatestWindowsBaselineAsync(inputContents, withEnumMemberStrip: new Dictionary { ["*"] = "common-suffix" }); + } + + // A strip that would leave a leading digit keeps an underscore so the result stays a valid C# identifier: + // `abc_version_1_0` -> `_1_0`. + [Test] + public Task DigitAfterPrefixIsGuarded() + { + var inputContents = @"enum abc_version +{ + abc_version_1_0, + abc_version_2_0, +}; +"; + + return ValidateGeneratedCSharpLatestWindowsBaselineAsync(inputContents, withEnumMemberStrip: new Dictionary { ["*"] = "common-prefix" }); + } + + // The common prefix/suffix of a single member is the whole name, which is useless, so nothing is stripped. + [Test] + public Task SingleMemberIsNotStripped() + { + var inputContents = @"enum abc_solo +{ + abc_solo_only, +}; +"; + + return ValidateGeneratedCSharpLatestWindowsBaselineAsync(inputContents, withEnumMemberStrip: new Dictionary { ["*"] = "common-prefix" }); + } + + // Stripping is all-or-nothing: if it would cause two members to collide, the whole enum is left untouched + // and an informational diagnostic is emitted. + [Test] + public Task CollisionAbandonsStripping() + { + var inputContents = @"enum abc_flags +{ + abc_flags_1, + abc_flags__1, +}; +"; + + var expectedDiagnostics = new Diagnostic[] { + new Diagnostic(DiagnosticLevel.Info, "Not stripping prefix 'abc_flags_' from enum 'abc_flags' because it would cause member name collisions.", "Line 1, Column 6 in ClangUnsavedFile.h"), + }; + + return ValidateGeneratedCSharpLatestWindowsBaselineAsync(inputContents, withEnumMemberStrip: new Dictionary { ["*"] = "common-prefix" }, expectedDiagnostics: expectedDiagnostics); + } + + // `prefix:` strips an explicit prefix regardless of what the members otherwise share. + [Test] + public Task ExplicitPrefixIsStripped() + { + var inputContents = @"enum colors +{ + COLOR_RED, + COLOR_GREEN, + COLOR_BLUE, +}; +"; + + return ValidateGeneratedCSharpLatestWindowsBaselineAsync(inputContents, withEnumMemberStrip: new Dictionary { ["*"] = "prefix:COLOR_" }); + } + + // `suffix:` strips an explicit suffix. + [Test] + public Task ExplicitSuffixIsStripped() + { + var inputContents = @"enum access +{ + READ_FLAG, + WRITE_FLAG, + EXECUTE_FLAG, +}; +"; + + return ValidateGeneratedCSharpLatestWindowsBaselineAsync(inputContents, withEnumMemberStrip: new Dictionary { ["*"] = "suffix:_FLAG" }); + } + + // `type-name` matches the legacy `strip-enum-member-type-name` behavior: the enum type name is stripped + // from the beginning of each member. + [Test] + public Task TypeNameModeMatchesLegacy() + { + var inputContents = @"enum abc_some_enum +{ + abc_some_enum_key1, + abc_some_enum_key2, +}; +"; + + return ValidateGeneratedCSharpLatestWindowsBaselineAsync(inputContents, withEnumMemberStrip: new Dictionary { ["*"] = "type-name" }); + } + + // A per-enum entry overrides the `*` default: `second_group` opts out of the global `common-prefix`. + [Test] + public Task StarDefaultWithPerEnumOverride() + { + var inputContents = @"enum first_group +{ + first_group_alpha, + first_group_beta, +}; + +enum second_group +{ + second_group_gamma, + second_group_delta, +}; +"; + + return ValidateGeneratedCSharpLatestWindowsBaselineAsync(inputContents, withEnumMemberStrip: new Dictionary { ["*"] = "common-prefix", ["second_group"] = "none" }); + } + + // Sibling references inside an initializer expression are stripped identically to the declarations, so the + // generated bindings still compile: `primary = vulkan | metal`. + [Test] + public Task SiblingReferencesAreStripped() + { + var inputContents = @"enum abc_backend +{ + abc_backend_vulkan = 1 << 0, + abc_backend_gl = 1 << 1, + abc_backend_metal = 1 << 2, + abc_backend_primary = abc_backend_vulkan | abc_backend_metal, + abc_backend_secondary = abc_backend_gl, +}; +"; + + return ValidateGeneratedCSharpLatestWindowsBaselineAsync(inputContents, withEnumMemberStrip: new Dictionary { ["*"] = "common-prefix" }); + } +}