diff --git a/ClangSharp.slnx b/ClangSharp.slnx index 443546c4..9b682777 100644 --- a/ClangSharp.slnx +++ b/ClangSharp.slnx @@ -113,5 +113,6 @@ + diff --git a/Directory.Packages.props b/Directory.Packages.props index 59deeab5..c9dc746d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,6 @@ - diff --git a/README.md b/README.md index be44db44..35bd4435 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ Options: -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. [] + -std, --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. [] -v, --version Prints the current version information for the tool and its native dependencies. @@ -183,6 +183,7 @@ Options: -wmi, --with-manual-import A remapped function name to be treated as a manual import during binding generation. Supports wildcards. [] -wn, --with-namespace A namespace to be used for the given remapped declaration name during binding generation. Supports wildcards. [] -wp, --with-packing Overrides the StructLayoutAttribute.Pack property for the given type. Supports wildcards. [] + -wro, --with-readonly Add the readonly modifier to a given instance method. Supports wildcards. [] -wsle, --with-setlasterror Add the SetLastError=true modifier or SetsSystemLastError attribute to a given DllImport or UnmanagedFunctionPointer. Supports wildcards. [] -wsgct, --with-suppressgctransition Add the SuppressGCTransition calling convention to a given DllImport or UnmanagedFunctionPointer. Supports wildcards. [] -wts, --with-transparent-struct A remapped type name to be treated as a transparent wrapper during binding generation. Supports wildcards. [] @@ -196,79 +197,80 @@ You can use * as catch-all rule for remapping procedures. For example if you wan The available configuration options (visible with `-c help`) are: ``` ---config, -c A configuration option that controls how the bindings are generated. Specify 'help' to see the available options. +--config, -c A configuration option that controls how the bindings are generated. Specify 'help' to see the available options. Options: - ?, h, help Show help and usage information for -c, --config + ?, h, help Show help and usage information for -c, --config # Codegen Options - compatible-codegen Bindings should be generated with .NET Standard 2.0 compatibility. Setting this disables preview code generation. - default-codegen Bindings should be generated for the previous LTS version of .NET/C#. This is currently .NET 8/C# 12. - latest-codegen Bindings should be generated for the current LTS/STS version of .NET/C#. This is currently .NET 10/C# 14. - preview-codegen Bindings should be generated for the preview version of .NET/C#. This is currently .NET 10/C# 14. + compatible-codegen Bindings should be generated with .NET Standard 2.0 compatibility. Setting this disables preview code generation. + default-codegen Bindings should be generated for the current LTS version of .NET/C#. This is currently .NET 8/C# 12. + latest-codegen Bindings should be generated for the current STS version of .NET/C#. This is currently .NET 10/C# 14. + preview-codegen Bindings should be generated for the preview version of .NET/C#. This is currently .NET 10/C# 14. # File Options - single-file Bindings should be generated to a single output file. This is the default. - multi-file Bindings should be generated so there is approximately one type per file. + single-file Bindings should be generated to a single output file. This is the default. + multi-file Bindings should be generated so there is approximately one type per file. # Type Options - unix-types Bindings should be generated assuming Unix defaults. This is the default on Unix platforms. - windows-types Bindings should be generated assuming Windows defaults. This is the default on Windows platforms. + unix-types Bindings should be generated assuming Unix defaults. This is the default on Unix platforms. + windows-types Bindings should be generated assuming Windows defaults. This is the default on Windows platforms. # Exclusion Options - exclude-anonymous-field-helpers The helper ref properties generated for fields in nested anonymous structs and unions should not be generated. - exclude-com-proxies Types recognized as COM proxies should not have bindings generated. These are currently function declarations ending with _UserFree, _UserMarshal, _UserSize, _UserUnmarshal, _Proxy, or _Stub. - exclude-default-remappings Default remappings for well known types should not be added. This currently includes intptr_t, ptrdiff_t, size_t, and uintptr_t - exclude-empty-records Bindings for records that contain no members should not be generated. These are commonly encountered for opaque handle like types such as HWND. - exclude-enum-operators Bindings for operators over enum types should not be generated. These are largely unnecessary in C# as the operators are available by default. - exclude-fnptr-codegen Generated bindings for latest or preview codegen should not use function pointers. - exclude-funcs-with-body Bindings for functions with bodies should not be generated. - exclude-using-statics-for-enums Enum usages should be fully qualified and should not include a corresponding 'using static EnumName;' + exclude-anonymous-field-helpers The helper ref properties generated for fields in nested anonymous structs and unions should not be generated. + exclude-com-proxies Types recognized as COM proxies should not have bindings generated. These are currently function declarations ending with _UserFree, _UserMarshal, _UserSize, _UserUnmarshal, _Proxy, or _Stub. + exclude-default-remappings Default remappings for well known types should not be added. This currently includes intptr_t, ptrdiff_t, size_t, and uintptr_t + exclude-empty-records Bindings for records that contain no members should not be generated. These are commonly encountered for opaque handle like types such as HWND. + exclude-enum-operators Bindings for operators over enum types should not be generated. These are largely unnecessary in C# as the operators are available by default. + exclude-fnptr-codegen Generated bindings for latest or preview codegen should not use function pointers. + exclude-funcs-with-body Bindings for functions with bodies should not be generated. + exclude-using-statics-for-enums Enum usages should be fully qualified and should not include a corresponding 'using static EnumName;' # Vtbl Options - explicit-vtbls VTBLs should have an explicit type generated with named fields per entry. - implicit-vtbls VTBLs should be implicit to reduce metadata bloat. This is the current default - trimmable-vtbls VTBLs should be defined but not used in helper methods to reduce metadata bloat when trimming. + explicit-vtbls VTBLs should have an explicit type generated with named fields per entry. + implicit-vtbls VTBLs should be implicit to reduce metadata bloat. This is the current default + trimmable-vtbls VTBLs should be defined but not used in helper methods to reduce metadata bloat when trimming. # Test Options - generate-tests-nunit Basic tests validating size, blittability, and associated metadata should be generated for NUnit. - generate-tests-xunit Basic tests validating size, blittability, and associated metadata should be generated for XUnit. + generate-tests-nunit Basic tests validating size, blittability, and associated metadata should be generated for NUnit. + generate-tests-xunit Basic tests validating size, blittability, and associated metadata should be generated for XUnit. # Generation Options - generate-aggressive-inlining [MethodImpl(MethodImplOptions.AggressiveInlining)] should be added to generated helper functions. - generate-callconv-member-function Instance function pointers should use [CallConvMemberFunction] where applicable. - generate-cpp-attributes [CppAttributeList("")] should be generated to document the encountered C++ attributes. - generate-disable-runtime-marshalling [assembly: DisableRuntimeMarshalling] should be generated. - generate-doc-includes xml documentation tags should be generated for declarations. - generate-file-scoped-namespaces Namespaces should be scoped to the file to reduce nesting. - generate-guid-member Types with an associated GUID should have a corresponding member generated. - generate-helper-types Code files should be generated for various helper attributes and declared transparent structs. - generate-macro-bindings Bindings for macro-definitions should be generated. This currently only works with value like macros and not function-like ones. - generate-marker-interfaces Bindings for marker interfaces representing native inheritance hierarchies should be generated. - generate-native-bitfield-attribute [NativeBitfield("", offset: #, length: #)] attribute should be generated to document the encountered bitfield layout. - generate-native-inheritance-attribute [NativeInheritance("")] attribute should be generated to document the encountered C++ base type. - generate-generic-pointer-wrapper Pointer should be used for limited generic type support. - generate-setslastsystemerror-attribute [SetsLastSystemError] attribute should be generated rather than using SetLastError = true. - generate-template-bindings Bindings for template-definitions should be generated. This is currently experimental. - generate-unmanaged-constants Unmanaged constants should be generated using static ref readonly properties. This is currently experimental. - generate-vtbl-index-attribute [VtblIndex(#)] attribute should be generated to document the underlying VTBL index for a helper method. + generate-aggressive-inlining [MethodImpl(MethodImplOptions.AggressiveInlining)] should be added to generated helper functions. + generate-callconv-member-function Instance function pointers should use [CallConvMemberFunction] where applicable. + generate-cpp-attributes [CppAttributeList("")] should be generated to document the encountered C++ attributes. + generate-disable-runtime-marshalling [assembly: DisableRuntimeMarshalling] should be generated. + generate-doc-includes xml documentation tags should be generated for declarations. + generate-file-scoped-namespaces Namespaces should be scoped to the file to reduce nesting. + generate-fixed-buffer-indexer-overloads Fixed sized buffer helper types should generate additional uint, nint, and nuint indexer overloads. + generate-guid-member Types with an associated GUID should have a corresponding member generated. + generate-helper-types Code files should be generated for various helper attributes and declared transparent structs. + generate-macro-bindings Bindings for macro-definitions should be generated. This currently only works with value like macros and not function-like ones. + generate-marker-interfaces Bindings for marker interfaces representing native inheritance hierarchies should be generated. + generate-native-bitfield-attribute [NativeBitfield("", offset: #, length: #)] attribute should be generated to document the encountered bitfield layout. + generate-native-inheritance-attribute [NativeInheritance("")] attribute should be generated to document the encountered C++ base type. + generate-generic-pointer-wrapper Pointer should be used for limited generic type support. + generate-setslastsystemerror-attribute [SetsLastSystemError] attribute should be generated rather than using SetLastError = true. + generate-template-bindings Bindings for template-definitions should be generated. This is currently experimental. + generate-unmanaged-constants Unmanaged constants should be generated using static ref readonly properties. This is currently experimental. + generate-vtbl-index-attribute [VtblIndex(#)] attribute should be generated to document the underlying VTBL index for a helper method. # Stripping Options - strip-enum-member-type-name Strips the enum type name from the beginning of its member names. + strip-enum-member-type-name Strips the enum type name from the beginning of its member names. # Logging Options - log-exclusions A list of excluded declaration types should be generated. This will also log if the exclusion was due to an exact or partial match. - log-potential-typedef-remappings A list of potential typedef remappings should be generated. This can help identify missing remappings. - log-visited-files A list of the visited files should be generated. This can help identify traversal issues. + log-exclusions A list of excluded declaration types should be generated. This will also log if the exclusion was due to an exact or partial match. + log-potential-typedef-remappings A list of potential typedef remappings should be generated. This can help identify missing remappings. + log-visited-files A list of the visited files should be generated. This can help identify traversal issues. ``` ### Using locally built versions diff --git a/sources/ClangSharpPInvokeGenerator/ClangSharpPInvokeGenerator.csproj b/sources/ClangSharpPInvokeGenerator/ClangSharpPInvokeGenerator.csproj index 73f6008d..c13ea9c7 100644 --- a/sources/ClangSharpPInvokeGenerator/ClangSharpPInvokeGenerator.csproj +++ b/sources/ClangSharpPInvokeGenerator/ClangSharpPInvokeGenerator.csproj @@ -8,6 +8,8 @@ true linux-arm64;linux-x64;osx-arm64;win-arm64;win-x64 net10.0 + + $(NoWarn);CA1303 @@ -24,11 +26,11 @@ - + - + diff --git a/sources/ClangSharpPInvokeGenerator/CommandLine.cs b/sources/ClangSharpPInvokeGenerator/CommandLine.cs new file mode 100644 index 00000000..41442b46 --- /dev/null +++ b/sources/ClangSharpPInvokeGenerator/CommandLine.cs @@ -0,0 +1,334 @@ +// 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; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace ClangSharp; + +internal enum CommandLineOptionKind +{ + Flag, + SingleValue, + MultipleValue, +} + +internal sealed class CommandLineOption +{ + private readonly List _values; + + public CommandLineOption(string[] aliases, string description, CommandLineOptionKind kind, string? valueName = null, string? defaultValue = null, string[]? allowedValues = null) + { + Aliases = aliases; + Description = description; + Kind = kind; + ValueName = valueName ?? aliases[0].TrimStart('-'); + DefaultValue = defaultValue; + AllowedValues = allowedValues; + _values = []; + } + + public string[] Aliases { get; } + + public string[]? AllowedValues { get; } + + // The canonical name used in diagnostics; the first alias is always the long form. + public string CanonicalName => Aliases[0]; + + public string? DefaultValue { get; } + + public string Description { get; } + + public bool IsPresent { get; set; } + + public CommandLineOptionKind Kind { get; } + + public string ValueName { get; } + + // The effective value for a single-value option: the last one specified or the default. + public string SingleValue => (_values.Count != 0) ? _values[^1] : (DefaultValue ?? ""); + + public void AddValue(string value) => _values.Add(value); + + public string[] GetValues() => [.. _values]; +} + +internal readonly struct HelpRow(string term, string description) +{ + public string Term { get; } = term; + + public string Description { get; } = description; +} + +internal sealed class CommandLineParser +{ + private const int MaxResponseFileDepth = 32; + + private readonly IReadOnlyList _options; + private readonly Dictionary _optionsByAlias; + private readonly List _errors; + + public CommandLineParser(IReadOnlyList options) + { + _options = options; + _optionsByAlias = new Dictionary(StringComparer.Ordinal); + _errors = []; + + foreach (var option in options) + { + foreach (var alias in option.Aliases) + { + _optionsByAlias.Add(alias, option); + } + } + } + + public IReadOnlyList Errors => _errors; + + public void Parse(IReadOnlyList args) + { + var tokens = new List(); + + foreach (var arg in args) + { + ExpandToken(arg, tokens, depth: 0); + } + + var index = 0; + + while (index < tokens.Count) + { + var token = tokens[index]; + + if (!TryMatchOption(token, out var option, out var inlineValue)) + { + _errors.Add($"Error: Unrecognized command or argument '{token}'."); + index++; + continue; + } + + index++; + option.IsPresent = true; + + if (option.Kind == CommandLineOptionKind.Flag) + { + if (inlineValue is not null) + { + _errors.Add($"Error: Option '{option.CanonicalName}' does not accept an argument."); + } + continue; + } + + if (inlineValue is not null) + { + AddValue(option, inlineValue); + continue; + } + + var count = 0; + + while ((index < tokens.Count) && !TryMatchOption(tokens[index], out _, out _)) + { + AddValue(option, tokens[index]); + index++; + count++; + + if (option.Kind == CommandLineOptionKind.SingleValue) + { + break; + } + } + + if (count == 0) + { + _errors.Add($"Error: Required argument missing for option: '{option.CanonicalName}'."); + } + } + } + + private void AddValue(CommandLineOption option, string value) + { + if ((option.AllowedValues is not null) && !option.AllowedValues.Contains(value, StringComparer.Ordinal)) + { + _errors.Add($"Error: Argument '{value}' not recognized for option '{option.CanonicalName}'. Must be one of: {string.Join(", ", option.AllowedValues)}."); + return; + } + + option.AddValue(value); + } + + private bool TryMatchOption(string token, out CommandLineOption option, out string? inlineValue) + { + inlineValue = null; + + if (_optionsByAlias.TryGetValue(token, out option!)) + { + return true; + } + + // Support the `--alias=value` (or `-alias=value`) inline form, but only when the text + // before the first '=' is itself a known alias. This keeps bare `name=value` tokens + // (such as a `--remap` value) intact even when the value contains '=' or spaces. + + if ((token.Length != 0) && (token[0] == '-')) + { + var equalsIndex = token.IndexOf('=', StringComparison.Ordinal); + + if (equalsIndex > 0) + { + var alias = token[..equalsIndex]; + + if (_optionsByAlias.TryGetValue(alias, out option!)) + { + inlineValue = token[(equalsIndex + 1)..]; + return true; + } + } + } + + option = null!; + return false; + } + + private void ExpandToken(string token, List result, int depth) + { + if ((token.Length == 0) || (token[0] != '@')) + { + result.Add(token); + return; + } + + var responseFilePath = token[1..]; + + if (depth > MaxResponseFileDepth) + { + _errors.Add($"Error: Response file recursion limit exceeded at '{responseFilePath}'."); + return; + } + + if (!File.Exists(responseFilePath)) + { + _errors.Add($"Error: Response file not found '{responseFilePath}'."); + return; + } + + string[] lines; + + try + { + lines = File.ReadAllLines(responseFilePath); + } + catch (IOException ex) + { + _errors.Add($"Error: {ex.Message}"); + return; + } + + // Each non-empty, non-comment line is treated as a single token; the contents are + // never split on whitespace so a value containing spaces is preserved as-is. + + foreach (var line in lines) + { + var trimmed = line.Trim(); + + if ((trimmed.Length == 0) || (trimmed[0] == '#')) + { + continue; + } + + ExpandToken(trimmed, result, depth + 1); + } + } + + public void WriteHelp(TextWriter writer, string name, string description, string epilogTitle, string epilog) + { + writer.WriteLine(name); + writer.WriteLine($" {description}"); + writer.WriteLine(); + writer.WriteLine("Usage:"); + writer.WriteLine($" {name} [options]"); + writer.WriteLine(); + writer.WriteLine("Options:"); + + var terms = new string[_options.Count]; + var maxTermLength = 0; + + for (var i = 0; i < _options.Count; i++) + { + var option = _options[i]; + var term = string.Join(", ", GetDisplayAliases(option)); + + if (option.Kind != CommandLineOptionKind.Flag) + { + term += $" <{option.ValueName}>"; + } + + terms[i] = term; + maxTermLength = Math.Max(maxTermLength, term.Length); + } + + for (var i = 0; i < _options.Count; i++) + { + var option = _options[i]; + writer.WriteLine($" {terms[i].PadRight(maxTermLength + 2)}{option.Description}{GetDefaultSuffix(option)}"); + } + + writer.WriteLine(); + writer.WriteLine(epilogTitle); + writer.WriteLine(epilog); + } + + public static void WriteOptionHelp(TextWriter writer, CommandLineOption option, IReadOnlyList rows) + { + writer.WriteLine($"{string.Join(", ", option.Aliases)}\t{option.Description}"); + writer.WriteLine(); + WriteTwoColumn(writer, rows); + } + + public static void WriteTwoColumn(TextWriter writer, IReadOnlyList rows) + { + writer.WriteLine("Options:"); + + var maxTermLength = 0; + + foreach (var row in rows) + { + if (row.Description.Length != 0) + { + maxTermLength = Math.Max(maxTermLength, row.Term.Length); + } + } + + foreach (var row in rows) + { + if (row.Description.Length == 0) + { + writer.WriteLine((row.Term.Length == 0) ? "" : $" {row.Term}"); + } + else + { + writer.WriteLine($" {row.Term.PadRight(maxTermLength + 2)}{row.Description}"); + } + } + + writer.WriteLine(); + } + + private static IEnumerable GetDisplayAliases(CommandLineOption option) + { + // Match the historical help ordering: short (`-x`) aliases first, then long (`--xx`). + return option.Aliases.Where(static alias => !alias.StartsWith("--", StringComparison.Ordinal)) + .Concat(option.Aliases.Where(static alias => alias.StartsWith("--", StringComparison.Ordinal))); + } + + private static string GetDefaultSuffix(CommandLineOption option) + { + if (option.Kind == CommandLineOptionKind.Flag) + { + return ""; + } + + return string.IsNullOrEmpty(option.DefaultValue) ? " []" : $" [default: {option.DefaultValue}]"; + } +} diff --git a/sources/ClangSharpPInvokeGenerator/CustomHelpBuilder.cs b/sources/ClangSharpPInvokeGenerator/CustomHelpBuilder.cs deleted file mode 100644 index 5c6ba64e..00000000 --- a/sources/ClangSharpPInvokeGenerator/CustomHelpBuilder.cs +++ /dev/null @@ -1,35 +0,0 @@ -// 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. - -// Portions of this code are ported from https://github.com/dotnet/command-line-api -// The original source is Copyright © .NET Foundation and Contributor. All rights reserved. Licensed under the MIT License (MIT). - -using System.CommandLine; -using System.CommandLine.Help; -using System.CommandLine.IO; - -namespace ClangSharp; - -internal sealed class CustomHelpBuilder(IConsole console, LocalizationResources localizationResources, int maxWidth = int.MaxValue) : HelpBuilder(localizationResources, maxWidth) -{ - public void Write(Option option) - { - Write(string.Join(", ", option.Aliases)); - Write("\t"); - Write(option.Description ?? ""); - WriteLine(); - } - - public void Write(string value) => console.Out.Write(value); - - public void Write(params TwoColumnHelpRow[] helpItems) - { - WriteLine("Options:"); - var _ = new Command("unused"); - WriteColumns(helpItems, new HelpContext(this, _, console.Out.CreateTextWriter())); - WriteLine(); - } - - public void WriteLine() => console.Out.WriteLine(); - - public void WriteLine(string value) => console.Out.WriteLine(value); -} diff --git a/sources/ClangSharpPInvokeGenerator/Program.Options.cs b/sources/ClangSharpPInvokeGenerator/Program.Options.cs index eafe38a4..404b7d5e 100644 --- a/sources/ClangSharpPInvokeGenerator/Program.Options.cs +++ b/sources/ClangSharpPInvokeGenerator/Program.Options.cs @@ -1,10 +1,5 @@ // 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; -using System.CommandLine; -using System.CommandLine.Help; -using System.CommandLine.Invocation; - namespace ClangSharp; internal static partial class Program @@ -49,606 +44,193 @@ internal static partial class Program private static readonly string[] s_withTransparentStructOptionAliases = ["--with-transparent-struct", "-wts"]; private static readonly string[] s_withTypeOptionAliases = ["--with-type", "-wt"]; private static readonly string[] s_withUsingOptionAliases = ["--with-using", "-wu"]; - - private static readonly Option s_additionalOption = GetAdditionalOption(); - private static readonly Option s_configOption = GetConfigOption(); - private static readonly Option s_defineMacros = GetDefineMacroOption(); - private static readonly Option s_excludedNames = GetExcludeOption(); - private static readonly Option s_files = GetFileOption(); - private static readonly Option s_fileDirectory = GetFileDirectoryOption(); - private static readonly Option s_headerFile = GetHeaderOption(); - private static readonly Option s_includedNames = GetIncludeOption(); - private static readonly Option s_includeDirectories = GetIncludeDirectoryOption(); - private static readonly Option s_language = GetLanguageOption(); - private static readonly Option s_libraryPath = GetLibraryOption(); - private static readonly Option s_methodClassName = GetMethodClassNameOption(); - private static readonly Option s_methodPrefixToStrip = GetPrefixStripOption(); - private static readonly Option s_namespaceName = GetNamespaceOption(); - private static readonly Option s_nativeTypeNamesToStrip = GetNativeTypeNamesStripOption(); - 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(); - private static readonly Option s_versionOption = GetVersionOption(); - private static readonly Option s_withAccessSpecifierNameValuePairs = GetWithAccessSpecifierOption(); - private static readonly Option s_withAttributeNameValuePairs = GetWithAttributeOption(); - private static readonly Option s_withCallConvNameValuePairs = GetWithCallConvOption(); - private static readonly Option s_withClassNameValuePairs = GetWithClassOption(); - private static readonly Option s_withGuidNameValuePairs = GetWithGuidOption(); - private static readonly Option s_withLengthNameValuePairs = GetWithLengthOption(); - private static readonly Option s_withLibraryPathNameValuePairs = GetWithLibraryPathOption(); - private static readonly Option s_withManualImports = GetWithManualImportOption(); - private static readonly Option s_withNamespaceNameValuePairs = GetWithNamespaceOption(); - private static readonly Option s_withPackingNameValuePairs = GetWithPackingOption(); - private static readonly Option s_withReadonlys = GetWithReadonlyOption(); - private static readonly Option s_withSetLastErrors = GetWithSetLastErrorOption(); - private static readonly Option s_withSuppressGCTransitions = GetWithSuppressGCTransitionOption(); - private static readonly Option s_withTransparentStructNameValuePairs = GetWithTransparentStructOption(); - private static readonly Option s_withTypeNameValuePairs = GetWithTypeOption(); - private static readonly Option s_withUsingNameValuePairs = GetWithUsingOption(); - - private static readonly RootCommand s_rootCommand = GetRootCommand(); - - private static readonly TwoColumnHelpRow[] s_configOptions = + private static readonly string[] s_helpOptionAliases = ["--help", "-?", "-h"]; + + private static readonly CommandLineOption s_additionalOption = Multi(s_additionalOptionAliases, "An argument to pass to Clang when parsing the input files."); + private static readonly CommandLineOption s_configOption = Multi(s_configOptionAliases, "A configuration option that controls how the bindings are generated. Specify 'help' to see the available options."); + private static readonly CommandLineOption s_defineMacros = Multi(s_defineMacroOptionAliases, "Define to (or 1 if omitted)."); + private static readonly CommandLineOption s_excludedNames = Multi(s_excludeOptionAliases, "A declaration name to exclude from binding generation."); + private static readonly CommandLineOption s_files = Multi(s_fileOptionAliases, "A file to parse and generate bindings for."); + private static readonly CommandLineOption s_fileDirectory = Single(s_fileDirectionOptionAliases, "The base path for files to parse."); + private static readonly CommandLineOption s_headerFile = Single(s_headerOptionAliases, "A file which contains the header to prefix every generated file with."); + private static readonly CommandLineOption s_includedNames = Multi(s_includeOptionAliases, "A declaration name to include in binding generation."); + private static readonly CommandLineOption s_includeDirectories = Multi(s_includeDirectoryOptionAliases, "Add directory to include search path."); + private static readonly CommandLineOption s_language = Single(s_languageOptionAliases, "Treat subsequent input files as having type .", defaultValue: "c++", valueName: "c|c++", allowedValues: ["c", "c++"]); + private static readonly CommandLineOption s_libraryPath = Single(s_libraryOptionAliases, "The string to use in the DllImport attribute used when generating bindings."); + private static readonly CommandLineOption s_methodClassName = Single(s_methodClassNameOptionAliases, "The name of the static class that will contain the generated method bindings.", defaultValue: "Methods"); + private static readonly CommandLineOption s_namespaceName = Single(s_namespaceOptionAliases, "The namespace in which to place the generated bindings."); + private static readonly CommandLineOption s_nativeTypeNamesToStrip = Multi(s_nativeTypeNamesStripOptionAliases, "The contents to strip from the generated NativeTypeName attributes."); + private static readonly CommandLineOption s_outputMode = Single(s_outputModeOptionAliases, "The mode describing how the information collected from the headers are presented in the resultant bindings.", defaultValue: "CSharp", valueName: "CSharp|Xml"); + private static readonly CommandLineOption s_outputLocation = Single(s_outputOptionAliases, "The output location to write the generated bindings to."); + private static readonly CommandLineOption s_methodPrefixToStrip = Single(s_prefixStripOptionAliases, "The prefix to strip from the generated method bindings."); + private static readonly CommandLineOption s_remappedNameValuePairs = Multi(s_remapOptionAliases, "A declaration name to be remapped to another name during binding generation."); + private static readonly CommandLineOption s_remappedTypeNameValuePairs = Multi(s_remapTypeOptionAliases, "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."); + private static readonly CommandLineOption s_remappedFieldNameValuePairs = Multi(s_remapFieldOptionAliases, "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."); + private static readonly CommandLineOption s_std = Single(s_stdOptionAliases, "Language standard to compile for."); + private static readonly CommandLineOption s_testOutputLocation = Single(s_testOutputOptionAliases, "The output location to write the generated tests to."); + private static readonly CommandLineOption s_traversalNames = Multi(s_traverseOptionAliases, "A file name included either directly or indirectly by -f that should be traversed during binding generation."); + private static readonly CommandLineOption s_versionOption = Flag(s_versionOptionAliases, "Prints the current version information for the tool and its native dependencies."); + private static readonly CommandLineOption s_withAccessSpecifierNameValuePairs = Multi(s_withAccessSpecifierOptionAliases, "An access specifier to be used with the given qualified or remapped declaration name during binding generation. Supports wildcards."); + 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_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."); + private static readonly CommandLineOption s_withManualImports = Multi(s_withManualImportOptionAliases, "A remapped function name to be treated as a manual import during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withNamespaceNameValuePairs = Multi(s_withNamespaceOptionAliases, "A namespace to be used for the given remapped declaration name during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withPackingNameValuePairs = Multi(s_withPackingOptionAliases, "Overrides the StructLayoutAttribute.Pack property for the given type. Supports wildcards."); + private static readonly CommandLineOption s_withReadonlys = Multi(s_withReadonlyOptionAliases, "Add the readonly modifier to a given instance method. Supports wildcards."); + private static readonly CommandLineOption s_withSetLastErrors = Multi(s_withSetLastErrorOptionAliases, "Add the SetLastError=true modifier or SetsSystemLastError attribute to a given DllImport or UnmanagedFunctionPointer. Supports wildcards."); + private static readonly CommandLineOption s_withSuppressGCTransitions = Multi(s_withSuppressGCTransitionOptionAliases, "Add the SuppressGCTransition calling convention to a given DllImport or UnmanagedFunctionPointer. Supports wildcards."); + private static readonly CommandLineOption s_withTransparentStructNameValuePairs = Multi(s_withTransparentStructOptionAliases, "A remapped type name to be treated as a transparent wrapper during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withTypeNameValuePairs = Multi(s_withTypeOptionAliases, "A type to be used for the given enum declaration during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withUsingNameValuePairs = Multi(s_withUsingOptionAliases, "A using directive to be included for the given remapped declaration name during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_helpOption = Flag(s_helpOptionAliases, "Show help and usage information"); + + private static readonly CommandLineOption[] s_options = [ - new TwoColumnHelpRow("?, h, help", "Show help and usage information for -c, --config"), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Codegen Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("compatible-codegen", "Bindings should be generated with .NET Standard 2.0 compatibility. Setting this disables preview code generation."), - new TwoColumnHelpRow("default-codegen", "Bindings should be generated for the current LTS version of .NET/C#. This is currently .NET 8/C# 12."), - new TwoColumnHelpRow("latest-codegen", "Bindings should be generated for the current STS version of .NET/C#. This is currently .NET 10/C# 14."), - new TwoColumnHelpRow("preview-codegen", "Bindings should be generated for the preview version of .NET/C#. This is currently .NET 10/C# 14."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# File Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("single-file", "Bindings should be generated to a single output file. This is the default."), - new TwoColumnHelpRow("multi-file", "Bindings should be generated so there is approximately one type per file."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Type Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("unix-types", "Bindings should be generated assuming Unix defaults. This is the default on Unix platforms."), - new TwoColumnHelpRow("windows-types", "Bindings should be generated assuming Windows defaults. This is the default on Windows platforms."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Exclusion Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("exclude-anonymous-field-helpers", "The helper ref properties generated for fields in nested anonymous structs and unions should not be generated."), - new TwoColumnHelpRow("exclude-com-proxies", "Types recognized as COM proxies should not have bindings generated. These are currently function declarations ending with _UserFree, _UserMarshal, _UserSize, _UserUnmarshal, _Proxy, or _Stub."), - new TwoColumnHelpRow("exclude-default-remappings", "Default remappings for well known types should not be added. This currently includes intptr_t, ptrdiff_t, size_t, and uintptr_t"), - new TwoColumnHelpRow("exclude-empty-records", "Bindings for records that contain no members should not be generated. These are commonly encountered for opaque handle like types such as HWND."), - new TwoColumnHelpRow("exclude-enum-operators", "Bindings for operators over enum types should not be generated. These are largely unnecessary in C# as the operators are available by default."), - new TwoColumnHelpRow("exclude-fnptr-codegen", "Generated bindings for latest or preview codegen should not use function pointers."), - new TwoColumnHelpRow("exclude-funcs-with-body", "Bindings for functions with bodies should not be generated."), - new TwoColumnHelpRow("exclude-using-statics-for-enums", "Enum usages should be fully qualified and should not include a corresponding 'using static EnumName;'"), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Vtbl Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("explicit-vtbls", "VTBLs should have an explicit type generated with named fields per entry."), - new TwoColumnHelpRow("implicit-vtbls", "VTBLs should be implicit to reduce metadata bloat. This is the current default"), - new TwoColumnHelpRow("trimmable-vtbls", "VTBLs should be defined but not used in helper methods to reduce metadata bloat when trimming."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Test Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("generate-tests-nunit", "Basic tests validating size, blittability, and associated metadata should be generated for NUnit."), - new TwoColumnHelpRow("generate-tests-xunit", "Basic tests validating size, blittability, and associated metadata should be generated for XUnit."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Generation Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("generate-aggressive-inlining", "[MethodImpl(MethodImplOptions.AggressiveInlining)] should be added to generated helper functions."), - new TwoColumnHelpRow("generate-callconv-member-function", "Instance function pointers should use [CallConvMemberFunction] where applicable."), - new TwoColumnHelpRow("generate-cpp-attributes", "[CppAttributeList(\"\")] should be generated to document the encountered C++ attributes."), - new TwoColumnHelpRow("generate-disable-runtime-marshalling", "[assembly: DisableRuntimeMarshalling] should be generated."), - new TwoColumnHelpRow("generate-doc-includes", " xml documentation tags should be generated for declarations."), - new TwoColumnHelpRow("generate-file-scoped-namespaces", "Namespaces should be scoped to the file to reduce nesting."), - new TwoColumnHelpRow("generate-fixed-buffer-indexer-overloads", "Fixed sized buffer helper types should generate additional uint, nint, and nuint indexer overloads."), - new TwoColumnHelpRow("generate-guid-member", "Types with an associated GUID should have a corresponding member generated."), - new TwoColumnHelpRow("generate-helper-types", "Code files should be generated for various helper attributes and declared transparent structs."), - new TwoColumnHelpRow("generate-macro-bindings", "Bindings for macro-definitions should be generated. This currently only works with value like macros and not function-like ones."), - new TwoColumnHelpRow("generate-marker-interfaces", "Bindings for marker interfaces representing native inheritance hierarchies should be generated."), - new TwoColumnHelpRow("generate-native-bitfield-attribute", "[NativeBitfield(\"\", offset: #, length: #)] attribute should be generated to document the encountered bitfield layout."), - new TwoColumnHelpRow("generate-native-inheritance-attribute", "[NativeInheritance(\"\")] attribute should be generated to document the encountered C++ base type."), - new TwoColumnHelpRow("generate-generic-pointer-wrapper", "Pointer should be used for limited generic type support."), - new TwoColumnHelpRow("generate-setslastsystemerror-attribute", "[SetsLastSystemError] attribute should be generated rather than using SetLastError = true."), - new TwoColumnHelpRow("generate-template-bindings", "Bindings for template-definitions should be generated. This is currently experimental."), - new TwoColumnHelpRow("generate-unmanaged-constants", "Unmanaged constants should be generated using static ref readonly properties. This is currently experimental."), - new TwoColumnHelpRow("generate-vtbl-index-attribute", "[VtblIndex(#)] attribute should be generated to document the underlying VTBL index for a helper method."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Stripping Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("strip-enum-member-type-name", "Strips the enum type name from the beginning of its member names."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Logging Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("log-exclusions", "A list of excluded declaration types should be generated. This will also log if the exclusion was due to an exact or partial match."), - new TwoColumnHelpRow("log-potential-typedef-remappings", "A list of potential typedef remappings should be generated. This can help identify missing remappings."), - new TwoColumnHelpRow("log-visited-files", "A list of the visited files should be generated. This can help identify traversal issues."), + s_additionalOption, + s_configOption, + s_defineMacros, + s_excludedNames, + s_files, + s_fileDirectory, + s_headerFile, + s_includedNames, + s_includeDirectories, + s_language, + s_libraryPath, + s_methodClassName, + s_namespaceName, + s_outputMode, + s_outputLocation, + s_methodPrefixToStrip, + s_nativeTypeNamesToStrip, + s_remappedNameValuePairs, + s_remappedTypeNameValuePairs, + s_remappedFieldNameValuePairs, + s_std, + s_testOutputLocation, + s_traversalNames, + s_versionOption, + s_withAccessSpecifierNameValuePairs, + s_withAttributeNameValuePairs, + s_withCallConvNameValuePairs, + s_withClassNameValuePairs, + s_withGuidNameValuePairs, + s_withLengthNameValuePairs, + s_withLibraryPathNameValuePairs, + s_withManualImports, + s_withNamespaceNameValuePairs, + s_withPackingNameValuePairs, + s_withReadonlys, + s_withSetLastErrors, + s_withSuppressGCTransitions, + s_withTransparentStructNameValuePairs, + s_withTypeNameValuePairs, + s_withUsingNameValuePairs, + s_helpOption, ]; - private static Option GetAdditionalOption() - { - return new Option( - aliases: s_additionalOptionAliases, - description: "An argument to pass to Clang when parsing the input files.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetConfigOption() - { - return new Option( - aliases: s_configOptionAliases, - description: "A configuration option that controls how the bindings are generated. Specify 'help' to see the available options.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetDefineMacroOption() - { - return new Option( - aliases: s_defineMacroOptionAliases, - description: "Define to (or 1 if omitted).", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetExcludeOption() - { - return new Option( - aliases: s_excludeOptionAliases, - description: "A declaration name to exclude from binding generation.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetFileOption() - { - return new Option( - aliases: s_fileOptionAliases, - description: "A file to parse and generate bindings for.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetFileDirectoryOption() - { - return new Option( - aliases: s_fileDirectionOptionAliases, - description: "The base path for files to parse.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetHeaderOption() - { - return new Option( - aliases: s_headerOptionAliases, - description: "A file which contains the header to prefix every generated file with.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetIncludeOption() - { - return new Option( - aliases: s_includeOptionAliases, - description: "A declaration name to include in binding generation.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetIncludeDirectoryOption() - { - return new Option( - aliases: s_includeDirectoryOptionAliases, - description: "Add directory to include search path.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } + private static readonly CommandLineParser s_parser = new(s_options); - private static Option GetLanguageOption() - { - return new Option( - aliases: s_languageOptionAliases, - description: "Treat subsequent input files as having type .", - getDefaultValue: () => "c++" - ).FromAmong("c", "c++"); - } - - private static Option GetLibraryOption() - { - return new Option( - aliases: s_libraryOptionAliases, - description: "The string to use in the DllImport attribute used when generating bindings.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetMethodClassNameOption() - { - return new Option( - aliases: s_methodClassNameOptionAliases, - description: "The name of the static class that will contain the generated method bindings.", - getDefaultValue: () => "Methods" - ); - } - - private static Option GetNamespaceOption() - { - return new Option( - aliases: s_namespaceOptionAliases, - description: "The namespace in which to place the generated bindings.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetNativeTypeNamesStripOption() - { - return new Option( - aliases: s_nativeTypeNamesStripOptionAliases, - description: "The contents to strip from the generated NativeTypeName attributes.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetOutputModeOption() - { - return new Option( - aliases: s_outputModeOptionAliases, - description: "The mode describing how the information collected from the headers are presented in the resultant bindings.", - getDefaultValue: () => PInvokeGeneratorOutputMode.CSharp - ); - } - - private static Option GetOutputOption() - { - return new Option( - aliases: s_outputOptionAliases, - description: "The output location to write the generated bindings to.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetPrefixStripOption() - { - return new Option( - aliases: s_prefixStripOptionAliases, - description: "The prefix to strip from the generated method bindings.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetRemapOption() - { - return new Option( - aliases: s_remapOptionAliases, - description: "A declaration name to be remapped to another name during binding generation.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - 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") - { - s_additionalOption, - s_configOption, - s_defineMacros, - s_excludedNames, - s_files, - s_fileDirectory, - s_headerFile, - s_includedNames, - s_includeDirectories, - s_language, - s_libraryPath, - s_methodClassName, - s_namespaceName, - s_outputMode, - s_outputLocation, - s_methodPrefixToStrip, - s_nativeTypeNamesToStrip, - s_remappedNameValuePairs, - s_remappedTypeNameValuePairs, - s_remappedFieldNameValuePairs, - s_std, - s_testOutputLocation, - s_traversalNames, - s_versionOption, - s_withAccessSpecifierNameValuePairs, - s_withAttributeNameValuePairs, - s_withCallConvNameValuePairs, - s_withClassNameValuePairs, - s_withGuidNameValuePairs, - s_withLengthNameValuePairs, - s_withLibraryPathNameValuePairs, - s_withManualImports, - s_withNamespaceNameValuePairs, - s_withPackingNameValuePairs, - s_withReadonlys, - s_withSetLastErrors, - s_withSuppressGCTransitions, - s_withTransparentStructNameValuePairs, - s_withTypeNameValuePairs, - s_withUsingNameValuePairs - }; - Handler.SetHandler(rootCommand, (Action)Run); - return rootCommand; - } - - private static Option GetStdOption() - { - return new Option( - aliases: s_stdOptionAliases, - description: "Language standard to compile for.", - getDefaultValue: () => "" - ); - } - - private static Option GetTestOutputOption() - { - return new Option( - aliases: s_testOutputOptionAliases, - description: "The output location to write the generated tests to.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetVersionOption() - { - return new Option( - aliases: s_versionOptionAliases, - description: "Prints the current version information for the tool and its native dependencies." - ) { - Arity = ArgumentArity.Zero - }; - } - - private static Option GetTraverseOption() - { - return new Option( - aliases: s_traverseOptionAliases, - description: "A file name included either directly or indirectly by -f that should be traversed during binding generation.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithAccessSpecifierOption() - { - return new Option( - aliases: s_withAccessSpecifierOptionAliases, - description: "An access specifier to be used with the given qualified or remapped declaration name during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithAttributeOption() - { - return new Option( - aliases: s_withAttributeOptionAliases, - description: "An attribute to be added to the given remapped declaration name during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithCallConvOption() - { - return new Option( - aliases: s_withCallConvOptionAliases, - description: "A calling convention to be used for the given declaration during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithClassOption() - { - return new Option( - aliases: s_withClassOptionAliases, - description: "A class to be used for the given remapped constant or function declaration name during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithGuidOption() - { - return new Option( - aliases: s_withGuidOptionAliases, - description: "A GUID to be used for the given declaration during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithLengthOption() - { - return new Option( - aliases: s_withLengthOptionAliases, - description: "A length to be used for the given declaration during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithLibraryPathOption() - { - return new Option( - aliases: s_withLibraryPathOptionAliases, - description: "A library path to be used for the given declaration during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithManualImportOption() - { - return new Option( - aliases: s_withManualImportOptionAliases, - description: "A remapped function name to be treated as a manual import during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithNamespaceOption() - { - return new Option( - aliases: s_withNamespaceOptionAliases, - description: "A namespace to be used for the given remapped declaration name during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithReadonlyOption() - { - return new Option( - aliases: s_withReadonlyOptionAliases, - description: "Add the readonly modifier to a given instance method. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithSetLastErrorOption() - { - return new Option( - aliases: s_withSetLastErrorOptionAliases, - description: "Add the SetLastError=true modifier or SetsSystemLastError attribute to a given DllImport or UnmanagedFunctionPointer. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithSuppressGCTransitionOption() - { - return new Option( - aliases: s_withSuppressGCTransitionOptionAliases, - description: "Add the SuppressGCTransition calling convention to a given DllImport or UnmanagedFunctionPointer. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithTransparentStructOption() - { - return new Option( - aliases: s_withTransparentStructOptionAliases, - description: "A remapped type name to be treated as a transparent wrapper during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } + private static readonly HelpRow[] s_configOptions = + [ + new HelpRow("?, h, help", "Show help and usage information for -c, --config"), + + new HelpRow("", ""), + new HelpRow("# Codegen Options", ""), + new HelpRow("", ""), + + new HelpRow("compatible-codegen", "Bindings should be generated with .NET Standard 2.0 compatibility. Setting this disables preview code generation."), + new HelpRow("default-codegen", "Bindings should be generated for the current LTS version of .NET/C#. This is currently .NET 8/C# 12."), + new HelpRow("latest-codegen", "Bindings should be generated for the current STS version of .NET/C#. This is currently .NET 10/C# 14."), + new HelpRow("preview-codegen", "Bindings should be generated for the preview version of .NET/C#. This is currently .NET 10/C# 14."), + + new HelpRow("", ""), + new HelpRow("# File Options", ""), + new HelpRow("", ""), + + new HelpRow("single-file", "Bindings should be generated to a single output file. This is the default."), + new HelpRow("multi-file", "Bindings should be generated so there is approximately one type per file."), + + new HelpRow("", ""), + new HelpRow("# Type Options", ""), + new HelpRow("", ""), + + new HelpRow("unix-types", "Bindings should be generated assuming Unix defaults. This is the default on Unix platforms."), + new HelpRow("windows-types", "Bindings should be generated assuming Windows defaults. This is the default on Windows platforms."), + + new HelpRow("", ""), + new HelpRow("# Exclusion Options", ""), + new HelpRow("", ""), + + new HelpRow("exclude-anonymous-field-helpers", "The helper ref properties generated for fields in nested anonymous structs and unions should not be generated."), + new HelpRow("exclude-com-proxies", "Types recognized as COM proxies should not have bindings generated. These are currently function declarations ending with _UserFree, _UserMarshal, _UserSize, _UserUnmarshal, _Proxy, or _Stub."), + new HelpRow("exclude-default-remappings", "Default remappings for well known types should not be added. This currently includes intptr_t, ptrdiff_t, size_t, and uintptr_t"), + new HelpRow("exclude-empty-records", "Bindings for records that contain no members should not be generated. These are commonly encountered for opaque handle like types such as HWND."), + new HelpRow("exclude-enum-operators", "Bindings for operators over enum types should not be generated. These are largely unnecessary in C# as the operators are available by default."), + new HelpRow("exclude-fnptr-codegen", "Generated bindings for latest or preview codegen should not use function pointers."), + new HelpRow("exclude-funcs-with-body", "Bindings for functions with bodies should not be generated."), + new HelpRow("exclude-using-statics-for-enums", "Enum usages should be fully qualified and should not include a corresponding 'using static EnumName;'"), + + new HelpRow("", ""), + new HelpRow("# Vtbl Options", ""), + new HelpRow("", ""), + + new HelpRow("explicit-vtbls", "VTBLs should have an explicit type generated with named fields per entry."), + new HelpRow("implicit-vtbls", "VTBLs should be implicit to reduce metadata bloat. This is the current default"), + new HelpRow("trimmable-vtbls", "VTBLs should be defined but not used in helper methods to reduce metadata bloat when trimming."), + + new HelpRow("", ""), + new HelpRow("# Test Options", ""), + new HelpRow("", ""), + + new HelpRow("generate-tests-nunit", "Basic tests validating size, blittability, and associated metadata should be generated for NUnit."), + new HelpRow("generate-tests-xunit", "Basic tests validating size, blittability, and associated metadata should be generated for XUnit."), + + new HelpRow("", ""), + new HelpRow("# Generation Options", ""), + new HelpRow("", ""), + + new HelpRow("generate-aggressive-inlining", "[MethodImpl(MethodImplOptions.AggressiveInlining)] should be added to generated helper functions."), + new HelpRow("generate-callconv-member-function", "Instance function pointers should use [CallConvMemberFunction] where applicable."), + new HelpRow("generate-cpp-attributes", "[CppAttributeList(\"\")] should be generated to document the encountered C++ attributes."), + new HelpRow("generate-disable-runtime-marshalling", "[assembly: DisableRuntimeMarshalling] should be generated."), + new HelpRow("generate-doc-includes", " xml documentation tags should be generated for declarations."), + new HelpRow("generate-file-scoped-namespaces", "Namespaces should be scoped to the file to reduce nesting."), + new HelpRow("generate-fixed-buffer-indexer-overloads", "Fixed sized buffer helper types should generate additional uint, nint, and nuint indexer overloads."), + new HelpRow("generate-guid-member", "Types with an associated GUID should have a corresponding member generated."), + new HelpRow("generate-helper-types", "Code files should be generated for various helper attributes and declared transparent structs."), + new HelpRow("generate-macro-bindings", "Bindings for macro-definitions should be generated. This currently only works with value like macros and not function-like ones."), + new HelpRow("generate-marker-interfaces", "Bindings for marker interfaces representing native inheritance hierarchies should be generated."), + new HelpRow("generate-native-bitfield-attribute", "[NativeBitfield(\"\", offset: #, length: #)] attribute should be generated to document the encountered bitfield layout."), + new HelpRow("generate-native-inheritance-attribute", "[NativeInheritance(\"\")] attribute should be generated to document the encountered C++ base type."), + new HelpRow("generate-generic-pointer-wrapper", "Pointer should be used for limited generic type support."), + new HelpRow("generate-setslastsystemerror-attribute", "[SetsLastSystemError] attribute should be generated rather than using SetLastError = true."), + new HelpRow("generate-template-bindings", "Bindings for template-definitions should be generated. This is currently experimental."), + new HelpRow("generate-unmanaged-constants", "Unmanaged constants should be generated using static ref readonly properties. This is currently experimental."), + new HelpRow("generate-vtbl-index-attribute", "[VtblIndex(#)] attribute should be generated to document the underlying VTBL index for a helper method."), + + new HelpRow("", ""), + new HelpRow("# Stripping Options", ""), + new HelpRow("", ""), + + new HelpRow("strip-enum-member-type-name", "Strips the enum type name from the beginning of its member names."), + + new HelpRow("", ""), + new HelpRow("# Logging Options", ""), + new HelpRow("", ""), + + new HelpRow("log-exclusions", "A list of excluded declaration types should be generated. This will also log if the exclusion was due to an exact or partial match."), + new HelpRow("log-potential-typedef-remappings", "A list of potential typedef remappings should be generated. This can help identify missing remappings."), + new HelpRow("log-visited-files", "A list of the visited files should be generated. This can help identify traversal issues."), + ]; - private static Option GetWithTypeOption() - { - return new Option( - aliases: s_withTypeOptionAliases, - description: "A type to be used for the given enum declaration during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } + private static CommandLineOption Multi(string[] aliases, string description) => new(aliases, description, CommandLineOptionKind.MultipleValue); - private static Option GetWithUsingOption() - { - return new Option( - aliases: s_withUsingOptionAliases, - description: "A using directive to be included for the given remapped declaration name during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } + private static CommandLineOption Single(string[] aliases, string description, string defaultValue = "", string? valueName = null, string[]? allowedValues = null) => new(aliases, description, CommandLineOptionKind.SingleValue, valueName, defaultValue, allowedValues); - private static Option GetWithPackingOption() - { - return new Option( - aliases: s_withPackingOptionAliases, - description: "Overrides the StructLayoutAttribute.Pack property for the given type. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } + private static CommandLineOption Flag(string[] aliases, string description) => new(aliases, description, CommandLineOptionKind.Flag); } diff --git a/sources/ClangSharpPInvokeGenerator/Program.cs b/sources/ClangSharpPInvokeGenerator/Program.cs index eb3e58a6..fc8bd19d 100644 --- a/sources/ClangSharpPInvokeGenerator/Program.cs +++ b/sources/ClangSharpPInvokeGenerator/Program.cs @@ -2,17 +2,11 @@ using System; using System.Collections.Generic; -using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Help; -using System.CommandLine.Invocation; -using System.CommandLine.IO; -using System.CommandLine.Parsing; using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Runtime.InteropServices; -using System.Threading.Tasks; using ClangSharp.Interop; using static ClangSharp.Interop.CXDiagnosticSeverity; using static ClangSharp.Interop.CXErrorCode; @@ -22,92 +16,91 @@ namespace ClangSharp; internal static partial class Program { - public static IEnumerable GetExtendedHelp(HelpContext context) + private const string Name = "ClangSharpPInvokeGenerator"; + private const string Description = "ClangSharp P/Invoke Binding Generator"; + + // The clang release the tool targets, tracked via the assembly version (major.minor.build) + // so it stays in sync with VersionPrefix and matches the clang/clangsharp lines below it. + private static readonly string Version = GetVersion(); + + private const string WildcardsTitle = "Wildcards:"; + private const string Wildcards = "You can use * as catch-all rule for remapping procedures. For example if you want make all of your generated code internal you can use --with-access-specifier *=Internal."; + + public static int Main(params string[] args) { - foreach (var sectionDelegate in HelpBuilder.Default.GetLayout()) + s_parser.Parse(args); + + if (s_helpOption.IsPresent) { - yield return sectionDelegate; + s_parser.WriteHelp(Console.Out, Name, Description, WildcardsTitle, Wildcards); + return 0; } - yield return _ => { - Console.WriteLine("Wildcards:"); - Console.WriteLine("You can use * as catch-all rule for remapping procedures. For example if you want make all of your generated code internal you can use --with-access-specifier *=Internal."); - }; + if (s_versionOption.IsPresent) + { + Console.WriteLine($"{Description} version {Version}"); + Console.WriteLine($" {clang.getClangVersion()}"); + Console.WriteLine($" {clangsharp.getVersion()}"); + return 0; + } + + return Run(); } - public static async Task Main(params string[] args) + private static string GetVersion() { - var parser = new CommandLineBuilder(s_rootCommand) - .UseHelp(context => context.HelpBuilder.CustomizeLayout(GetExtendedHelp)) - .UseEnvironmentVariableDirective() - .UseParseDirective() - .UseSuggestDirective() - .RegisterWithDotnetSuggest() - .UseTypoCorrections() - .UseParseErrorReporting() - .UseExceptionHandler() - .CancelOnProcessTermination() - .Build(); - return await parser.InvokeAsync(args).ConfigureAwait(false); + var version = Assembly.GetExecutingAssembly().GetName().Version; + return (version is not null) ? $"{version.Major}.{version.Minor}.{version.Build}" : ""; } - public static void Run(InvocationContext context) + public static int Run() { - ArgumentNullException.ThrowIfNull(context); - - var additionalArgs = context.ParseResult.GetValueForOption(s_additionalOption) ?? []; - var configSwitches = context.ParseResult.GetValueForOption(s_configOption) ?? []; - var defineMacros = context.ParseResult.GetValueForOption(s_defineMacros) ?? []; - var excludedNames = context.ParseResult.GetValueForOption(s_excludedNames) ?? []; - var files = context.ParseResult.GetValueForOption(s_files) ?? []; - var fileDirectory = context.ParseResult.GetValueForOption(s_fileDirectory) ?? ""; - var headerFile = context.ParseResult.GetValueForOption(s_headerFile) ?? ""; - var includedNames = context.ParseResult.GetValueForOption(s_includedNames) ?? []; - var includeDirectories = context.ParseResult.GetValueForOption(s_includeDirectories) ?? []; - var language = context.ParseResult.GetValueForOption(s_language) ?? ""; - var libraryPath = context.ParseResult.GetValueForOption(s_libraryPath) ?? ""; - var methodClassName = context.ParseResult.GetValueForOption(s_methodClassName) ?? ""; - var methodPrefixToStrip = context.ParseResult.GetValueForOption(s_methodPrefixToStrip) ?? ""; - var nativeTypeNamesToStrip = context.ParseResult.GetValueForOption(s_nativeTypeNamesToStrip) ?? []; - var namespaceName = context.ParseResult.GetValueForOption(s_namespaceName) ?? ""; - 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) ?? []; - var withAccessSpecifierNameValuePairs = context.ParseResult.GetValueForOption(s_withAccessSpecifierNameValuePairs) ?? []; - var withAttributeNameValuePairs = context.ParseResult.GetValueForOption(s_withAttributeNameValuePairs) ?? []; - var withCallConvNameValuePairs = context.ParseResult.GetValueForOption(s_withCallConvNameValuePairs) ?? []; - var withClassNameValuePairs = context.ParseResult.GetValueForOption(s_withClassNameValuePairs) ?? []; - var withGuidNameValuePairs = context.ParseResult.GetValueForOption(s_withGuidNameValuePairs) ?? []; - var withLengthNameValuePairs = context.ParseResult.GetValueForOption(s_withLengthNameValuePairs) ?? []; - var withLibraryPathNameValuePairs = context.ParseResult.GetValueForOption(s_withLibraryPathNameValuePairs) ?? []; - var withManualImports = context.ParseResult.GetValueForOption(s_withManualImports) ?? []; - var withNamespaceNameValuePairs = context.ParseResult.GetValueForOption(s_withNamespaceNameValuePairs) ?? []; - var withReadonlys = context.ParseResult.GetValueForOption(s_withReadonlys) ?? []; - var withSetLastErrors = context.ParseResult.GetValueForOption(s_withSetLastErrors) ?? []; - var withSuppressGCTransitions = context.ParseResult.GetValueForOption(s_withSuppressGCTransitions) ?? []; - var withTransparentStructNameValuePairs = context.ParseResult.GetValueForOption(s_withTransparentStructNameValuePairs) ?? []; - var withTypeNameValuePairs = context.ParseResult.GetValueForOption(s_withTypeNameValuePairs) ?? []; - var withUsingNameValuePairs = context.ParseResult.GetValueForOption(s_withUsingNameValuePairs) ?? []; - var withPackingNameValuePairs = context.ParseResult.GetValueForOption(s_withPackingNameValuePairs) ?? []; - - var versionResult = context.ParseResult.FindResultFor(s_versionOption); - - if (versionResult is not null) + var errorList = new List(s_parser.Errors); + + var additionalArgs = s_additionalOption.GetValues(); + var configSwitches = s_configOption.GetValues(); + var defineMacros = s_defineMacros.GetValues(); + var excludedNames = s_excludedNames.GetValues(); + var files = s_files.GetValues(); + var fileDirectory = s_fileDirectory.SingleValue; + var headerFile = s_headerFile.SingleValue; + var includedNames = s_includedNames.GetValues(); + var includeDirectories = s_includeDirectories.GetValues(); + var language = s_language.SingleValue; + var libraryPath = s_libraryPath.SingleValue; + var methodClassName = s_methodClassName.SingleValue; + var methodPrefixToStrip = s_methodPrefixToStrip.SingleValue; + var nativeTypeNamesToStrip = s_nativeTypeNamesToStrip.GetValues(); + var namespaceName = s_namespaceName.SingleValue; + var outputLocation = s_outputLocation.SingleValue; + var remappedNameValuePairs = s_remappedNameValuePairs.GetValues(); + var remappedTypeNameValuePairs = s_remappedTypeNameValuePairs.GetValues(); + var remappedFieldNameValuePairs = s_remappedFieldNameValuePairs.GetValues(); + var std = s_std.SingleValue; + var testOutputLocation = s_testOutputLocation.SingleValue; + var traversalNames = s_traversalNames.GetValues(); + var withAccessSpecifierNameValuePairs = s_withAccessSpecifierNameValuePairs.GetValues(); + var withAttributeNameValuePairs = s_withAttributeNameValuePairs.GetValues(); + var withCallConvNameValuePairs = s_withCallConvNameValuePairs.GetValues(); + var withClassNameValuePairs = s_withClassNameValuePairs.GetValues(); + var withGuidNameValuePairs = s_withGuidNameValuePairs.GetValues(); + var withLengthNameValuePairs = s_withLengthNameValuePairs.GetValues(); + var withLibraryPathNameValuePairs = s_withLibraryPathNameValuePairs.GetValues(); + var withManualImports = s_withManualImports.GetValues(); + var withNamespaceNameValuePairs = s_withNamespaceNameValuePairs.GetValues(); + var withReadonlys = s_withReadonlys.GetValues(); + var withSetLastErrors = s_withSetLastErrors.GetValues(); + var withSuppressGCTransitions = s_withSuppressGCTransitions.GetValues(); + var withTransparentStructNameValuePairs = s_withTransparentStructNameValuePairs.GetValues(); + var withTypeNameValuePairs = s_withTypeNameValuePairs.GetValues(); + var withUsingNameValuePairs = s_withUsingNameValuePairs.GetValues(); + var withPackingNameValuePairs = s_withPackingNameValuePairs.GetValues(); + + if (!Enum.TryParse(s_outputMode.SingleValue, ignoreCase: true, out var outputMode)) { - context.Console.WriteLine($"{s_rootCommand.Description} version 21.1.8"); - context.Console.WriteLine($" {clang.getClangVersion()}"); - context.Console.WriteLine($" {clangsharp.getVersion()}"); - context.ExitCode = 0; - return; + errorList.Add($"Error: Unrecognized output mode: {s_outputMode.SingleValue}. Must be one of CSharp or Xml"); } - var errorList = new List(); - if (files.Length == 0) { errorList.Add("Error: No input C/C++ files provided. Use --file or -f"); @@ -502,34 +495,24 @@ public static void Run(InvocationContext context) if (printConfigHelp) { - var helpBuilder = new CustomHelpBuilder(context.Console, context.LocalizationResources); - - helpBuilder.Write(s_configOption); - helpBuilder.WriteLine(); - helpBuilder.Write(s_configOptions); - - context.ExitCode = -1; - return; + CommandLineParser.WriteOptionHelp(Console.Out, s_configOption, s_configOptions); + return -1; } if (errorList.Count != 0) { - context.Console.Error.Write($"Error in args for '{files.FirstOrDefault()}'"); - context.Console.Error.Write(Environment.NewLine); + Console.Error.Write($"Error in args for '{files.FirstOrDefault()}'"); + Console.Error.Write(Environment.NewLine); foreach (var error in errorList) { - context.Console.Error.Write(error); - context.Console.Error.Write(Environment.NewLine); + Console.Error.Write(error); + Console.Error.Write(Environment.NewLine); } - context.Console.Error.Write(Environment.NewLine); - - using var textWriter = context.Console.Out.CreateTextWriter(); - var customHelpBuilder = new CustomHelpBuilder(context.Console, context.LocalizationResources); - customHelpBuilder.Write(s_rootCommand, textWriter); + Console.Error.Write(Environment.NewLine); - context.ExitCode = -1; - return; + s_parser.WriteHelp(Console.Out, Name, Description, WildcardsTitle, Wildcards); + return -1; } var clangCommandLineArgs = string.IsNullOrWhiteSpace(std) @@ -599,19 +582,19 @@ public static void Run(InvocationContext context) if (translationUnitError != CXError_Success) { - context.Console.WriteLine($"Error: Parsing failed for '{filePath}' due to '{translationUnitError}'."); + Console.WriteLine($"Error: Parsing failed for '{filePath}' due to '{translationUnitError}'."); skipProcessing = true; } else if (handle.NumDiagnostics != 0) { - context.Console.WriteLine($"Diagnostics for '{filePath}':"); + Console.WriteLine($"Diagnostics for '{filePath}':"); for (uint i = 0; i < handle.NumDiagnostics; ++i) { using var diagnostic = handle.GetDiagnostic(i); - context.Console.Write(" "); - context.Console.WriteLine(diagnostic.Format(CXDiagnostic.DefaultDisplayOptions).ToString()); + Console.Write(" "); + Console.WriteLine(diagnostic.Format(CXDiagnostic.DefaultDisplayOptions).ToString()); skipProcessing |= diagnostic.Severity == CXDiagnostic_Error; skipProcessing |= diagnostic.Severity == CXDiagnostic_Fatal; @@ -620,8 +603,8 @@ public static void Run(InvocationContext context) if (skipProcessing) { - context.Console.WriteLine($"Skipping '{filePath}' due to one or more errors listed above."); - context.Console.WriteLine(""); + Console.WriteLine($"Skipping '{filePath}' due to one or more errors listed above."); + Console.WriteLine(""); exitCode = -1; continue; @@ -634,12 +617,12 @@ public static void Run(InvocationContext context) using var translationUnit = TranslationUnit.GetOrCreate(handle); Debug.Assert(translationUnit is not null); - context.Console.WriteLine($"Processing '{filePath}'"); + Console.WriteLine($"Processing '{filePath}'"); pinvokeGenerator.GenerateBindings(translationUnit, filePath, clangCommandLineArgs, translationFlags); } catch (Exception e) { - context.Console.WriteLine(e.ToString()); + Console.WriteLine(e.ToString()); } #pragma warning restore CA1031 // Do not catch general exception types @@ -647,12 +630,12 @@ public static void Run(InvocationContext context) if (pinvokeGenerator.Diagnostics.Count != 0) { - context.Console.WriteLine($"Diagnostics for binding generation of {pinvokeGenerator.FilePath}:"); + Console.WriteLine($"Diagnostics for binding generation of {pinvokeGenerator.FilePath}:"); foreach (var diagnostic in pinvokeGenerator.Diagnostics) { - context.Console.Write(" "); - context.Console.WriteLine(diagnostic.ToString()); + Console.Write(" "); + Console.WriteLine(diagnostic.ToString()); if (diagnostic.Level == DiagnosticLevel.Warning) { @@ -676,7 +659,7 @@ public static void Run(InvocationContext context) } } - context.ExitCode = exitCode; + return exitCode; } private static void ParseKeyValuePairs(IEnumerable keyValuePairs, List errorList, out Dictionary result) diff --git a/tests/ClangSharpPInvokeGenerator.UnitTests/ClangSharpPInvokeGenerator.UnitTests.csproj b/tests/ClangSharpPInvokeGenerator.UnitTests/ClangSharpPInvokeGenerator.UnitTests.csproj new file mode 100644 index 00000000..160a63ea --- /dev/null +++ b/tests/ClangSharpPInvokeGenerator.UnitTests/ClangSharpPInvokeGenerator.UnitTests.csproj @@ -0,0 +1,22 @@ + + + + + ClangSharp.UnitTests + net10.0 + + + + + + + + + $(NoWarn);CA1515;CA1707;CA1711;CA1861;IDE0130 + + + + + + + diff --git a/tests/ClangSharpPInvokeGenerator.UnitTests/CommandLineParserTests.cs b/tests/ClangSharpPInvokeGenerator.UnitTests/CommandLineParserTests.cs new file mode 100644 index 00000000..34e8755c --- /dev/null +++ b/tests/ClangSharpPInvokeGenerator.UnitTests/CommandLineParserTests.cs @@ -0,0 +1,179 @@ +// 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.IO; +using NUnit.Framework; + +namespace ClangSharp.UnitTests; + +public sealed class CommandLineParserTests +{ + private static CommandLineOption Multi(params string[] aliases) + => new(aliases, "", CommandLineOptionKind.MultipleValue); + + private static CommandLineOption Single(params string[] aliases) + => new(aliases, "", CommandLineOptionKind.SingleValue); + + private static CommandLineOption Flag(params string[] aliases) + => new(aliases, "", CommandLineOptionKind.Flag); + + // Regression test for dotnet/clangsharp#554: a `name=value` token whose value contains + // spaces must be preserved as a single value rather than split on whitespace. + [Test] + public void SpacedValueIsPreservedAsSingleValue() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + parser.Parse(["--remap", "__arglist=@params string[] args"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "__arglist=@params string[] args" })); + } + + [Test] + public void MultiplePairsAcrossSeparateTokensAreDistinctValues() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + parser.Parse(["--remap", "A=B", "C=D"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "A=B", "C=D" })); + } + + [Test] + public void InlineEqualsFormIsSplitOnlyOnTheAliasBoundary() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + parser.Parse(["--remap=Foo=System.String bar"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "Foo=System.String bar" })); + } + + [Test] + public void BareNameValueTokenIsTreatedAsValueNotOption() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + parser.Parse(["--remap", "-Foo=Bar"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "-Foo=Bar" })); + } + + // A value that happens to start with a known short alias prefix (e.g. `-m64` after + // `--additional`, where `-m` is a distinct option) must be kept as a value. + [Test] + public void ValueStartingWithAnotherOptionPrefixIsNotMatchedAsThatOption() + { + var additional = Multi("--additional", "-a"); + var methodClass = Single("--methodClassName", "-m"); + var parser = new CommandLineParser([additional, methodClass]); + + parser.Parse(["--additional", "-m64"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(additional.GetValues(), Is.EqualTo(new[] { "-m64" })); + Assert.That(methodClass.IsPresent, Is.False); + } + + [Test] + public void SingleValueOptionConsumesExactlyOneToken() + { + var output = Single("--output", "-o"); + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([output, remap]); + + parser.Parse(["--output", "out.cs", "--remap", "A=B"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(output.SingleValue, Is.EqualTo("out.cs")); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "A=B" })); + } + + [Test] + public void FlagOptionTakesNoArgument() + { + var version = Flag("--version"); + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([version, remap]); + + parser.Parse(["--version", "--remap", "A=B"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(version.IsPresent, Is.True); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "A=B" })); + } + + [Test] + public void UnrecognizedOptionProducesAnError() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + parser.Parse(["--unknown"]); + + Assert.That(parser.Errors, Has.Count.EqualTo(1)); + Assert.That(parser.Errors[0], Does.Contain("--unknown")); + } + + [Test] + public void AllowedValuesRejectsUnexpectedValue() + { + var language = new CommandLineOption(["--language", "-x"], "", CommandLineOptionKind.SingleValue, allowedValues: ["c", "c++"]); + var parser = new CommandLineParser([language]); + + parser.Parse(["--language", "rust"]); + + Assert.That(parser.Errors, Has.Count.EqualTo(1)); + Assert.That(parser.Errors[0], Does.Contain("rust")); + } + + // A response file must yield one token per non-empty, non-comment line so that a value + // containing spaces survives intact (the core of the #554 fix). + [Test] + public void ResponseFileTreatsEachLineAsASingleToken() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + var responseFile = Path.GetTempFileName(); + + try + { + File.WriteAllLines(responseFile, [ + "# a comment", + "--remap", + "__arglist=@params string[] args", + "", + "Foo=System.String bar", + ]); + + parser.Parse([$"@{responseFile}"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "__arglist=@params string[] args", "Foo=System.String bar" })); + } + finally + { + File.Delete(responseFile); + } + } + + [Test] + public void MissingResponseFileProducesAnError() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + parser.Parse(["@does-not-exist.rsp"]); + + Assert.That(parser.Errors, Has.Count.EqualTo(1)); + Assert.That(parser.Errors[0], Does.Contain("Response file not found")); + } +}