diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5b7c967..5151e3e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: 10.x - name: Run build script id: build_script - run: ./build.ps1 -ci + run: ./build.ps1 -ci ${{ matrix.os == 'windows-latest' && '-skipCoverage' || '' }} - uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows-latest' }} with: @@ -51,6 +51,7 @@ jobs: path: artifacts/ if-no-files-found: error - uses: codecov/codecov-action@v5 + if: ${{ matrix.os != 'windows-latest' }} with: name: unittests-${{ matrix.os }} fail_ci_if_error: true @@ -58,7 +59,7 @@ jobs: release: if: ${{ github.event.inputs.release }} needs: build - runs-on: ubuntu-latest + runs-on: windows-latest permissions: contents: write # for creating GitHub releases id-token: write # for NuGet trusted publishing (OIDC) diff --git a/build.ps1 b/build.ps1 index 989f4708..2343b28a 100755 --- a/build.ps1 +++ b/build.ps1 @@ -4,7 +4,9 @@ param( [ValidateSet('Debug', 'Release')] $Configuration = $null, [switch] - $ci + $ci, + [switch] + $skipCoverage ) Set-StrictMode -Version 1 @@ -27,11 +29,21 @@ if ($ci) { $formatArgs += '--check' } +[string[]] $testArgs = @('--no-build', '--configuration', $Configuration) +if (!$skipCoverage) { + $testArgs += "--collect:`"XPlat Code Coverage`"" +} + exec dotnet tool run dotnet-format -- -v detailed @formatArgs "$PSScriptRoot/CommandLineUtils.sln" exec dotnet tool run dotnet-format -- -v detailed @formatArgs "$PSScriptRoot/docs/samples/samples.sln" exec dotnet build --configuration $Configuration '-warnaserror:CS1591' exec dotnet pack --no-build --configuration $Configuration -o $artifacts exec dotnet build --configuration $Configuration "$PSScriptRoot/docs/samples/samples.sln" -exec dotnet test --no-build --configuration $Configuration --collect:"XPlat Code Coverage" + +if ($skipCoverage) { + exec dotnet test --no-build --configuration $Configuration +} else { + exec dotnet test --no-build --configuration $Configuration --collect:"XPlat Code Coverage" +} write-host -f green 'BUILD SUCCEEDED' diff --git a/src/CommandLineUtils.Generators/McMaster.Extensions.CommandLineUtils.Generators.csproj b/src/CommandLineUtils.Generators/McMaster.Extensions.CommandLineUtils.Generators.csproj index 2afadaf5..49a1a11b 100644 --- a/src/CommandLineUtils.Generators/McMaster.Extensions.CommandLineUtils.Generators.csproj +++ b/src/CommandLineUtils.Generators/McMaster.Extensions.CommandLineUtils.Generators.csproj @@ -2,7 +2,6 @@ netstandard2.0 - latest enable true true diff --git a/src/CommandLineUtils/CommandLineApplication.cs b/src/CommandLineUtils/CommandLineApplication.cs index 9a9e5bd7..a0468cf3 100644 --- a/src/CommandLineUtils/CommandLineApplication.cs +++ b/src/CommandLineUtils/CommandLineApplication.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Reflection; @@ -13,6 +12,7 @@ using System.Threading.Tasks; using McMaster.Extensions.CommandLineUtils.Abstractions; using McMaster.Extensions.CommandLineUtils.Conventions; +using McMaster.Extensions.CommandLineUtils.Extensions; using McMaster.Extensions.CommandLineUtils.HelpText; using McMaster.Extensions.CommandLineUtils.Internal; @@ -221,7 +221,7 @@ public IEnumerable Names { get { - if (!string.IsNullOrEmpty(Name)) + if (!Name.IsNullOrEmpty()) { yield return Name; } @@ -499,7 +499,7 @@ internal CommandLineApplication AddSubcommand(string name, Type modelType, Sourc private void AssertCommandNameIsUnique(string? name, CommandLineApplication? commandToIgnore) { - if (string.IsNullOrEmpty(name)) + if (name.IsNullOrEmpty()) { return; } diff --git a/src/CommandLineUtils/Conventions/OptionAttributeConvention.cs b/src/CommandLineUtils/Conventions/OptionAttributeConvention.cs index 5ac89455..e1e6b8e7 100644 --- a/src/CommandLineUtils/Conventions/OptionAttributeConvention.cs +++ b/src/CommandLineUtils/Conventions/OptionAttributeConvention.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using McMaster.Extensions.CommandLineUtils.Extensions; using McMaster.Extensions.CommandLineUtils.SourceGeneration; namespace McMaster.Extensions.CommandLineUtils.Conventions @@ -32,33 +33,33 @@ public virtual void Apply(ConventionContext context) var (template, shortName, longName) = GetOptionNames(optMeta); // Check for same-class conflicts (options in the same provider with conflicting names) - if (!string.IsNullOrEmpty(shortName) && addedShortOptions.TryGetValue(shortName, out var existingShort)) + if (!shortName.IsNullOrEmpty() && addedShortOptions.TryGetValue(shortName, out var existingShort)) { throw new InvalidOperationException( Strings.OptionNameIsAmbiguous(shortName, optMeta.PropertyName, optMeta.DeclaringType, existingShort.PropertyName, existingShort.DeclaringType)); } - if (!string.IsNullOrEmpty(longName) && addedLongOptions.TryGetValue(longName, out var existingLong)) + if (!longName.IsNullOrEmpty() && addedLongOptions.TryGetValue(longName, out var existingLong)) { throw new InvalidOperationException( Strings.OptionNameIsAmbiguous(longName, optMeta.PropertyName, optMeta.DeclaringType, existingLong.PropertyName, existingLong.DeclaringType)); } // Check if option already exists from parent command (inherited options) - if (!string.IsNullOrEmpty(shortName) && context.Application._shortOptions.ContainsKey(shortName)) + if (!shortName.IsNullOrEmpty() && context.Application._shortOptions.ContainsKey(shortName)) { continue; // Skip - option already registered by parent } - if (!string.IsNullOrEmpty(longName) && context.Application._longOptions.ContainsKey(longName)) + if (!longName.IsNullOrEmpty() && context.Application._longOptions.ContainsKey(longName)) { continue; // Skip - option already registered by parent } // Track this option - if (!string.IsNullOrEmpty(shortName)) + if (!shortName.IsNullOrEmpty()) { addedShortOptions[shortName] = optMeta; } - if (!string.IsNullOrEmpty(longName)) + if (!longName.IsNullOrEmpty()) { addedLongOptions[longName] = optMeta; } @@ -74,18 +75,18 @@ private static (string template, string? shortName, string? longName) GetOptionN string? shortName = meta.ShortName; string? longName = meta.LongName; - if (string.IsNullOrEmpty(template)) + if (template.IsNullOrEmpty()) { // Build template from ShortName/LongName - if (!string.IsNullOrEmpty(shortName) && !string.IsNullOrEmpty(longName)) + if (!shortName.IsNullOrEmpty() && !longName.IsNullOrEmpty()) { template = $"-{shortName}|--{longName}"; } - else if (!string.IsNullOrEmpty(longName)) + else if (!longName.IsNullOrEmpty()) { template = $"--{longName}"; } - else if (!string.IsNullOrEmpty(shortName)) + else if (!shortName.IsNullOrEmpty()) { template = $"-{shortName}"; } @@ -99,17 +100,17 @@ private static (string template, string? shortName, string? longName) GetOptionN else { // Parse short/long names from template if not already set - if (string.IsNullOrEmpty(shortName) || string.IsNullOrEmpty(longName)) + if (shortName.IsNullOrEmpty() || longName.IsNullOrEmpty()) { var parts = template.Split('|'); foreach (var part in parts) { var trimmed = part.Trim(); - if (trimmed.StartsWith("--") && string.IsNullOrEmpty(longName)) + if (trimmed.StartsWith("--") && longName.IsNullOrEmpty()) { longName = trimmed.Substring(2).Split(' ', '<', ':', '=')[0]; } - else if (trimmed.StartsWith("-") && string.IsNullOrEmpty(shortName)) + else if (trimmed.StartsWith("-") && shortName.IsNullOrEmpty()) { shortName = trimmed.Substring(1).Split(' ', '<', ':', '=')[0]; } @@ -175,12 +176,12 @@ private void AddOptionFromMetadata(ConventionContext context, CommandOption opti } // Register names for duplicate checking - if (!string.IsNullOrEmpty(option.ShortName)) + if (!option.ShortName.IsNullOrEmpty()) { context.Application._shortOptions.TryAdd(option.ShortName, null!); } - if (!string.IsNullOrEmpty(option.LongName)) + if (!option.LongName.IsNullOrEmpty()) { context.Application._longOptions.TryAdd(option.LongName, null!); } diff --git a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs index d9c484a5..2e383e4f 100644 --- a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs +++ b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs @@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; +using McMaster.Extensions.CommandLineUtils.Extensions; using McMaster.Extensions.CommandLineUtils.Validation; namespace McMaster.Extensions.CommandLineUtils.Conventions @@ -38,7 +39,7 @@ private protected void AddOption(ConventionContext context, CommandOption option throw new InvalidOperationException(Strings.NoValueTypesMustBeBoolean); } - if (!string.IsNullOrEmpty(option.ShortName)) + if (!option.ShortName.IsNullOrEmpty()) { if (context.Application._shortOptions.TryGetValue(option.ShortName, out var otherProp)) { @@ -53,7 +54,7 @@ private protected void AddOption(ConventionContext context, CommandOption option context.Application._shortOptions.Add(option.ShortName, prop); } - if (!string.IsNullOrEmpty(option.LongName)) + if (!option.LongName.IsNullOrEmpty()) { if (context.Application._longOptions.TryGetValue(option.LongName, out var otherProp)) { diff --git a/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs b/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs index f615fc1a..d005eb45 100644 --- a/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs +++ b/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs @@ -6,6 +6,7 @@ using System.Reflection; using McMaster.Extensions.CommandLineUtils.Abstractions; using McMaster.Extensions.CommandLineUtils.Errors; +using McMaster.Extensions.CommandLineUtils.Extensions; using McMaster.Extensions.CommandLineUtils.SourceGeneration; namespace McMaster.Extensions.CommandLineUtils.Conventions @@ -46,7 +47,7 @@ public virtual void Apply(ConventionContext context) private static string GetSubcommandName(Type subcommandType, ICommandMetadataProvider provider) { var commandInfo = provider.CommandInfo; - if (!string.IsNullOrEmpty(commandInfo?.Name)) + if (!(commandInfo?.Name).IsNullOrEmpty()) { // Use the explicit name as-is return commandInfo.Name; @@ -58,7 +59,10 @@ private static string GetSubcommandName(Type subcommandType, ICommandMetadataPro private void AddSubcommandFromMetadata( ConventionContext context, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type subcommandType, +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] +#endif + Type subcommandType, ICommandMetadataProvider provider, string name) { diff --git a/src/CommandLineUtils/Extensions/DictionaryExtensions.cs b/src/CommandLineUtils/Extensions/DictionaryExtensions.cs new file mode 100644 index 00000000..86a64785 --- /dev/null +++ b/src/CommandLineUtils/Extensions/DictionaryExtensions.cs @@ -0,0 +1,22 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace McMaster.Extensions.CommandLineUtils.Extensions +{ + internal static class DictionaryExtensions + { +#if !NET6_0_OR_GREATER + public static bool TryAdd(this IDictionary dictionary, TKey key, TValue value) + { + if (dictionary.ContainsKey(key)) + { + return false; + } + dictionary.Add(key, value); + return true; + } +#endif + } +} diff --git a/src/CommandLineUtils/Extensions/StringExtensions.cs b/src/CommandLineUtils/Extensions/StringExtensions.cs new file mode 100644 index 00000000..fb128474 --- /dev/null +++ b/src/CommandLineUtils/Extensions/StringExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics.CodeAnalysis; + +namespace McMaster.Extensions.CommandLineUtils.Extensions +{ + internal static class StringExtensions + { + /// + /// A wrapper around that allows proper nullability annotation. + /// This is a workaround because .NET Framework assemblies are not nullability annotated. + /// + public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrEmpty(value); + /// + /// A wrapper around that allows proper nullability annotation. + /// This is a workaround because .NET Framework assemblies are not nullability annotated. + /// + public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value); + } +} diff --git a/src/CommandLineUtils/HelpText/HangingIndentWriter.cs b/src/CommandLineUtils/HelpText/HangingIndentWriter.cs index 7ad7b818..79c4ad84 100644 --- a/src/CommandLineUtils/HelpText/HangingIndentWriter.cs +++ b/src/CommandLineUtils/HelpText/HangingIndentWriter.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Text; +using McMaster.Extensions.CommandLineUtils.Extensions; namespace McMaster.Extensions.CommandLineUtils.HelpText { @@ -46,7 +47,7 @@ public HangingIndentWriter(int indentSize, int? maxLineLength = null, bool inden /// Dynamically wrapped description with explicit newlines preserved. public string Write(string? input) { - if (string.IsNullOrWhiteSpace(input)) + if (input.IsNullOrWhiteSpace()) { return string.Empty; } diff --git a/src/CommandLineUtils/Internal/CommandLineProcessor.cs b/src/CommandLineUtils/Internal/CommandLineProcessor.cs index 4efa9701..3d33fbf8 100644 --- a/src/CommandLineUtils/Internal/CommandLineProcessor.cs +++ b/src/CommandLineUtils/Internal/CommandLineProcessor.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using McMaster.Extensions.CommandLineUtils.Abstractions; +using McMaster.Extensions.CommandLineUtils.Extensions; namespace McMaster.Extensions.CommandLineUtils { @@ -325,7 +326,7 @@ private bool ProcessUnexpectedArg(string argTypeName, string? argValue = null) var suggestions = Enumerable.Empty(); - if (_currentCommand.MakeSuggestionsInErrorMessage && !string.IsNullOrEmpty(value)) + if (_currentCommand.MakeSuggestionsInErrorMessage && !value.IsNullOrEmpty()) { suggestions = SuggestionCreator.GetTopSuggestions(_currentCommand, value); } diff --git a/src/CommandLineUtils/Internal/ReflectionHelper.cs b/src/CommandLineUtils/Internal/ReflectionHelper.cs index 3567560a..9f5a25f1 100644 --- a/src/CommandLineUtils/Internal/ReflectionHelper.cs +++ b/src/CommandLineUtils/Internal/ReflectionHelper.cs @@ -188,12 +188,37 @@ public bool Equals(MethodInfo? x, MethodInfo? y) return true; } - return x != null && y != null && x.HasSameMetadataDefinitionAs(y); + if (x == null || y == null) + { + return false; + } + +#if NET_6_0_OR_GREATER + return x.HasSameMetadataDefinitionAs(y); +#else + return x.MetadataToken == y.MetadataToken && x.Module.Equals(y.Module); +#endif } public int GetHashCode(MethodInfo obj) { +#if NET_6_0_OR_GREATER return obj.HasMetadataToken() ? obj.GetMetadataToken().GetHashCode() : 0; +#else + // see https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Reflection.TypeExtensions/src/System/Reflection/TypeExtensions.cs#L496 + int token = obj.MetadataToken; + + // Tokens have MSB = table index, 3 LSBs = row index + // row index of 0 is a nil token + const int rowMask = 0x00FFFFFF; + if ((token & rowMask) == 0) + { + // Nil token is returned for edge cases like typeof(byte[]).MetadataToken. + return 0; + } + + return token; +#endif } } diff --git a/src/CommandLineUtils/Internal/SuggestionCreator.cs b/src/CommandLineUtils/Internal/SuggestionCreator.cs index 95e78b48..1344035e 100644 --- a/src/CommandLineUtils/Internal/SuggestionCreator.cs +++ b/src/CommandLineUtils/Internal/SuggestionCreator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using McMaster.Extensions.CommandLineUtils.Extensions; namespace McMaster.Extensions.CommandLineUtils { @@ -45,17 +46,17 @@ private static IEnumerable GetCandidates(CommandLineApplication command) foreach (var option in command.GetOptions().Where(o => o.ShowInHelpText)) { - if (!string.IsNullOrEmpty(option.LongName)) + if (!option.LongName.IsNullOrEmpty()) { yield return option.LongName; } - if (!string.IsNullOrEmpty(option.ShortName)) + if (!option.ShortName.IsNullOrEmpty()) { yield return option.ShortName; } - if (!string.IsNullOrEmpty(option.SymbolName)) + if (!option.SymbolName.IsNullOrEmpty()) { yield return option.SymbolName; } diff --git a/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj b/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj index ab9ee63c..70f543d4 100644 --- a/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj +++ b/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj @@ -1,7 +1,7 @@ - + - net8.0 + netstandard2.0;net8.0 true true Command-line parsing API. @@ -30,27 +30,27 @@ McMaster.Extensions.CommandLineUtils.ArgumentEscaper + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - + - + - + - + \ No newline at end of file diff --git a/src/CommandLineUtils/Properties/NullabilityHelpers.cs b/src/CommandLineUtils/Properties/NullabilityHelpers.cs index 2ba73d2a..aa27683e 100644 --- a/src/CommandLineUtils/Properties/NullabilityHelpers.cs +++ b/src/CommandLineUtils/Properties/NullabilityHelpers.cs @@ -15,7 +15,7 @@ public NotNullWhenAttribute(bool returnValue) } // https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.allownullattribute - [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Parameter | System.AttributeTargets.Property, Inherited=false)] + [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Parameter | System.AttributeTargets.Property, Inherited = false)] internal sealed class AllowNullAttribute : Attribute { } } #endif diff --git a/src/CommandLineUtils/Properties/Strings.cs b/src/CommandLineUtils/Properties/Strings.cs index 34fd28d9..d1611363 100644 --- a/src/CommandLineUtils/Properties/Strings.cs +++ b/src/CommandLineUtils/Properties/Strings.cs @@ -64,7 +64,7 @@ public static string BothOptionAndHelpOptionAttributesCannotBeSpecified(Property public static string BothOptionAndVersionOptionAttributesCannotBeSpecified(PropertyInfo prop) => $"Cannot specify both {nameof(OptionAttribute)} and {nameof(VersionOptionAttribute)} on property {prop.DeclaringType?.Name}.{prop.Name}."; - internal static string UnsupportedParameterTypeOnMethod(string methodName, ParameterInfo methodParam) + internal static string UnsupportedParameterTypeOnMethod(string? methodName, ParameterInfo methodParam) => $"Unsupported type on {methodName} '{methodParam.ParameterType.FullName}' on parameter {methodParam.Name}."; public static string BothHelpOptionAndVersionOptionAttributesCannotBeSpecified(PropertyInfo prop) diff --git a/src/CommandLineUtils/SourceGeneration/ActivatorModelFactory.cs b/src/CommandLineUtils/SourceGeneration/ActivatorModelFactory.cs index 2ec3c8d9..d9953417 100644 --- a/src/CommandLineUtils/SourceGeneration/ActivatorModelFactory.cs +++ b/src/CommandLineUtils/SourceGeneration/ActivatorModelFactory.cs @@ -11,7 +11,9 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration /// /// Model factory that uses Activator.CreateInstance or DI with constructor injection. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Uses Activator.CreateInstance or DI with constructor injection")] +#endif internal sealed class ActivatorModelFactory : IModelFactory { private readonly Type _modelType; diff --git a/src/CommandLineUtils/SourceGeneration/DefaultMetadataResolver.cs b/src/CommandLineUtils/SourceGeneration/DefaultMetadataResolver.cs index 302a4a4d..a2ca3943 100644 --- a/src/CommandLineUtils/SourceGeneration/DefaultMetadataResolver.cs +++ b/src/CommandLineUtils/SourceGeneration/DefaultMetadataResolver.cs @@ -30,7 +30,9 @@ private DefaultMetadataResolver() /// For full AOT compatibility, ensure the CommandLineUtils.Generators package is referenced /// and the source generator runs during compilation. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Falls back to reflection when no generated metadata is available. Use the source generator for AOT compatibility.")] +#endif public ICommandMetadataProvider GetProvider(Type modelType) { // Check for generated metadata first (AOT-safe path) @@ -50,7 +52,9 @@ public ICommandMetadataProvider GetProvider(Type modelType) /// For full AOT compatibility, ensure the CommandLineUtils.Generators package is referenced /// and the source generator runs during compilation. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Falls back to reflection when no generated metadata is available. Use the source generator for AOT compatibility.")] +#endif public ICommandMetadataProvider GetProvider() where TModel : class { // Check for generated metadata first (AOT-safe path) @@ -78,7 +82,9 @@ public bool HasGeneratedMetadata(Type modelType) return CommandMetadataRegistry.HasMetadata(modelType); } +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Uses reflection to analyze the model type")] +#endif private static ICommandMetadataProvider CreateReflectionProvider(Type modelType) { // This creates a reflection-based implementation of ICommandMetadataProvider diff --git a/src/CommandLineUtils/SourceGeneration/ReflectionExecuteHandler.cs b/src/CommandLineUtils/SourceGeneration/ReflectionExecuteHandler.cs index 7ed1237c..8e7ae1fd 100644 --- a/src/CommandLineUtils/SourceGeneration/ReflectionExecuteHandler.cs +++ b/src/CommandLineUtils/SourceGeneration/ReflectionExecuteHandler.cs @@ -12,7 +12,9 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration /// /// Execute handler that uses reflection to invoke OnExecute/OnExecuteAsync. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Uses reflection to invoke method")] +#endif internal sealed class ReflectionExecuteHandler : IExecuteHandler { private readonly MethodInfo _method; diff --git a/src/CommandLineUtils/SourceGeneration/ReflectionMetadataProvider.cs b/src/CommandLineUtils/SourceGeneration/ReflectionMetadataProvider.cs index e5b26f7d..7dfbc8ff 100644 --- a/src/CommandLineUtils/SourceGeneration/ReflectionMetadataProvider.cs +++ b/src/CommandLineUtils/SourceGeneration/ReflectionMetadataProvider.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; +using McMaster.Extensions.CommandLineUtils.Extensions; namespace McMaster.Extensions.CommandLineUtils.SourceGeneration { @@ -15,7 +16,9 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration /// Provides command metadata by analyzing a type using reflection. /// This is the fallback when generated metadata is not available. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Uses reflection to analyze the model type")] +#endif internal sealed class ReflectionMetadataProvider : ICommandMetadataProvider { private const BindingFlags MethodLookup = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public; @@ -442,7 +445,7 @@ private IReadOnlyList ExtractSubcommands() { Func? versionGetter = null; - if (!string.IsNullOrEmpty(fromMemberAttr.MemberName)) + if (!fromMemberAttr.MemberName.IsNullOrEmpty()) { var members = ReflectionHelper.GetPropertyOrMethod(_modelType, fromMemberAttr.MemberName); if (members.Length > 0) diff --git a/src/CommandLineUtils/SourceGeneration/ReflectionValidateHandler.cs b/src/CommandLineUtils/SourceGeneration/ReflectionValidateHandler.cs index bb473e9f..359711d0 100644 --- a/src/CommandLineUtils/SourceGeneration/ReflectionValidateHandler.cs +++ b/src/CommandLineUtils/SourceGeneration/ReflectionValidateHandler.cs @@ -12,7 +12,9 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration /// /// Validate handler that uses reflection to invoke OnValidate. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Uses reflection to invoke method")] +#endif internal sealed class ReflectionValidateHandler : IValidateHandler { private readonly MethodInfo _method; diff --git a/src/CommandLineUtils/SourceGeneration/ReflectionValidationErrorHandler.cs b/src/CommandLineUtils/SourceGeneration/ReflectionValidationErrorHandler.cs index 0619433f..2791d181 100644 --- a/src/CommandLineUtils/SourceGeneration/ReflectionValidationErrorHandler.cs +++ b/src/CommandLineUtils/SourceGeneration/ReflectionValidationErrorHandler.cs @@ -11,7 +11,9 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration /// /// Validation error handler that uses reflection to invoke OnValidationError. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Uses reflection to invoke method")] +#endif internal sealed class ReflectionValidationErrorHandler : IValidationErrorHandler { private readonly MethodInfo _method; diff --git a/src/CommandLineUtils/Utilities/DotNetExe.cs b/src/CommandLineUtils/Utilities/DotNetExe.cs index e7424021..72395afb 100644 --- a/src/CommandLineUtils/Utilities/DotNetExe.cs +++ b/src/CommandLineUtils/Utilities/DotNetExe.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; +using McMaster.Extensions.CommandLineUtils.Extensions; namespace McMaster.Extensions.CommandLineUtils { @@ -49,7 +50,7 @@ public static string FullPathOrDefault() } var mainModule = Process.GetCurrentProcess().MainModule; - if (!string.IsNullOrEmpty(mainModule?.FileName) + if (!(mainModule?.FileName).IsNullOrEmpty() && Path.GetFileName(mainModule.FileName).Equals(fileName, StringComparison.OrdinalIgnoreCase)) { return mainModule.FileName; diff --git a/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj b/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj index 14193d87..6b503df0 100644 --- a/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj +++ b/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj @@ -1,7 +1,7 @@  - net8.0 + netstandard2.0;net8.0 true true Provides command-line parsing API integration with the generic host API (Microsoft.Extensions.Hosting). @@ -15,6 +15,14 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/test/CommandLineUtils.Tests/AppNameFromEntryAssemblyConventionTests.cs b/test/CommandLineUtils.Tests/AppNameFromEntryAssemblyConventionTests.cs index a3e49d34..77b7d19d 100644 --- a/test/CommandLineUtils.Tests/AppNameFromEntryAssemblyConventionTests.cs +++ b/test/CommandLineUtils.Tests/AppNameFromEntryAssemblyConventionTests.cs @@ -16,7 +16,7 @@ public void ItSetsAppNameToEntryAssemblyIfNotSpecified() return; } - var expected = Assembly.GetEntryAssembly().GetName().Name; + var expected = Assembly.GetEntryAssembly()?.GetName().Name; var app = new CommandLineApplication(); app.Conventions.SetAppNameFromEntryAssembly(); Assert.Equal(expected, app.Name); diff --git a/test/CommandLineUtils.Tests/ArgumentAttributeTests.cs b/test/CommandLineUtils.Tests/ArgumentAttributeTests.cs index 7bf5aa87..71926578 100644 --- a/test/CommandLineUtils.Tests/ArgumentAttributeTests.cs +++ b/test/CommandLineUtils.Tests/ArgumentAttributeTests.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Linq; +using System.Reflection; using Xunit; using Xunit.Abstractions; @@ -43,8 +43,8 @@ public void ThrowsWhenDuplicateArgumentPositionsAreSpecified() Assert.Equal( Strings.DuplicateArgumentPosition( 0, - typeof(DuplicateArguments).GetProperty("AlsoFirst"), - typeof(DuplicateArguments).GetProperty("First")), + Assert.IsAssignableFrom(typeof(DuplicateArguments).GetProperty("AlsoFirst")), + Assert.IsAssignableFrom(typeof(DuplicateArguments).GetProperty("First"))), ex.Message); } diff --git a/test/CommandLineUtils.Tests/AttributeValidatorTests.cs b/test/CommandLineUtils.Tests/AttributeValidatorTests.cs index 2cfad77f..99febd92 100644 --- a/test/CommandLineUtils.Tests/AttributeValidatorTests.cs +++ b/test/CommandLineUtils.Tests/AttributeValidatorTests.cs @@ -39,7 +39,7 @@ public void ItOnlyInvokesAttributeIfValueExists() [InlineData(typeof(PhoneAttribute), "(800) 555-5555", "xyz")] public void ItExecutesValidationAttribute(Type attributeType, string validValue, string invalidValue) { - var attr = (ValidationAttribute)Activator.CreateInstance(attributeType); + var attr = Assert.IsAssignableFrom(Activator.CreateInstance(attributeType)); var app = new CommandLineApplication(); var arg = app.Argument("arg", "arg"); var validator = new AttributeValidator(attr); @@ -53,7 +53,7 @@ public void ItExecutesValidationAttribute(Type attributeType, string validValue, arg.Reset(); arg.TryParse(invalidValue); var result = validator.GetValidationResult(arg, context); - Assert.NotNull(result); + Assert.NotNull(result?.ErrorMessage); Assert.NotEmpty(result.ErrorMessage); } @@ -61,7 +61,7 @@ public void ItExecutesValidationAttribute(Type attributeType, string validValue, [InlineData(typeof(ClassLevelValidationAttribute), "good", "also good", "bad", "also bad")] public void ItExecutesClassLevelValidationAttribute(Type attributeType, string validProp1Value, string validProp2Value, string invalidProp1Value, string invalidProp2Value) { - var attr = (ValidationAttribute)Activator.CreateInstance(attributeType); + var attr = Assert.IsAssignableFrom(Activator.CreateInstance(attributeType)); var app = new CommandLineApplication(); var validator = new AttributeValidator(attr); var factory = new CommandLineValidationContextFactory(app); @@ -76,7 +76,7 @@ public void ItExecutesClassLevelValidationAttribute(Type attributeType, string v app.Model.Arg2 = invalidProp2Value; var result = validator.GetValidationResult(app, context); - Assert.NotNull(result); + Assert.NotNull(result?.ErrorMessage); Assert.NotEmpty(result.ErrorMessage); } @@ -96,7 +96,7 @@ private void OnExecute() { } [InlineData("email@example.com", 0)] public void ValidatesEmailArgument(string? email, int exitCode) { - Assert.Equal(exitCode, CommandLineApplication.Execute(new TestConsole(_output), email)); + Assert.Equal(exitCode, CommandLineApplication.Execute(new TestConsole(_output), email!)); } private class OptionBuilderApp : CommandLineApplication @@ -165,7 +165,7 @@ public void ValidatesAttributesOnOption(string[] args, int exitCode) private sealed class ThrowingValidationAttribute : ValidationAttribute { - public override bool IsValid(object value) + public override bool IsValid(object? value) { throw new InvalidOperationException(); } @@ -182,7 +182,7 @@ private sealed class ClassLevelValidationApp [AttributeUsage(AttributeTargets.Class)] private sealed class ClassLevelValidationAttribute : ValidationAttribute { - public override bool IsValid(object value) + public override bool IsValid(object? value) => value is ClassLevelValidationApp app && app.Arg1 != null && app.Arg1.Contains("good") && app.Arg2 != null && app.Arg2.Contains("good"); @@ -191,7 +191,7 @@ public override bool IsValid(object value) [AttributeUsage(AttributeTargets.Property)] private sealed class ModeValidationAttribute : ValidationAttribute { - public override bool IsValid(object value) + public override bool IsValid(object? value) { return value is string text && text.Contains("mode"); } diff --git a/test/CommandLineUtils.Tests/CommandLineApplicationExecutorTests.cs b/test/CommandLineUtils.Tests/CommandLineApplicationExecutorTests.cs index 99b6cbfd..02f30d1b 100755 --- a/test/CommandLineUtils.Tests/CommandLineApplicationExecutorTests.cs +++ b/test/CommandLineUtils.Tests/CommandLineApplicationExecutorTests.cs @@ -158,8 +158,8 @@ public void ThrowsForUnknownOnExecuteTypes() var ex = Assert.Throws( () => CommandLineApplication.Execute()); var method = typeof(ExecuteWithUnknownTypes).GetMethod("OnExecute", BindingFlags.Instance | BindingFlags.NonPublic); - var param = Assert.Single(method.GetParameters()); - Assert.Equal(Strings.UnsupportedParameterTypeOnMethod(method.Name, param), ex.Message); + var param = Assert.Single(method?.GetParameters() ?? []); + Assert.Equal(Strings.UnsupportedParameterTypeOnMethod(method?.Name, param), ex.Message); } private class ExecuteAsyncWithInt @@ -312,7 +312,7 @@ public void Dispose() [Command("sub")] private class Subcommand { - public DisposableParentCommand Parent { get; } + public DisposableParentCommand? Parent { get; } public void OnExecute() { diff --git a/test/CommandLineUtils.Tests/CommandLineApplicationTests.cs b/test/CommandLineUtils.Tests/CommandLineApplicationTests.cs index c7d3d5c8..717ab5ed 100644 --- a/test/CommandLineUtils.Tests/CommandLineApplicationTests.cs +++ b/test/CommandLineUtils.Tests/CommandLineApplicationTests.cs @@ -492,7 +492,7 @@ public void AllowNoThrowBehaviorOnUnexpectedOptionAfterSubcommand() // (does not throw) app.Execute("k", "run", unexpectedOption); Assert.Empty(testCmd.RemainingArguments); - var arg = Assert.Single(subCmd?.RemainingArguments); + var arg = Assert.Single(subCmd?.RemainingArguments ?? []); Assert.Equal(unexpectedOption, arg); } @@ -697,9 +697,9 @@ public void NestedInheritedOptions() Assert.Contains(subcmd1.GetOptions(), o => o.LongName == "nest1"); Assert.Contains(subcmd1.GetOptions(), o => o.LongName == "global"); - Assert.Contains(subcmd2?.GetOptions(), o => o.LongName == "nest2"); - Assert.Contains(subcmd2?.GetOptions(), o => o.LongName == "nest1"); - Assert.Contains(subcmd2?.GetOptions(), o => o.LongName == "global"); + Assert.Contains(subcmd2?.GetOptions() ?? [], o => o.LongName == "nest2"); + Assert.Contains(subcmd2?.GetOptions() ?? [], o => o.LongName == "nest1"); + Assert.Contains(subcmd2?.GetOptions() ?? [], o => o.LongName == "global"); Assert.ThrowsAny(() => app.Execute("--nest2", "N2", "--nest1", "N1", "-g", "G")); Assert.ThrowsAny(() => app.Execute("lvl1", "--nest2", "N2", "--nest1", "N1", "-g", "G")); @@ -1051,7 +1051,7 @@ public void ThrowsExceptionOnInvalidArgument(string? inputOption) { var app = new CommandLineApplication(); - var exception = Assert.ThrowsAny(() => app.Execute(inputOption)); + var exception = Assert.ThrowsAny(() => app.Execute(inputOption!)); Assert.Equal($"Unrecognized command or argument '{inputOption}'", exception.Message); } diff --git a/test/CommandLineUtils.Tests/CustomValidationAttributeTest.cs b/test/CommandLineUtils.Tests/CustomValidationAttributeTest.cs index 0cd119f2..2f6692cd 100644 --- a/test/CommandLineUtils.Tests/CustomValidationAttributeTest.cs +++ b/test/CommandLineUtils.Tests/CustomValidationAttributeTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.ComponentModel.DataAnnotations; +using System.Linq; using Xunit; namespace McMaster.Extensions.CommandLineUtils.Tests @@ -12,7 +13,7 @@ public class CustomValidationAttributeTest [InlineData(null)] [InlineData("-c", "red")] [InlineData("-c", "blue")] - public void CustomValidationAttributePasses(params string?[] args) + public void CustomValidationAttributePasses(params string[]? args) { var app = new CommandLineApplication(); app.Conventions.UseDefaultConventions(); @@ -34,7 +35,7 @@ public void CustomValidationAttributeFails(params string?[] args) { var app = new CommandLineApplication(); app.Conventions.UseAttributes(); - var result = app.Parse(args); + var result = app.Parse(args.Select(a => a!).ToArray()); var validationResult = result.SelectedCommand.GetValidationResult(); Assert.NotEqual(ValidationResult.Success, validationResult); var program = Assert.IsType>(result.SelectedCommand); @@ -43,7 +44,7 @@ public void CustomValidationAttributeFails(params string?[] args) { Assert.Equal(args[1], app.Model.Color); } - Assert.Equal("The value for --color must be 'red' or 'blue'", validationResult.ErrorMessage); + Assert.Equal("The value for --color must be 'red' or 'blue'", validationResult?.ErrorMessage); } private class RedBlueProgram diff --git a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs index 7f46adf4..e2084052 100644 --- a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs +++ b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs @@ -79,7 +79,7 @@ public void DoesNotOrderCommandsByName() Assert.True(indexOfA > indexOfB); } - private string GetHelpText(CommandLineApplication app, DefaultHelpTextGenerator generator, string helpOption = null) + private string GetHelpText(CommandLineApplication app, DefaultHelpTextGenerator generator, string? helpOption = null) { var sb = new StringBuilder(); app.Out = new StringWriter(sb); @@ -90,7 +90,7 @@ private string GetHelpText(CommandLineApplication app, DefaultHelpTextGenerator return helpText; } - private string GetHelpText(CommandLineApplication app, string helpOption = null) + private string GetHelpText(CommandLineApplication app, string? helpOption = null) { var generator = new DefaultHelpTextGenerator { @@ -232,12 +232,12 @@ SomeNullableEnumArgument nullable enum arg desc. public class MyApp { [Option(ShortName = "strOpt", ValueName = "STR_OPT", Description = "str option desc.")] - public string strOpt { get; set; } + public string? strOpt { get; set; } [Option(ShortName = "rStrOpt", ValueName = "STR_OPT", Description = "restricted str option desc.")] [Required] [AllowedValues("Foo", "Bar")] - public string rStrOpt { get; set; } + public string? rStrOpt { get; set; } [Option(ShortName = "dStrOpt", ValueName = "STR_OPT", Description = "str option with default value desc.")] public string dStrOpt { get; set; } = "Foo"; @@ -265,12 +265,12 @@ public class MyApp public SomeEnum Verb5 { get; set; } [Argument(0, Description = "string arg desc.")] - public string SomeStringArgument { get; set; } + public string? SomeStringArgument { get; set; } [Argument(1, Description = "restricted string arg desc.")] [Required] [AllowedValues("Foo", "Bar")] - public string RestrictedStringArgument { get; set; } + public string? RestrictedStringArgument { get; set; } [Argument(2, Description = "string arg with default value desc.")] public string DefaultValStringArgument { get; set; } = "Foo"; diff --git a/test/CommandLineUtils.Tests/FilePathExistsAttributeTests.cs b/test/CommandLineUtils.Tests/FilePathExistsAttributeTests.cs index 63fd189c..22f06da0 100644 --- a/test/CommandLineUtils.Tests/FilePathExistsAttributeTests.cs +++ b/test/CommandLineUtils.Tests/FilePathExistsAttributeTests.cs @@ -45,7 +45,7 @@ public void ValidatesFilesMustExist(string? filePath) .GetValidationResult(); Assert.NotEqual(ValidationResult.Success, result); - Assert.Equal($"The file path '{filePath}' does not exist.", result.ErrorMessage); + Assert.Equal($"The file path '{filePath}' does not exist.", result?.ErrorMessage); var console = new TestConsole(_output); Assert.NotEqual(0, CommandLineApplication.Execute(console, filePath!)); @@ -95,7 +95,7 @@ public void ValidatesFilesRelativeToAppContext() Assert.Equal(ValidationResult.Success, success); Assert.NotEqual(ValidationResult.Success, fails); - Assert.Equal("The file path 'exists.txt' does not exist.", fails.ErrorMessage); + Assert.Equal("The file path 'exists.txt' does not exist.", fails?.ErrorMessage); var console = new TestConsole(_output); var context = new DefaultCommandLineContext(console, appNotInBaseDir.WorkingDirectory, new[] { "exists.txt" }); diff --git a/test/CommandLineUtils.Tests/FilePathNotExistsAttributeTests.cs b/test/CommandLineUtils.Tests/FilePathNotExistsAttributeTests.cs index 79fff038..9a0374c4 100644 --- a/test/CommandLineUtils.Tests/FilePathNotExistsAttributeTests.cs +++ b/test/CommandLineUtils.Tests/FilePathNotExistsAttributeTests.cs @@ -51,7 +51,7 @@ public void ValidatesFilesMustNotExist(string filePath) .GetValidationResult(); Assert.NotEqual(ValidationResult.Success, result); - Assert.Equal($"The file path '{filePath}' already exists.", result.ErrorMessage); + Assert.Equal($"The file path '{filePath}' already exists.", result?.ErrorMessage); var console = new TestConsole(_output); Assert.NotEqual(0, CommandLineApplication.Execute(console, filePath)); @@ -90,7 +90,7 @@ public void ValidatesFilesRelativeToAppContext() .GetValidationResult(); Assert.NotEqual(ValidationResult.Success, fails); - Assert.Equal("The file path 'exists.txt' already exists.", fails.ErrorMessage); + Assert.Equal("The file path 'exists.txt' already exists.", fails?.ErrorMessage); Assert.Equal(ValidationResult.Success, success); diff --git a/test/CommandLineUtils.Tests/HelpOptionAttributeTests.cs b/test/CommandLineUtils.Tests/HelpOptionAttributeTests.cs index 7e40b5f5..b3fab333 100644 --- a/test/CommandLineUtils.Tests/HelpOptionAttributeTests.cs +++ b/test/CommandLineUtils.Tests/HelpOptionAttributeTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Reflection; using System.Text; using Xunit; using Xunit.Abstractions; @@ -90,7 +91,7 @@ public void ThrowsIfMultipleAttributesApplied() { var ex = Assert.Throws(() => new CommandLineApplication().Conventions.UseHelpOptionAttribute()); - var prop = typeof(DuplicateOptionAttributes).GetProperty(nameof(DuplicateOptionAttributes.IsHelpOption)); + var prop = Assert.IsAssignableFrom(typeof(DuplicateOptionAttributes).GetProperty(nameof(DuplicateOptionAttributes.IsHelpOption))); Assert.Equal(Strings.BothOptionAndHelpOptionAttributesCannotBeSpecified(prop), ex.Message); } diff --git a/test/CommandLineUtils.Tests/LegalFilePathAttributeTests.cs b/test/CommandLineUtils.Tests/LegalFilePathAttributeTests.cs index 3f2bf4ea..2bd7b02c 100644 --- a/test/CommandLineUtils.Tests/LegalFilePathAttributeTests.cs +++ b/test/CommandLineUtils.Tests/LegalFilePathAttributeTests.cs @@ -49,7 +49,7 @@ public void ValidatesLegalFilePaths(string filePath) public void FailsInvalidLegalFilePaths(string? filePath) { var console = new TestConsole(_output); - Assert.NotEqual(0, CommandLineApplication.Execute(console, filePath)); + Assert.NotEqual(0, CommandLineApplication.Execute(console, filePath!)); } } } diff --git a/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj b/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj index 317c17cd..b0eab390 100644 --- a/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj +++ b/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj @@ -1,9 +1,11 @@  - net8.0;net10.0 - - annotations + <_IsWindows Condition="$([MSBuild]::IsOSPlatform('Windows'))">true + + net8.0;net10.0;net472 + net8.0;net10.0 + enable @@ -27,6 +29,13 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/test/CommandLineUtils.Tests/OptionAttributeTests.cs b/test/CommandLineUtils.Tests/OptionAttributeTests.cs index feb8f475..91416c43 100644 --- a/test/CommandLineUtils.Tests/OptionAttributeTests.cs +++ b/test/CommandLineUtils.Tests/OptionAttributeTests.cs @@ -31,7 +31,7 @@ public void ThrowsWhenOptionTypeCannotBeDetermined() var ex = Assert.Throws( () => Create()); Assert.Equal( - Strings.CannotDetermineOptionType(typeof(AppWithUnknownOptionType).GetProperty("Option")), + Strings.CannotDetermineOptionType(Assert.IsAssignableFrom(typeof(AppWithUnknownOptionType).GetProperty("Option"))), ex.Message); } @@ -71,7 +71,7 @@ private class EmptyShortName public void CanSetShortNameToEmptyString() { var app = Create(); - Assert.All(app.Options, o => Assert.Empty(o.ShortName)); + Assert.All(app.Options, o => Assert.Empty(o.ShortName ?? "test")); } private class AmbiguousShortOptionName @@ -91,8 +91,8 @@ public void ThrowsWhenShortOptionNamesAreAmbiguous() Assert.Equal( Strings.OptionNameIsAmbiguous("m", - typeof(AmbiguousShortOptionName).GetProperty("Mode"), - typeof(AmbiguousShortOptionName).GetProperty("Message")), + Assert.IsAssignableFrom(typeof(AmbiguousShortOptionName).GetProperty("Mode")), + Assert.IsAssignableFrom(typeof(AmbiguousShortOptionName).GetProperty("Message"))), ex.Message); } @@ -113,8 +113,8 @@ public void ThrowsWhenLongOptionNamesAreAmbiguous() Assert.Equal( Strings.OptionNameIsAmbiguous("no-edit", - typeof(AmbiguousLongOptionName).GetProperty("NoEdit"), - typeof(AmbiguousLongOptionName).GetProperty("ManuallySetToNoEdit")), + Assert.IsAssignableFrom(typeof(AmbiguousLongOptionName).GetProperty("NoEdit")), + Assert.IsAssignableFrom(typeof(AmbiguousLongOptionName).GetProperty("ManuallySetToNoEdit"))), ex.Message); } @@ -133,7 +133,7 @@ public void ThrowsWhenOptionAndArgumentAreSpecified() Assert.Equal( Strings.BothOptionAndArgumentAttributesCannotBeSpecified( - typeof(BothOptionAndArgument).GetProperty("NotPossible")), + Assert.IsAssignableFrom(typeof(BothOptionAndArgument).GetProperty("NotPossible"))), ex.Message); } @@ -203,7 +203,7 @@ public void KeepsDefaultValues() private class AppWithMultiValueStringOption { [Option("-o1")] - string[] Opt1 { get; } + string[]? Opt1 { get; } [Option("-o2")] string[] Opt2 { get; } = Array.Empty(); @@ -341,12 +341,12 @@ private CommandOption CreateOption(Type propType, string propName) var tb = mb.DefineType("Program"); var pb = tb.DefineProperty(propName, PropertyAttributes.None, propType, Array.Empty()); tb.DefineField($"<{propName}>k__BackingField", propType, FieldAttributes.Private); - var ctor = typeof(OptionAttribute).GetConstructor(Array.Empty()); + var ctor = Assert.IsAssignableFrom(typeof(OptionAttribute).GetConstructor(Array.Empty())); var ab = new CustomAttributeBuilder(ctor, Array.Empty()); pb.SetCustomAttribute(ab); var program = tb.CreateType(); var appBuilder = typeof(CommandLineApplication<>).MakeGenericType(program); - var app = (CommandLineApplication)Activator.CreateInstance(appBuilder, Array.Empty()); + var app = Assert.IsAssignableFrom(Activator.CreateInstance(appBuilder, Array.Empty())); app.Conventions.UseOptionAttributes(); return app.Options.First(); } @@ -465,7 +465,7 @@ public void ApplyingOptionConventionTwice_WithLongOnlyOptions_DoesNotThrow() Assert.Single(app.Options, o => o.LongName == "count"); // Verify short names are empty - Assert.All(app.Options, o => Assert.Empty(o.ShortName)); + Assert.All(app.Options, o => Assert.Empty(o.ShortName ?? "test")); } #endregion diff --git a/test/CommandLineUtils.Tests/ResponseFileTests.cs b/test/CommandLineUtils.Tests/ResponseFileTests.cs index 90621e12..bb5867c9 100644 --- a/test/CommandLineUtils.Tests/ResponseFileTests.cs +++ b/test/CommandLineUtils.Tests/ResponseFileTests.cs @@ -269,7 +269,7 @@ public void SubcommandsCanResponseFileOptions() }); var rspFile = CreateResponseFile(" 'lorem ipsum' ", "dolor sit amet"); app.Execute("save", "@" + rspFile); - Assert.Collection(wordArgs?.Values, + Assert.Collection(wordArgs?.Values ?? [], a => Assert.Equal("lorem ipsum", a), a => Assert.Equal("dolor", a), a => Assert.Equal("sit", a), diff --git a/test/CommandLineUtils.Tests/StringExtensionsTests.cs b/test/CommandLineUtils.Tests/StringExtensionsTests.cs index ce107778..b435ae47 100644 --- a/test/CommandLineUtils.Tests/StringExtensionsTests.cs +++ b/test/CommandLineUtils.Tests/StringExtensionsTests.cs @@ -29,7 +29,7 @@ public class StringExtensionsTests [InlineData("m_Field", "m-field")] public void ToKebabCase(string? input, string? expected) { - Assert.Equal(expected, input.ToKebabCase()); + Assert.Equal(expected, input!.ToKebabCase()); } [Theory] @@ -41,7 +41,7 @@ public void ToKebabCase(string? input, string? expected) [InlineData("MSBuildTask", "MSBUILD_TASK")] public void ToConstantCase(string? input, string? expected) { - Assert.Equal(expected, input.ToConstantCase()); + Assert.Equal(expected, input!.ToConstantCase()); } } } diff --git a/test/CommandLineUtils.Tests/ValidateMethodConventionTests.cs b/test/CommandLineUtils.Tests/ValidateMethodConventionTests.cs index a37097eb..09609a2f 100644 --- a/test/CommandLineUtils.Tests/ValidateMethodConventionTests.cs +++ b/test/CommandLineUtils.Tests/ValidateMethodConventionTests.cs @@ -25,7 +25,7 @@ public void ValidatorAddedViaConvention() app.Conventions.UseOnValidateMethodFromModel(); var result = app.GetValidationResult(); Assert.NotEqual(ValidationResult.Success, result); - Assert.Equal("Failed", result.ErrorMessage); + Assert.Equal("Failed", result?.ErrorMessage); } private class ProgramWithBadOnValidate @@ -49,7 +49,7 @@ private class MainValidate [Option] public int? Middle { get; } - private ValidationResult OnValidate(ValidationContext context, CommandLineContext appContext) + private ValidationResult? OnValidate(ValidationContext context, CommandLineContext appContext) { if (this.Middle.HasValue && this.Middle < 0) { @@ -71,7 +71,7 @@ private class SubcommandValidate [Option] public int End { get; private set; } = Int32.MaxValue; - private ValidationResult OnValidate(ValidationContext context, CommandLineContext appContext) + private ValidationResult? OnValidate(ValidationContext context, CommandLineContext appContext) { if (this.Start >= this.End) { @@ -119,7 +119,7 @@ public void ValidatorShouldGetDeserializedModelInSubcommands(string args, string else { Assert.NotEqual(ValidationResult.Success, result); - Assert.Equal(error, result.ErrorMessage); + Assert.Equal(error, result?.ErrorMessage); } } } diff --git a/test/CommandLineUtils.Tests/ValidationTests.cs b/test/CommandLineUtils.Tests/ValidationTests.cs index b4cfee74..1797c971 100644 --- a/test/CommandLineUtils.Tests/ValidationTests.cs +++ b/test/CommandLineUtils.Tests/ValidationTests.cs @@ -50,7 +50,7 @@ public void ValidatorInvoked() app.OnValidate(_ => { called = true; - return ValidationResult.Success; + return ValidationResult.Success!; }); Assert.Equal(0, app.Execute()); Assert.True(called); diff --git a/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs b/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs index 77edfb54..a5295fae 100644 --- a/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs +++ b/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs @@ -144,7 +144,7 @@ public void DefaultCultureCanBeChanged(string property, string test, string cult app.Conventions.UseAttributes(); app.Parse(test); - var actual = (DateTimeOffset)typeof(DateParserProgram).GetProperty(property).GetMethod.Invoke(app.Model, null); + var actual = Assert.IsAssignableFrom(typeof(DateParserProgram).GetProperty(property)?.GetMethod?.Invoke(app.Model, null)); Assert.Equal(expected, actual); } diff --git a/test/CommandLineUtils.Tests/VersionOptionAttributeTests.cs b/test/CommandLineUtils.Tests/VersionOptionAttributeTests.cs index 2afe5da4..b5b7b54d 100644 --- a/test/CommandLineUtils.Tests/VersionOptionAttributeTests.cs +++ b/test/CommandLineUtils.Tests/VersionOptionAttributeTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Reflection; using Xunit; using Xunit.Abstractions; @@ -88,7 +89,7 @@ public void ThrowsIfMultipleAttributesApplied() { var ex = Assert.Throws(() => new CommandLineApplication().Conventions.UseVersionOptionAttribute()); - var prop = typeof(DuplicateOptionAttributes).GetProperty(nameof(DuplicateOptionAttributes.IsVersionOption)); + var prop = Assert.IsAssignableFrom(typeof(DuplicateOptionAttributes).GetProperty(nameof(DuplicateOptionAttributes.IsVersionOption))); Assert.Equal(Strings.BothOptionAndVersionOptionAttributesCannotBeSpecified(prop), ex.Message); } @@ -104,8 +105,7 @@ public void ThrowsIfHelpAndVersionAttributesApplied() { var ex = Assert.Throws(() => new CommandLineApplication().Conventions.UseVersionOptionAttribute()); - var prop = typeof(DuplicateOptionAttributes2).GetProperty(nameof(DuplicateOptionAttributes - .IsVersionOption)); + var prop = Assert.IsAssignableFrom(typeof(DuplicateOptionAttributes2).GetProperty(nameof(DuplicateOptionAttributes.IsVersionOption))); Assert.Equal(Strings.BothHelpOptionAndVersionOptionAttributesCannotBeSpecified(prop), ex.Message); } diff --git a/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj b/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj index 5989a3de..5c925c68 100644 --- a/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj +++ b/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj @@ -1,7 +1,11 @@  - net8.0;net10.0 + <_IsWindows Condition="$([MSBuild]::IsOSPlatform('Windows'))">true + + net8.0;net10.0;net472 + net8.0;net10.0 + enable @@ -24,6 +28,13 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + +