diff --git a/.claude/skills/prepare-release/SKILL.md b/.claude/skills/prepare-release/SKILL.md
index d22fa5f2..cd24b4fc 100644
--- a/.claude/skills/prepare-release/SKILL.md
+++ b/.claude/skills/prepare-release/SKILL.md
@@ -23,16 +23,20 @@ Run git commands to analyze commits since last release:
- `git log --grep` for PR merges
- Look for conventional commit patterns: `fix:`, `feat:`, `break:`, `docs:`, etc.
-### 3. Categorize Changes
+### 3. Filter and Categorize Changes
-Group into categories (in this order):
+**Exclude from release notes:**
+- Dependabot / automated dependency bump commits (e.g., `chore(deps): Bump ...`). These are routine maintenance and not user-facing.
+- CI/tooling-only changes (e.g., updating GitHub Actions workflows, claude workflows) unless they affect the shipped package.
+
+Group remaining changes into categories (in this order):
1. **Breaking changes** (major versions only) - API removals, behavior changes
2. **Features** - New functionality or APIs
3. **Fixes** - Bug fixes and corrections
4. **Improvements** - Performance or usability enhancements
5. **Docs** - Documentation-only changes
-6. **Other** - Infrastructure, tooling, CI/CD
+6. **Other** - Infrastructure, tooling, CI/CD (only if user-facing or noteworthy)
### 4. Generate releasenotes.props Entry
@@ -57,15 +61,31 @@ Fixes:
```
**Patch versions:**
+
+Append the patch notes directly at the end of the existing parent version's `StartsWith('X.Y.')` block. Do NOT create a separate conditional block. Add the patch section just before the closing `` tag of the parent version:
+
```xml
-
-$(PackageReleaseNotes)
+
+...existing X.Y.0 release notes...
-X.Y.Z patch:
+Updates in X.Y.Z patch:
* @user: fix description (#123)
```
+For multiple patches, append each one in order at the end of the same block:
+
+```xml
+...existing X.Y.0 release notes...
+
+Updates in X.Y.1 patch:
+* @user: fix description (#123)
+
+Updates in X.Y.2 patch:
+* @user: another fix (#456)
+
+```
+
### 5. Generate CHANGELOG.md Entry
**Format:** `* [@contributor]: description ([#PR])`
@@ -196,10 +216,7 @@ See https://natemcmaster.github.io/CommandLineUtils/vX.0/upgrade-guide.html
### Patch Versions
-Use `$(PackageReleaseNotes)` to inherit parent version's notes.
-
-For first patch (X.Y.1), create new conditional entry after parent.
-For subsequent patches, add BEFORE existing patches but AFTER minor version.
+Append patch notes directly into the existing parent version's `StartsWith('X.Y.')` block in `releasenotes.props`. Do NOT create a separate conditional block or use `$(PackageReleaseNotes)` inheritance. Each patch gets an "Updates in X.Y.Z patch:" section appended at the end of the parent block.
## Quality Checklist
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a4ca9a14..5c428df7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [v5.0.1](https://github.com/natemcmaster/CommandLineUtils/compare/v5.0.0...v5.0.1)
+
+### Features
+* [@sensslen]: Restore target framework compilation for .NET Framework ([#591])
+
+[#591]: https://github.com/natemcmaster/CommandLineUtils/pull/591
+
## [v5.0.0](https://github.com/natemcmaster/CommandLineUtils/compare/v4.1.1...v5.0.0)
### Breaking changes
diff --git a/Directory.Build.props b/Directory.Build.props
index 2e972097..aa9c9601 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -31,6 +31,8 @@
$(WarningsNotAsErrors);1591
true
enable
+
+ annotations
$(MSBuildThisFileDirectory)src\StrongName.snk
true
@@ -41,7 +43,7 @@
- 5.0.0
+ 5.0.1
beta
true
$(GITHUB_RUN_NUMBER)
@@ -53,6 +55,11 @@
$(PackageVersion)+$(RepositoryCommit)
+
+
+
+
+
diff --git a/README.md b/README.md
index 0e5061a8..bab675ad 100644
--- a/README.md
+++ b/README.md
@@ -138,3 +138,15 @@ If you need help with this project, please ...
This is a fork of [Microsoft.Extensions.CommandLineUtils](https://github.com/aspnet/Common), which was [completely abandoned by Microsoft](https://github.com/aspnet/Common/issues/257). This project [forked in 2017](https://github.com/natemcmaster/CommandLineUtils/commit/f039360e4e51bbf8b8eb6236894b626ec7944cec) and continued to make improvements. From 2017 to 2021, over 30 contributors added new features and fixed bugs. As of 2022, the project has entered maintenance mode, so no major changes are planned. [See this issue for details on latest project status.](https://github.com/natemcmaster/CommandLineUtils/issues/485) This project is not abandoned -- I believe this library provides a stable API and rich feature set good enough for most developers to create command line apps in .NET -- but only the most critical of bugs will be fixed (such as security issues).
+## Supported .NET Versions
+
+Framework | Version | Reason
+---------------|---------|--------------------
+`dotnet` | 8.0 | Lowest Microsoft LTS version at time of release. See
+.NET Framework | 4.7.2 | Lowest .NET Framework version fully compatible with [.NET Standard 2.0][netstandard-guidance]
+
+_Why not directly compile for .NET Standard?_
+
+Microsoft guidance says ".NET 5 and later versions adopt a different approach to establishing uniformity that eliminates the need for .NET Standard in most scenarios." Compiling for 2 frameworks appears to be sufficient, so we avoid added complexity.
+
+[netstandard-guidance]: https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0
diff --git a/build.ps1 b/build.ps1
index 989f4708..a0b26fc9 100755
--- a/build.ps1
+++ b/build.ps1
@@ -32,6 +32,14 @@ exec dotnet tool run dotnet-format -- -v detailed @formatArgs "$PSScriptRoot/doc
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"
+
+[string[]] $testArgs = @()
+if (-not $IsWindows) {
+ $testArgs += '-p:TestFullFramework=false'
+}
+
+exec dotnet test --no-build --configuration $Configuration `
+ --collect:"XPlat Code Coverage" `
+ @testArgs
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 cfd7d030..cc21eccc 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/Conventions/SubcommandAttributeConvention.cs b/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs
index f615fc1a..4c2e5839 100644
--- a/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs
+++ b/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs
@@ -58,7 +58,13 @@ 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)]
+#elif NET472_OR_GREATER
+#else
+#error Target framework misconfiguration
+#endif
+ Type subcommandType,
ICommandMetadataProvider provider,
string name)
{
diff --git a/src/CommandLineUtils/IO/Pager.cs b/src/CommandLineUtils/IO/Pager.cs
index 31559ec4..a1a0307a 100644
--- a/src/CommandLineUtils/IO/Pager.cs
+++ b/src/CommandLineUtils/IO/Pager.cs
@@ -122,8 +122,11 @@ public void Kill()
FileName = "less",
Arguments = ArgumentEscaper.EscapeAndConcatenate(args),
RedirectStandardInput = true,
-#if NET46_OR_GREATER
+#if NET472_OR_GREATER
UseShellExecute = false,
+#elif NET6_0_OR_GREATER
+#else
+#error Target framework misconfiguration
#endif
}
};
diff --git a/src/CommandLineUtils/Internal/DictionaryExtensions.cs b/src/CommandLineUtils/Internal/DictionaryExtensions.cs
new file mode 100644
index 00000000..5918f826
--- /dev/null
+++ b/src/CommandLineUtils/Internal/DictionaryExtensions.cs
@@ -0,0 +1,25 @@
+// 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
+{
+ internal static class DictionaryExtensions
+ {
+#if NET6_0_OR_GREATER
+#elif NET472_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;
+ }
+#else
+#error Target framework misconfiguration
+#endif
+ }
+}
diff --git a/src/CommandLineUtils/Internal/ReflectionHelper.cs b/src/CommandLineUtils/Internal/ReflectionHelper.cs
index 3567560a..2f9cdef5 100644
--- a/src/CommandLineUtils/Internal/ReflectionHelper.cs
+++ b/src/CommandLineUtils/Internal/ReflectionHelper.cs
@@ -188,12 +188,41 @@ 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 NET6_0_OR_GREATER
+ return x.HasSameMetadataDefinitionAs(y);
+#elif NET472_OR_GREATER
+ return x.MetadataToken == y.MetadataToken && x.Module.Equals(y.Module);
+#else
+#error Target framework misconfiguration
+#endif
}
public int GetHashCode(MethodInfo obj)
{
+#if NET6_0_OR_GREATER
return obj.HasMetadataToken() ? obj.GetMetadataToken().GetHashCode() : 0;
+#elif NET472_OR_GREATER
+ // 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;
+#else
+#error Target framework misconfiguration
+#endif
}
}
diff --git a/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj b/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj
index ac9b2874..236eb6f2 100644
--- a/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj
+++ b/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj
@@ -1,7 +1,7 @@
-
+
- net8.0
+ net8.0;net472
true
true
Command-line parsing API.
@@ -30,6 +30,10 @@ McMaster.Extensions.CommandLineUtils.ArgumentEscaper
+
+
+
+
-
diff --git a/src/CommandLineUtils/Properties/NullabilityHelpers.cs b/src/CommandLineUtils/Properties/NullabilityHelpers.cs
deleted file mode 100644
index 2ba73d2a..00000000
--- a/src/CommandLineUtils/Properties/NullabilityHelpers.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) Nate McMaster.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-// Files here are for simplify annotations of nullable code and are not functional in .NET Standard 2.0
-#if NETSTANDARD2_0 || NET46_OR_GREATER
-namespace System.Diagnostics.CodeAnalysis
-{
- // https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.notnullwhenattribute?
- [AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
- internal sealed class NotNullWhenAttribute : Attribute
- {
- 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)]
- internal sealed class AllowNullAttribute : Attribute { }
-}
-#endif
diff --git a/src/CommandLineUtils/SourceGeneration/ActivatorModelFactory.cs b/src/CommandLineUtils/SourceGeneration/ActivatorModelFactory.cs
index 2ec3c8d9..b9967031 100644
--- a/src/CommandLineUtils/SourceGeneration/ActivatorModelFactory.cs
+++ b/src/CommandLineUtils/SourceGeneration/ActivatorModelFactory.cs
@@ -11,7 +11,12 @@ 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")]
+#elif NET472_OR_GREATER
+#else
+#error Target framework misconfiguration
+#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..1e6f95d9 100644
--- a/src/CommandLineUtils/SourceGeneration/DefaultMetadataResolver.cs
+++ b/src/CommandLineUtils/SourceGeneration/DefaultMetadataResolver.cs
@@ -30,7 +30,12 @@ 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.")]
+#elif NET472_OR_GREATER
+#else
+#error Target framework misconfiguration
+#endif
public ICommandMetadataProvider GetProvider(Type modelType)
{
// Check for generated metadata first (AOT-safe path)
@@ -50,7 +55,12 @@ 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.")]
+#elif NET472_OR_GREATER
+#else
+#error Target framework misconfiguration
+#endif
public ICommandMetadataProvider GetProvider() where TModel : class
{
// Check for generated metadata first (AOT-safe path)
@@ -78,7 +88,12 @@ public bool HasGeneratedMetadata(Type modelType)
return CommandMetadataRegistry.HasMetadata(modelType);
}
+#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Uses reflection to analyze the model type")]
+#elif NET472_OR_GREATER
+#else
+#error Target framework misconfiguration
+#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..f3e26c38 100644
--- a/src/CommandLineUtils/SourceGeneration/ReflectionExecuteHandler.cs
+++ b/src/CommandLineUtils/SourceGeneration/ReflectionExecuteHandler.cs
@@ -12,7 +12,12 @@ 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")]
+#elif NET472_OR_GREATER
+#else
+#error Target framework misconfiguration
+#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..3659f241 100644
--- a/src/CommandLineUtils/SourceGeneration/ReflectionMetadataProvider.cs
+++ b/src/CommandLineUtils/SourceGeneration/ReflectionMetadataProvider.cs
@@ -15,7 +15,12 @@ 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")]
+#elif NET472_OR_GREATER
+#else
+#error Target framework misconfiguration
+#endif
internal sealed class ReflectionMetadataProvider : ICommandMetadataProvider
{
private const BindingFlags MethodLookup = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
diff --git a/src/CommandLineUtils/SourceGeneration/ReflectionValidateHandler.cs b/src/CommandLineUtils/SourceGeneration/ReflectionValidateHandler.cs
index bb473e9f..3ead1679 100644
--- a/src/CommandLineUtils/SourceGeneration/ReflectionValidateHandler.cs
+++ b/src/CommandLineUtils/SourceGeneration/ReflectionValidateHandler.cs
@@ -12,7 +12,12 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration
///
/// Validate handler that uses reflection to invoke OnValidate.
///
+#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Uses reflection to invoke method")]
+#elif NET472_OR_GREATER
+#else
+#error Target framework misconfiguration
+#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..68dbf57f 100644
--- a/src/CommandLineUtils/SourceGeneration/ReflectionValidationErrorHandler.cs
+++ b/src/CommandLineUtils/SourceGeneration/ReflectionValidationErrorHandler.cs
@@ -11,7 +11,12 @@ 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")]
+#elif NET472_OR_GREATER
+#else
+#error Target framework misconfiguration
+#endif
internal sealed class ReflectionValidationErrorHandler : IValidationErrorHandler
{
private readonly MethodInfo _method;
diff --git a/src/CommandLineUtils/releasenotes.props b/src/CommandLineUtils/releasenotes.props
index 0e5f7489..b857d9fc 100644
--- a/src/CommandLineUtils/releasenotes.props
+++ b/src/CommandLineUtils/releasenotes.props
@@ -26,6 +26,9 @@ Other:
* @natemcmaster: Use NuGet trusted publishing with OIDC
* @dependabot: Update GitHub Actions (#568)
* @natemcmaster: Upgrade docfx to 2.78.4
+
+Updates in 5.0.1 patch:
+* @sensslen: Restore target framework compilation for .NET Framework (#591)
Changes since 4.0:
diff --git a/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj b/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj
index 42eafd8a..ec78fa0d 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
+ net8.0;net472
true
true
Provides command-line parsing API integration with the generic host API (Microsoft.Extensions.Hosting).
diff --git a/test/.runsettings b/test/.runsettings
new file mode 100644
index 00000000..29e4ea44
--- /dev/null
+++ b/test/.runsettings
@@ -0,0 +1,11 @@
+
+
+
+
+
+ True
+
+
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..d94d0773 100755
--- a/test/CommandLineUtils.Tests/CommandLineApplicationExecutorTests.cs
+++ b/test/CommandLineUtils.Tests/CommandLineApplicationExecutorTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Nate McMaster.
+// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@@ -158,6 +158,7 @@ public void ThrowsForUnknownOnExecuteTypes()
var ex = Assert.Throws(
() => CommandLineApplication.Execute());
var method = typeof(ExecuteWithUnknownTypes).GetMethod("OnExecute", BindingFlags.Instance | BindingFlags.NonPublic);
+ Assert.NotNull(method);
var param = Assert.Single(method.GetParameters());
Assert.Equal(Strings.UnsupportedParameterTypeOnMethod(method.Name, param), ex.Message);
}
@@ -312,7 +313,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..e8b0013f 100644
--- a/test/CommandLineUtils.Tests/CommandLineApplicationTests.cs
+++ b/test/CommandLineUtils.Tests/CommandLineApplicationTests.cs
@@ -492,7 +492,8 @@ public void AllowNoThrowBehaviorOnUnexpectedOptionAfterSubcommand()
// (does not throw)
app.Execute("k", "run", unexpectedOption);
Assert.Empty(testCmd.RemainingArguments);
- var arg = Assert.Single(subCmd?.RemainingArguments);
+ Assert.NotNull(subCmd);
+ var arg = Assert.Single(subCmd.RemainingArguments);
Assert.Equal(unexpectedOption, arg);
}
@@ -697,9 +698,10 @@ 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.NotNull(subcmd2);
+ 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 +1053,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..15226d80 100644
--- a/test/CommandLineUtils.Tests/CustomValidationAttributeTest.cs
+++ b/test/CommandLineUtils.Tests/CustomValidationAttributeTest.cs
@@ -12,7 +12,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 +34,7 @@ public void CustomValidationAttributeFails(params string?[] args)
{
var app = new CommandLineApplication();
app.Conventions.UseAttributes();
- var result = app.Parse(args);
+ var result = app.Parse(args!);
var validationResult = result.SelectedCommand.GetValidationResult();
Assert.NotEqual(ValidationResult.Success, validationResult);
var program = Assert.IsType>(result.SelectedCommand);
@@ -43,7 +43,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/DotNetExeTests.cs b/test/CommandLineUtils.Tests/DotNetExeTests.cs
index 4e3797b3..14c7cdb0 100644
--- a/test/CommandLineUtils.Tests/DotNetExeTests.cs
+++ b/test/CommandLineUtils.Tests/DotNetExeTests.cs
@@ -3,7 +3,7 @@
// This file has been modified from the original form. See Notice.txt in the project root for more information.
-#if NETCOREAPP3_1_OR_GREATER
+#if NET6_0_OR_GREATER
using System.IO;
using Xunit;
@@ -22,7 +22,7 @@ public void FindsTheDotNetPath()
}
}
}
-#elif NET472
+#elif NET472_OR_GREATER
#else
#error Update target frameworks
#endif
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..d40eec30 100644
--- a/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj
+++ b/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj
@@ -2,8 +2,7 @@
net8.0;net10.0
-
- annotations
+ $(TargetFrameworks);net472
@@ -27,6 +26,10 @@
+
+
+
+
diff --git a/test/CommandLineUtils.Tests/OptionAttributeTests.cs b/test/CommandLineUtils.Tests/OptionAttributeTests.cs
index feb8f475..beeca0f9 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.True(o.ShortName is null or ""));
}
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
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..4f29a0a9 100644
--- a/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj
+++ b/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj
@@ -2,6 +2,7 @@
net8.0;net10.0
+ $(TargetFrameworks);net472