diff --git a/Directory.Build.props b/Directory.Build.props
index e65d6bd..15f915a 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -91,6 +91,11 @@
+
+
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f3e83bf..b54992b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -22,6 +22,7 @@
+
@@ -32,7 +33,12 @@
+
+
+
+
+
diff --git a/IgnoredWords.dic b/IgnoredWords.dic
index 86f8cae..957a873 100644
--- a/IgnoredWords.dic
+++ b/IgnoredWords.dic
@@ -47,6 +47,7 @@ Cmp
Comdat
Comdats
Committers
+comparand
compat
Concat
Config
@@ -67,9 +68,11 @@ downcasts
endian
endianess
endif
+endregion
enum
Enums
env
+equatability
exe
facepalm
fallback
@@ -170,6 +173,7 @@ stdcall
struct
structs
Subrange
+suppressions
Sym
Tag
telliam
@@ -205,6 +209,7 @@ Users
usings
utils
validator
+validators
varargs
variadic
vcxproj
diff --git a/NuGet.Config b/NuGet.Config
index 7572faf..4c68dc5 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -1,9 +1,20 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 0f81ffa..068c82b 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,6 @@ of multiple small libraries that didn't warrant a distinct repository.
>[!IMPORTANT]
> When editing code in this repository make certain that any extensions or tooling that
-> automatically removes trailing whitespace is disabled. It is fine to highlight such cases
-> and most of the time remove any. However, there are some tests where a trailing whitespace
-> is required and a critical part of the tests.
+> ***automatically*** removes trailing whitespace is disabled. It is fine to highlight such
+> cases and most of the time remove any. However, there are some tests where a trailing
+> whitespace is required and a critical part of the tests.
diff --git a/.editorconfig b/src/.editorconfig
similarity index 99%
rename from .editorconfig
rename to src/.editorconfig
index b90504f..80a7700 100644
--- a/.editorconfig
+++ b/src/.editorconfig
@@ -7,14 +7,14 @@ indent_size = 4
insert_final_newline = true
tab_width = 4
end_of_line = crlf
+
+# VSSPELL: Spell checker settings for all files
+vsspell_section_id = bc80fe46ff7a40189dee3f3476198102
# until [VSSpellChecker bug 277](https://github.com/EWSoftware/VSSpellChecker/issues/277)
# is fixed, disable the analyzers. It's more of a PITA than a help
# VSSPELL: Disable the analyzers until Bug 277 is fixed.
vsspell_code_analyzers_enabled = false
-
-# VSSPELL: Spell checker settings for all files
-vsspell_section_id = 1c7003ec377c4bd9bb3c509d29770210
-vsspell_ignored_words_1c7003ec377c4bd9bb3c509d29770210 = File:.\IgnoredWords.dic|bar
+vsspell_ignored_words_bc80fe46ff7a40189dee3f3476198102 = File:.\IgnoredWords.dic
# match VS generated formatting for MSBuild project files
[*.*proj,*.props,*.targets]
@@ -105,6 +105,10 @@ csharp_style_prefer_local_over_anonymous_function = true:error
csharp_style_prefer_index_operator = true:error
csharp_style_prefer_range_operator = true:error
+# CS0618: Type or member is obsolete
+dotnet_diagnostic.CS0618.severity = error
+csharp_style_prefer_simple_property_accessors = true:suggestion
+
# Analysis and refactoring rules for Ubiquity.NET
# Description: Code analysis rules for Ubiquity.NET projects
@@ -983,7 +987,8 @@ dotnet_diagnostic.CA1303.severity = silent
dotnet_diagnostic.CA1304.severity = warning
-dotnet_diagnostic.CA1305.severity = warning
+// CA1305: Specify IFormatProvider
+dotnet_diagnostic.CA1305.severity = silent
dotnet_diagnostic.CA1306.severity = warning
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/AttributeDataExtensions.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/AttributeDataExtensions.cs
new file mode 100644
index 0000000..ab3681e
--- /dev/null
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/AttributeDataExtensions.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CodeAnalysis.Utils
+{
+ /// Utility class to provide extensions to
+ public static class AttributeDataExtensions
+ {
+ /// Tests if the full namespace qualified name of the name matches this type
+ /// AttributeData to test
+ /// Sequence of parts for the name to test with outer most namespace first (Including the simple name)
+ /// true if the name matches and false otherwise
+ public static bool IsFullNameMatch( this AttributeData self, NamespaceQualifiedName fullName )
+ {
+ return self.AttributeClass is not null
+ && self.AttributeClass.Name == fullName.SimpleName
+ && self.AttributeClass.GetNamespaceNames()
+ .SequenceEqual( fullName.NamespaceNames );
+ }
+
+ /// Gets the for the attribute
+ /// self
+ /// for the attribute
+ public static NamespaceQualifiedName GetNamespaceQualifiedName( this AttributeData self )
+ {
+ return self.AttributeClass is null
+ ? new( [], string.Empty )
+ : self.AttributeClass.GetNamespaceQualifiedName();
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/BaseTypeDeclarationSyntaxExtensions.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/BaseTypeDeclarationSyntaxExtensions.cs
index 6ca2942..77bfc7d 100644
--- a/src/Ubiquity.NET.CodeAnalysis.Utils/BaseTypeDeclarationSyntaxExtensions.cs
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/BaseTypeDeclarationSyntaxExtensions.cs
@@ -60,27 +60,26 @@ public static string GetDeclaredNamespace(this BaseTypeDeclarationSyntax syntax)
/// Syntax to get the name for
/// Flag to indicate if the type itself is included in the name [Default:
/// of the syntax or
+ ///
+ ///
+ /// The return type is never null if is true AND it is a structural type (reference or value)
+ /// as the name of the type itself is included.
+ ///
+ ///
public static NestedClassName? GetNestedClassName( this BaseTypeDeclarationSyntax syntax, bool includeSelf = false)
{
// Try and get the parent syntax. If it isn't a type like class/struct, this will be null
- TypeDeclarationSyntax? parentSyntax = includeSelf ? syntax as TypeDeclarationSyntax : syntax.Parent as TypeDeclarationSyntax;
+ TypeDeclarationSyntax? parentSyntax = includeSelf
+ ? syntax as TypeDeclarationSyntax
+ : syntax.Parent as TypeDeclarationSyntax;
+
NestedClassName? parentClassInfo = null;
// We can only be nested in class/struct/record
-
// Keep looping while we're in a supported nested type
while (parentSyntax is not null)
{
- // NOTE: due to bug https://github.com/dotnet/roslyn/issues/78042 this
- // is not using a local static function to evaluate this in the condition
- // of the while loop [Workaround: go back to "old" extension syntax...]
- var rawKind = parentSyntax.Kind();
- bool isAllowedKind
- = rawKind == SyntaxKind.ClassDeclaration
- || rawKind == SyntaxKind.StructDeclaration
- || rawKind == SyntaxKind.RecordDeclaration;
-
- if (!isAllowedKind)
+ if(!IsAllowedKind( parentSyntax ))
{
break;
}
@@ -90,7 +89,8 @@ bool isAllowedKind
keyword: parentSyntax.Keyword.ValueText,
name: parentSyntax.Identifier.ToString() + parentSyntax.TypeParameterList,
constraints: parentSyntax.ConstraintClauses.ToString(),
- children: parentClassInfo is null ? [] : [parentClassInfo]); // set the child link (null initially)
+ children: parentClassInfo is null ? [] : [ parentClassInfo ]
+ ); // set the child link (null initially)
// Move to the next outer type
parentSyntax = parentSyntax.Parent as TypeDeclarationSyntax;
@@ -98,6 +98,15 @@ bool isAllowedKind
// return a link to the outermost parent type
return parentClassInfo;
+
+ // local static function to test for allowed kinds
+ static bool IsAllowedKind( TypeDeclarationSyntax parentSyntax )
+ {
+ var rawKind = parentSyntax.Kind();
+ return rawKind == SyntaxKind.ClassDeclaration
+ || rawKind == SyntaxKind.StructDeclaration
+ || rawKind == SyntaxKind.RecordDeclaration;
+ }
}
}
}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/DebugAssert.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/DebugAssert.cs
new file mode 100644
index 0000000..437316c
--- /dev/null
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/DebugAssert.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Diagnostics;
+
+namespace Ubiquity.NET.CodeAnalysis.Utils
+{
+ /// Utility class to support Debug asserts
+ public static class DebugAssert
+ {
+ /// Tests if a structure size is < 16 bytes and generates a debug assertion if not
+ /// Type of the struct to test
+ ///
+ /// This uses a runtime debug assert as it isn't possible to know the size at compile time of a managed struct.
+ /// The `sizeof` doesn't apply for anything with a managed reference or a native pointer sized member
+ /// as such sizes depend on the actual runtime used.
+ ///
+ /// This function ONLY operates in a debug build. That is, this is the compiler will elide calls to this method
+ /// at the call site unless the "DEBUG" symbol is defined as it has a attached to it.
+ ///
+ ///
+ ///
+ [Conditional( "DEBUG" )]
+ public static void StructSizeOK( )
+ where T : struct
+ {
+ Debug.Assert( Unsafe.SizeOf() <= 16, $"{nameof( T )} size is > 16 bytes; Make it a class" );
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/DiagnosticInfo.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/DiagnosticInfo.cs
index 67ffa21..f7b4d3b 100644
--- a/src/Ubiquity.NET.CodeAnalysis.Utils/DiagnosticInfo.cs
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/DiagnosticInfo.cs
@@ -11,7 +11,8 @@ namespace Ubiquity.NET.CodeAnalysis.Utils
/// that is needed for caching. A is not, so this record bundles
/// the parameters needed for creation of one and defers the construction until needed.
///
- public sealed record DiagnosticInfo
+ public sealed class DiagnosticInfo
+ : IEquatable
{
#if !NET9_0_OR_GREATER
/// Initializes a new instance of the class.
@@ -38,17 +39,17 @@ public DiagnosticInfo(DiagnosticDescriptor descriptor, Location? location, param
{
Descriptor = descriptor;
Location = location;
- Params = msgArgs.ToImmutableArray();
+ Params = [ .. msgArgs ];
}
/// Gets the parameters for this diagnostic
- public EquatableArray Params { get; }
+ public ImmutableArray Params { get; }
/// Gets the descriptor for this diagnostic
public DiagnosticDescriptor Descriptor { get; }
- // Location is an abstract type but all derived types implement IEquatable where T is Location
- // Thus a location is equatable even though the base abstract type doesn't implement that interface.
+ // Microsoft.CodeAnalysis.Location is an abstract type but all derived types implement `IEquatable where T is Location`
+ // Thus, a location is equatable even though the base abstract type doesn't implement that interface.
/// Gets the location of the source of this diagnostic
public Location? Location { get; }
@@ -59,5 +60,37 @@ public Diagnostic CreateDiagnostic()
{
return Diagnostic.Create(Descriptor, Location, Params.ToArray());
}
+
+ ///
+ public bool Equals( DiagnosticInfo other )
+ {
+ return other is not null
+ && StructuralComparisons.StructuralEqualityComparer.Equals(Params, other.Params)
+ && Descriptor.Equals(other.Descriptor)
+ && ( ReferenceEquals(Location, other.Location)
+ || (Location is not null && Location.Equals(other.Location))
+ );
+ }
+
+ ///
+ public override bool Equals( object obj )
+ {
+ return obj is DiagnosticInfo other
+ && Equals( other );
+ }
+
+ ///
+ public override int GetHashCode( )
+ {
+ // sadly this will re-hash the hashcode computed for the structure, but there is no way
+ // to combine the result of a hash with other things. (The overload of Add(int) is private)
+ // The generic Add() will call the type's GetHashCode() and ignores the implementation of
+ // IStructuralEquatable.
+ return HashCode.Combine(
+ StructuralComparisons.StructuralEqualityComparer.GetHashCode( Params ),
+ Descriptor,
+ Location
+ );
+ }
}
}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/EquatableArray.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/EquatableArray.cs
index 29a5c8e..91964ca 100644
--- a/src/Ubiquity.NET.CodeAnalysis.Utils/EquatableArray.cs
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/EquatableArray.cs
@@ -14,6 +14,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+// Modified heavily to support IStructuralEquatable
+
namespace Ubiquity.NET.CodeAnalysis.Utils
{
/// Extensions for .
@@ -23,11 +25,22 @@ public static class EquatableArray
/// The type of items in the input array.
/// The input instance.
/// An instance from a given .
- public static EquatableArray AsEquatableArray(this ImmutableArray array)
+ public static EquatableArray AsEquatableArray( this ImmutableArray array )
+ where T : IEquatable
+ {
+ return array.IsDefault ? throw new ArgumentNullException( nameof( array ) )
+ : new( array );
+ }
+
+ /// Creates an instance from a given .
+ /// The type of items in the input array.
+ /// The input builder instance.
+ /// An instance from a given .
+ [Obsolete( "Use ImmutableArry instead" )]
+ public static EquatableArray AsEquatableArray( this ImmutableArray.Builder self )
where T : IEquatable
{
- return array.IsDefault ? throw new ArgumentNullException(nameof(array))
- : new(array);
+ return AsEquatableArray( self.ToImmutable() );
}
}
@@ -35,23 +48,31 @@ public static EquatableArray AsEquatableArray(this ImmutableArray array
/// An immutable, equatable array. This is equivalent to but with value equality of members support.
///
/// The type of values in the array.
+ ///
+ /// Use of this type should be limited to cases where it is the result of analysis itself. That is, when the array is a
+ /// member of some other equatable type, then it should use instead. The container should
+ /// use the support to implement it's . This type will enforce
+ /// structural comparison as it's implementation of equality checks. All forms of retrieving a hash code resolve to the
+ /// structural form. This ensures that the behavior is consistent and the array is cacheable.
+ ///
public readonly struct EquatableArray
: IEquatable>
, IEnumerable
+ , IStructuralEquatable
where T : IEquatable
{
///
/// The underlying array.
///
- private readonly T[]? array;
+ private readonly T[]? InnerArray;
///
/// Creates a new instance.
///
/// The input to wrap.
- public EquatableArray(ImmutableArray array)
+ public EquatableArray( ImmutableArray array )
{
- this.array = Unsafe.As, T[]?>(ref array);
+ InnerArray = Unsafe.As, T[]?>( ref array );
}
///
@@ -59,10 +80,10 @@ public EquatableArray(ImmutableArray array)
///
/// The index of the item to retrieve a reference to.
/// A reference to an item at a specified position within the array.
- public ref readonly T this[int index]
+ public ref readonly T this[ int index ]
{
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => ref AsImmutableArray().ItemRef(index);
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ get => ref AsImmutableArray().ItemRef( index );
}
///
@@ -70,51 +91,40 @@ public ref readonly T this[int index]
///
public bool IsEmpty
{
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
get => AsImmutableArray().IsEmpty;
}
/// Gets the length of the array
- public int Length => array?.Length ?? 0;
+ public int Length => InnerArray?.Length ?? 0;
- ///
- public bool Equals(EquatableArray array)
+ ///
+ public bool Equals( EquatableArray array )
{
- return AsSpan().SequenceEqual(array.AsSpan());
+ return StructuralComparisons.StructuralEqualityComparer.Equals( InnerArray );
}
- ///
- public override bool Equals([NotNullWhen(true)] object? obj)
+ ///
+ public override bool Equals( [NotNullWhen( true )] object? obj )
{
- return obj is EquatableArray array && Equals(this, array);
+ return obj is EquatableArray other
+ && Equals( other );
}
- ///
- public override int GetHashCode()
+ ///
+ public override int GetHashCode( )
{
- if (this.array is not T[] array)
- {
- return 0;
- }
-
- HashCode hashCode = default;
-
- foreach (T item in array)
- {
- hashCode.Add(item);
- }
-
- return hashCode.ToHashCode();
+ return StructuralComparisons.StructuralEqualityComparer.GetHashCode( InnerArray );
}
///
/// Gets an instance from the current .
///
/// The from the current .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public ImmutableArray AsImmutableArray()
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ public ImmutableArray AsImmutableArray( )
{
- return Unsafe.As>(ref Unsafe.AsRef(in array));
+ return Unsafe.As>( ref Unsafe.AsRef( in InnerArray ) );
}
///
@@ -122,16 +132,16 @@ public ImmutableArray AsImmutableArray()
///
/// The input instance.
/// An instance from a given .
- public static EquatableArray FromImmutableArray(ImmutableArray array)
+ public static EquatableArray FromImmutableArray( ImmutableArray array )
{
- return new(array);
+ return new( array );
}
///
/// Returns a wrapping the current items.
///
/// A wrapping the current items.
- public ReadOnlySpan AsSpan()
+ public ReadOnlySpan AsSpan( )
{
return AsImmutableArray().AsSpan();
}
@@ -140,46 +150,66 @@ public ReadOnlySpan AsSpan()
/// Copies the contents of this instance to a mutable array.
///
/// The newly instantiated array.
- public T[] ToArray()
+ public T[] ToArray( )
{
- return [.. AsImmutableArray()];
+ return [ .. AsImmutableArray() ];
}
///
/// Gets an value to traverse items in the current array.
///
/// An value to traverse items in the current array.
- public ImmutableArray.Enumerator GetEnumerator()
+ public ImmutableArray.Enumerator GetEnumerator( )
{
return AsImmutableArray().GetEnumerator();
}
- ///
- IEnumerator IEnumerable.GetEnumerator()
+ ///
+ IEnumerator IEnumerable.GetEnumerator( )
{
return ((IEnumerable)AsImmutableArray()).GetEnumerator();
}
- ///
- IEnumerator IEnumerable.GetEnumerator()
+ ///
+ IEnumerator IEnumerable.GetEnumerator( )
{
return ((IEnumerable)AsImmutableArray()).GetEnumerator();
}
+ ///
+ [SuppressMessage( "Style", "IDE0046:Convert to conditional expression", Justification = "Nested conditionals are not simpler" )]
+ bool IStructuralEquatable.Equals( object other, IEqualityComparer comparer )
+ {
+ if(other is not EquatableArray otherArray)
+ {
+ return false;
+ }
+
+ return InnerArray is null
+ ? ReferenceEquals(InnerArray, otherArray.InnerArray)
+ : ((IStructuralEquatable)InnerArray).Equals(other, comparer);
+ }
+
+ ///
+ int IStructuralEquatable.GetHashCode( IEqualityComparer comparer )
+ {
+ return StructuralComparisons.StructuralEqualityComparer.GetHashCode(InnerArray);
+ }
+
///
/// Implicitly converts an to .
///
/// An instance from a given .
- public static implicit operator EquatableArray(ImmutableArray array)
+ public static implicit operator EquatableArray( ImmutableArray array )
{
- return FromImmutableArray(array);
+ return FromImmutableArray( array );
}
///
/// Implicitly converts an to .
///
/// An instance from a given .
- public static implicit operator ImmutableArray(EquatableArray array)
+ public static implicit operator ImmutableArray( EquatableArray array )
{
return array.AsImmutableArray();
}
@@ -190,9 +220,9 @@ public static implicit operator ImmutableArray(EquatableArray array)
/// The first value.
/// The second value.
/// Whether and are equal.
- public static bool operator ==(EquatableArray left, EquatableArray right)
+ public static bool operator ==( EquatableArray left, EquatableArray right )
{
- return left.Equals(right);
+ return left.Equals( right );
}
///
@@ -201,9 +231,9 @@ public static implicit operator ImmutableArray(EquatableArray array)
/// The first value.
/// The second value.
/// Whether and are not equal.
- public static bool operator !=(EquatableArray left, EquatableArray right)
+ public static bool operator !=( EquatableArray left, EquatableArray right )
{
- return !left.Equals(right);
+ return !left.Equals( right );
}
}
}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/EquatableAttributeData.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/EquatableAttributeData.cs
new file mode 100644
index 0000000..deb0514
--- /dev/null
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/EquatableAttributeData.cs
@@ -0,0 +1,148 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Diagnostics;
+
+namespace Ubiquity.NET.CodeAnalysis.Utils
+{
+ /// Equatable form of
+ ///
+ ///
+ /// This CAPTURES only the portion of the attributes data that is relevant to source generators.
+ /// Specifically, it captures the full for the unnamed
+ /// constructor arguments and a dictionary for the named arguments. All of which is captured
+ /// in a manner that supports equality checks. These form the semantic set of properties
+ /// necessary to capture for an attribute.
+ ///
+ ///
+ ///
+ /// C# language specification §23.2.4 Attribute parameter types
+ ///
+ public class EquatableAttributeData
+ : IEquatable
+ {
+ /// Initializes a new instance of the class.
+ /// The to capture equatable information from
+ public EquatableAttributeData( AttributeData data )
+ {
+ PolyFillExceptionValidators.ThrowIfNull( data );
+
+ Name = data.GetNamespaceQualifiedName();
+ ConstructorArguments = [ .. data.ConstructorArguments.Select(e=>(StructurallyEquatableTypedConstant)e) ];
+ var namedArgs = data.NamedArguments.Select(kvp => new KeyValuePair(kvp.Key, (StructurallyEquatableTypedConstant)kvp.Value));
+ NamedArguments = namedArgs.ToImmutableDictionary();
+ }
+
+ /// Gets the full namespace qualified name for this attribute
+ public NamespaceQualifiedName Name { get; }
+
+ /// Gets the unnamed constructor arguments for this attribute
+ public ImmutableArray ConstructorArguments { get; }
+
+ /// Gets dictionary for the named arguments
+ public EquatableDictionary NamedArguments { get; } = [];
+
+ /// Gets the constant for a named argument
+ /// Name of the argument to fetch
+ /// Optional value for the named argument ( is false if isn't provided)
+ public Optional GetNamedArgValue( string argName )
+ {
+ return NamedArguments.TryGetValue( argName, out StructurallyEquatableTypedConstant typedConst )
+ ? new(typedConst)
+ : default;
+ }
+
+ /// Gets the named argument constant as a specified type
+ /// Type of the value to retrieve if present
+ /// Name of the attribute argument
+ /// for the value
+ ///
+ /// The name may not be specified in which case the result
+ /// will have not value ( is false). It is also
+ /// possible that it was specified AND that the value is null (if T is a nullable type
+ /// or a default instance if it is not.) Thus it is important to examine the return
+ /// to know if a value was specified that happens to be the default value for a type.
+ ///
+ public Optional GetNamedArgValue( string name )
+ {
+ var argInfo = GetNamedArgValue(name);
+ return !argInfo.HasValue
+ ? default
+ : new((T)argInfo.Value.Value!);
+ }
+
+ /// Gets the named argument constant as an of elements of specified type
+ /// Type of the value to retrieve if present
+ /// Name of the attribute argument
+ /// for the array of values
+ ///
+ /// The name may not be specified in which case the result
+ /// will have not value ( is false). It is also
+ /// possible that it was specified AND that the value is null (if T is a nullable type
+ /// or a default instance if it is not.) Thus it is important to examine the return
+ /// to know if a value was specified that happens to be the default value for a type.
+ ///
+ /// As nullability is NOT part of the type this method
+ /// assumes it is allowed. Therefore, the resulting array may contain null values if
+ /// that is what was provided to the attribute in the original source.
+ ///
+ ///
+ public Optional> GetNamedArgValueArray( string argName )
+ {
+ var elementType = typeof( TElement );
+ if(elementType.IsArray)
+ {
+ throw new InvalidOperationException("Arrays of arrays not supported. TElement must be a scalar.");
+ }
+
+ if(!NamedArguments.TryGetValue( argName, out StructurallyEquatableTypedConstant typedConst )
+ || typedConst.IsNull
+ || typedConst.Kind != TypedConstantKind.Array
+ )
+ {
+ Debug.WriteLineIf( !typedConst.IsNull && typedConst.Kind != TypedConstantKind.Array, $"Non array named attribute; retrieved as array! '{argName}'" );
+ return default;
+ }
+
+ // Nullability of TElement is not known, if the typed constant indicates it is a null value
+ // then that is what it is. This does not skip such a thing nor attempt to validate nullability.
+#pragma warning disable CS8601 // Possible null reference assignment.
+#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
+ return new( [ .. typedConst.Values.Select( static tc => (TElement)tc.Value ) ] );
+#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
+#pragma warning restore CS8601 // Possible null reference assignment.
+ }
+
+ ///
+ public bool Equals( EquatableAttributeData other )
+ {
+ return other is not null
+ && Name == other.Name
+ && StructuralComparisons.StructuralEqualityComparer.Equals(ConstructorArguments, other.ConstructorArguments)
+ && StructuralComparisons.StructuralEqualityComparer.Equals(NamedArguments, other.NamedArguments);
+ }
+
+ ///
+ public override int GetHashCode( )
+ {
+ return HashCode.Combine(
+ Name,
+ StructuralComparisons.StructuralEqualityComparer.GetHashCode( ConstructorArguments ),
+ StructuralComparisons.StructuralEqualityComparer.GetHashCode( NamedArguments )
+ );
+ }
+
+ ///
+ public override bool Equals( object obj )
+ {
+ return obj is EquatableAttributeData other
+ && Equals( other );
+ }
+
+ [SuppressMessage( "Usage", "CA2225:Operator overloads have named alternates", Justification = "Implicit cast for public constructor" )]
+ public static implicit operator EquatableAttributeData( AttributeData data )
+ {
+ return new( data );
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/EquatableAttributeDataCollection.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/EquatableAttributeDataCollection.cs
new file mode 100644
index 0000000..908038e
--- /dev/null
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/EquatableAttributeDataCollection.cs
@@ -0,0 +1,144 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CodeAnalysis.Utils
+{
+ /// Keyed collection of keyed by
+ public class EquatableAttributeDataCollection
+ : IEnumerable
+ , IReadOnlyCollection
+ , IEquatable
+ , IStructuralEquatable
+ {
+ /// Initializes a new instance of the class.
+ /// Attribute information for this collection
+ ///
+ /// This will iterate over the collection to get the
+ /// as a key for the item in the collection.
+ ///
+ public EquatableAttributeDataCollection( IEnumerable attributes )
+ : this( ToDictionary( attributes ) )
+ {
+ }
+
+ /// Initializes a new instance of the class.
+ /// Dictionary of attributes for the collection
+ public EquatableAttributeDataCollection( EquatableDictionary attributes )
+ {
+ InnerDictionary = attributes;
+ }
+
+ /// Gets the value for a key
+ /// Name of the attribute data
+ /// Attribute data for the key
+ /// is null
+ /// does not match any entry in this collection
+ [SuppressMessage( "Design", "CA1043:Use Integral Or String Argument For Indexers", Justification = "Neither string nor integer is the key type" )]
+ public EquatableAttributeData this[ NamespaceQualifiedName key ] => InnerDictionary[key];
+
+ ///
+ public int Count => InnerDictionary.Count;
+
+ ///
+ public IEnumerator GetEnumerator( )
+ {
+ return Values.GetEnumerator();
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator( )
+ {
+ return GetEnumerator();
+ }
+
+ #region Equatable implementation
+
+ ///
+ public bool Equals( EquatableAttributeDataCollection other )
+ {
+ return StructuralComparisons.StructuralEqualityComparer.Equals(this, other);
+ }
+
+ ///
+ public override bool Equals( object obj )
+ {
+ return obj is not null
+ && obj is EquatableAttributeDataCollection collection
+ && Equals( collection );
+ }
+
+ ///
+ public override int GetHashCode( )
+ {
+ // CONSIDER: Optimize this to cache the hash code.
+ HashCode retVal = default;
+ foreach(var item in this)
+ {
+ retVal.Add( item );
+ }
+
+ return retVal.ToHashCode();
+ }
+ #endregion
+
+ /// Gets a sequence of the names of all values.
+ ///
+ /// These names are keys for the values used in and
+ /// .
+ ///
+ public IEnumerable Keys => InnerDictionary.Keys;
+
+ /// Gets a sequence of all the values in this collection
+ ///
+ /// This is no different that using the implemented by this instance
+ /// directly. It is here to support common dictionary functionality.
+ ///
+ public IEnumerable Values => InnerDictionary.Values;
+
+ /// Tries to get the value for a given
+ /// The type name for the attribute
+ /// Resulting attribute if found (default constructed if not)
+ /// true if the attribute is found in this collection
+ public bool TryGetValue( NamespaceQualifiedName key, [MaybeNullWhen( false )] out EquatableAttributeData item )
+ {
+ return InnerDictionary.TryGetValue( key, out item );
+ }
+
+ [SuppressMessage( "Usage", "CA2225:Operator overloads have named alternates", Justification = "Simple wrapper over public constructor" )]
+ public static implicit operator EquatableAttributeDataCollection( ImmutableArray attributes )
+ {
+ return new EquatableAttributeDataCollection( attributes );
+ }
+
+ private readonly EquatableDictionary InnerDictionary;
+
+ private static EquatableDictionary ToDictionary( IEnumerable attributes )
+ {
+ var bldr = ImmutableDictionary.CreateBuilder();
+ if(attributes is not null)
+ {
+ foreach(var attr in attributes)
+ {
+ bldr.Add( attr.Name, attr );
+ }
+ }
+
+ return bldr.ToImmutable();
+ }
+
+ ///
+ bool IStructuralEquatable.Equals( object other, IEqualityComparer comparer )
+ {
+ bool retVal = other is EquatableAttributeDataCollection otherCollection
+ && ((IStructuralEquatable)InnerDictionary).Equals( otherCollection.InnerDictionary, comparer );
+
+ return retVal;
+ }
+
+ ///
+ int IStructuralEquatable.GetHashCode( IEqualityComparer comparer )
+ {
+ return ((IStructuralEquatable)InnerDictionary).GetHashCode( comparer );
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/EquatableDictionary.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/EquatableDictionary.cs
new file mode 100644
index 0000000..c0ef644
--- /dev/null
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/EquatableDictionary.cs
@@ -0,0 +1,223 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CodeAnalysis.Utils
+{
+ /// An equatable, immutable, dictionary
+ /// Type of keys in the dictionary (Must be for TKey
+ /// Type of keys in the dictionary (Must be for TValue
+ ///
+ /// This is a struct that has no additional size beyond that of the wrapped reference to the underlying dictionary. It
+ /// merely adds equatability checks to the dictionary for use in Roslyn source generator caching.
+ ///
+ public readonly struct EquatableDictionary
+ : IImmutableDictionary
+ , IEquatable>
+ , IStructuralEquatable
+ where TKey : IEquatable
+ where TValue : IEquatable
+ {
+ /// Initializes a new instance of the struct.
+ /// Dictionary to wrap
+ public EquatableDictionary( IImmutableDictionary dictionaryToWrap )
+ {
+ InnerDictionary = dictionaryToWrap;
+ }
+
+ #region Equatability
+
+ ///
+ public override int GetHashCode( )
+ {
+ return InnerDictionary.GetHashCode();
+ }
+
+ ///
+ public override bool Equals( object obj )
+ {
+ return obj is EquatableDictionary dictionary
+ && Equals( dictionary );
+ }
+
+ ///
+ public bool Equals( EquatableDictionary other )
+ {
+ return ((IStructuralEquatable)this).Equals( other, EqualityComparer>.Default);
+ }
+
+ public static bool operator ==( EquatableDictionary left, EquatableDictionary right )
+ {
+ return left.Equals( right );
+ }
+
+ public static bool operator !=( EquatableDictionary left, EquatableDictionary right )
+ {
+ return !(left == right);
+ }
+
+ ///
+ bool IStructuralEquatable.Equals( object other, IEqualityComparer comparer )
+ {
+ if(other is not EquatableDictionary otherItem)
+ {
+ return false;
+ }
+
+ using var thisIterator = GetEnumerator();
+ using var otherIterator = otherItem.GetEnumerator();
+
+ bool mismatchFound = false;
+ bool lhsValid = thisIterator.MoveNext(); // move to first entry; if any
+ bool rhsValid = otherIterator.MoveNext();
+ while(lhsValid && rhsValid)
+ {
+ // NOTE: KeyValuePair is NOT equatable, it's just a struct
+ // if one or both of the pair is a managed reference then it
+ // is compared with what amount to reference equality. Which is
+ // NOT what is desired here.
+ if( !comparer.Equals( thisIterator.Current.Key, otherIterator.Current.Key )
+ || !comparer.Equals( thisIterator.Current.Value, otherIterator.Current.Value )
+ )
+ {
+ mismatchFound = true;
+ break; // stop loop as soon as mismatch is found
+ }
+
+ lhsValid = thisIterator.MoveNext();
+ rhsValid = otherIterator.MoveNext();
+ }
+
+ // Only equal if no mismatch found AND both sequences are the same length
+ // (that is, if one sequence is a super set of the other, it is not equal!
+ return !mismatchFound && (lhsValid == rhsValid);
+ }
+
+ ///
+ int IStructuralEquatable.GetHashCode( IEqualityComparer comparer )
+ {
+ HashCode hashCode = default;
+ foreach( var kvp in this)
+ {
+ hashCode.Add(kvp, comparer as IEqualityComparer> );
+ }
+
+ return hashCode.ToHashCode();
+ }
+ #endregion
+
+ #region IImmutableDictionary interface implementations through wrapped dictionary
+
+ ///
+ public TValue this[ TKey key ] => ((IReadOnlyDictionary)InnerDictionary)[ key ];
+
+ ///
+ public IEnumerable Keys => InnerDictionary.Keys;
+
+ ///
+ public IEnumerable Values => InnerDictionary.Values;
+
+ ///
+ public int Count => InnerDictionary.Count;
+
+ ///
+ public IImmutableDictionary Add( TKey key, TValue value )
+ {
+ return new EquatableDictionary(InnerDictionary.Add( key, value ));
+ }
+
+ ///
+ public IImmutableDictionary AddRange( IEnumerable> pairs )
+ {
+ return new EquatableDictionary( InnerDictionary.AddRange( pairs ) );
+ }
+
+ ///
+ public IImmutableDictionary Clear( )
+ {
+ return new EquatableDictionary( InnerDictionary.Clear() );
+ }
+
+ ///
+ public bool Contains( KeyValuePair pair )
+ {
+ return InnerDictionary.Contains( pair );
+ }
+
+ ///
+ public bool ContainsKey( TKey key )
+ {
+ return InnerDictionary.ContainsKey( key );
+ }
+
+ ///
+ public IEnumerator> GetEnumerator( )
+ {
+ return InnerDictionary.GetEnumerator();
+ }
+
+ ///
+ public IImmutableDictionary Remove( TKey key )
+ {
+ return InnerDictionary.Remove( key );
+ }
+
+ ///
+ public IImmutableDictionary RemoveRange( IEnumerable keys )
+ {
+ return InnerDictionary.RemoveRange( keys );
+ }
+
+ ///
+ public IImmutableDictionary SetItem( TKey key, TValue value )
+ {
+ return InnerDictionary.SetItem( key, value );
+ }
+
+ ///
+ public IImmutableDictionary SetItems( IEnumerable> items )
+ {
+ return InnerDictionary.SetItems( items );
+ }
+
+ ///
+ public bool TryGetKey( TKey equalKey, out TKey actualKey )
+ {
+ return InnerDictionary.TryGetKey( equalKey, out actualKey );
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator( )
+ {
+ return ((IEnumerable)InnerDictionary).GetEnumerator();
+ }
+
+ ///
+ public bool TryGetValue( TKey key, out TValue value )
+ {
+ value = default!;
+ if(!InnerDictionary.TryGetValue( key, out TValue? foundValue ))
+ {
+ return false;
+ }
+
+ value = foundValue;
+ return true;
+ }
+
+ #endregion
+
+ [SuppressMessage( "Usage", "CA2225:Operator overloads have named alternates", Justification = "Implicit cast for public constructor" )]
+ public static implicit operator EquatableDictionary( ImmutableDictionary dictionaryToWrap )
+ {
+ return new(dictionaryToWrap);
+ }
+
+ [SuppressMessage( "Usage", "CA2225:Operator overloads have named alternates", Justification = "Implicit cast for public constructor" )]
+ public static implicit operator EquatableDictionary( ImmutableSortedDictionary dictionaryToWrap )
+ {
+ return new( dictionaryToWrap );
+ }
+
+ private readonly IImmutableDictionary InnerDictionary;
+ }
+}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/GlobalNamespaceImports.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/GlobalNamespaceImports.cs
index 746eb94..9587eb7 100644
--- a/src/Ubiquity.NET.CodeAnalysis.Utils/GlobalNamespaceImports.cs
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/GlobalNamespaceImports.cs
@@ -34,3 +34,5 @@ set of namespaces that is NOT consistent or controlled by the developer. THAT is
global using Microsoft.CodeAnalysis.CSharp.Syntax;
global using Microsoft.CodeAnalysis.Diagnostics;
global using Microsoft.CodeAnalysis.Text;
+
+global using Ubiquity.NET.Extensions;
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedName.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedName.cs
new file mode 100644
index 0000000..83e4f75
--- /dev/null
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedName.cs
@@ -0,0 +1,177 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CodeAnalysis.Utils
+{
+ /// Simple record to represent a namespace qualified name as a sequence of strings
+ ///
+ /// This is useful for matching names of attributes where comparison on the simple name is performed first. This filters
+ /// out anything without a correct simple name before taking on the cost of establishing the full namespace hierarchical
+ /// name.
+ ///
+ /// This does NOT support nested names. The syntax, and even support, of that is language dependent.
+ /// Additionally, there is no provision to represent the transition to a nested type as the elements of the sequence
+ /// are just strings.
+ ///
+ ///
+ public class NamespaceQualifiedName
+ : IEquatable
+ , IEquatable
+ , IFormattable
+ {
+ /// Initializes a new instance of the class.
+ /// sequence of namespace names (outermost to innermost)
+ /// Unqualified name of the symbol
+ public NamespaceQualifiedName(IEnumerable namespaceNames, string simpleName )
+ {
+ PolyFillExceptionValidators.ThrowIfNull(namespaceNames);
+ PolyFillExceptionValidators.ThrowIfNullOrWhiteSpace(simpleName);
+
+ SimpleName = simpleName;
+ NamespaceNames = [ .. namespaceNames.Select( s => ValidateNamespacePart(s) ) ];
+
+ static string ValidateNamespacePart( string s, [CallerArgumentExpression(nameof(s))] string? exp = null )
+ {
+ PolyFillExceptionValidators.ThrowIfNullOrWhiteSpace( s, exp );
+ return s;
+ }
+ }
+
+ /// Gets the sequence of the namespace names starting with the outermost namespace moving inwards
+ public ImmutableArray NamespaceNames { get; }
+
+ /// Gets the simple (unqualified) name of the symbol
+ public string SimpleName { get; }
+
+ /// Gets a string for the namespaces instance
+ ///
+ /// This essentially returns an enumerable sequence that is the joining of
+ /// with "." as the delimiter.
+ ///
+ public string Namespace => string.Join( ".", NamespaceNames );
+
+ /// Gets the full sequence of the names for this instance
+ ///
+ /// This essentially returns an enumerable sequence that is the concatenation of
+ /// with the to form a sequence
+ /// that contains the full name.
+ ///
+ public IEnumerable FullNameParts
+ {
+ get
+ {
+ foreach(string name in NamespaceNames)
+ {
+ yield return name;
+ }
+
+ yield return SimpleName;
+ }
+ }
+
+ ///
+ public bool Equals( NamespaceQualifiedName other )
+ {
+ return FullNameParts.SequenceEqual( other.FullNameParts );
+ }
+
+ ///
+ public override bool Equals( object obj )
+ {
+ return obj is NamespaceQualifiedName other
+ && Equals( other );
+ }
+
+ ///
+ public override int GetHashCode( )
+ {
+ // CONSIDER: Lazy create this so that overhead is paid only once.
+
+ HashCode hashCode = default;
+
+ foreach(string item in NamespaceNames)
+ {
+ hashCode.Add( item );
+ }
+
+ hashCode.Add( SimpleName );
+
+ return hashCode.ToHashCode();
+ }
+
+ /// Equality operator to test two names for equality
+ /// left name to test
+ /// Right name to test
+ /// true if the names are equal
+ public static bool operator ==( NamespaceQualifiedName left, NamespaceQualifiedName right )
+ {
+ return left.Equals( right );
+ }
+
+ /// Equality operator to test two names for inequality
+ /// left name to test
+ /// Right name to test
+ /// true if the names are NOT equal; false otherwise
+ public static bool operator !=( NamespaceQualifiedName left, NamespaceQualifiedName right )
+ {
+ return !(left == right);
+ }
+
+ /// Compares this name with that of a
+ /// The type to compare the name to
+ /// true if the name of matches this name
+ public bool Equals( Type other )
+ {
+ return ToString() == other.FullName;
+ }
+
+ /// Gets the string representation of the full namespace using '.' as the delimiter
+ /// Full namespace qualified name as a string (with a global prefix or an alias if available)
+ ///
+ /// This is just a tail call to with "AG" as the format.
+ /// That is, the default formatting is to provide an alias if possible and if not available use a global prefixed
+ /// name. If any other behavior is desired then is available
+ /// to allow formatting as needed. (NOTE: That version is called when formatting a string with a specifier
+ /// such as var s = $"{MyNamespaceQualifiedName:R}";
+ ///
+ public override string ToString( )
+ {
+ return ToString("R", null);
+ }
+
+ /// Formats this instance according to the args
+ /// Format string for this instance (see remarks)
+ /// [ignored]
+ /// Formatted string representation of this instance
+ ///
+ /// The supported values for are:
+ ///
+ /// ValueDescription
+ /// - AFormat as a language specific alias if possible
+ /// - GFormat with a language specific global prefix.
+ /// - AGFormat with a language specific alias if possible, otherwise include a global prefix.
+ /// - RThe raw full name without any qualifications
+ ///
+ ///
+ /// is not supported
+ [SuppressMessage( "Style", "IDE0046:Convert to conditional expression", Justification = "Result is anything but simpler" )]
+ public string ToString( string format, IFormatProvider? formatProvider )
+ {
+ // default to the C# formatter unless specified.
+ formatProvider ??= NamespaceQualifiedNameFormatter.CSharp;
+
+ // if no custom formatter is available, then just produce the raw name.
+ var customFormatter = (ICustomFormatter)formatProvider.GetFormat(typeof(ICustomFormatter));
+ return customFormatter is null
+ ? string.Join(".", FullNameParts)
+ : customFormatter.Format(format, this, formatProvider);
+ }
+
+ /// Implicit conversion to a string (Shorthand for calling
+ /// Name to convert
+ public static implicit operator string( NamespaceQualifiedName self )
+ {
+ return self.ToString();
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedNameFormatter.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedNameFormatter.cs
new file mode 100644
index 0000000..e70c77b
--- /dev/null
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedNameFormatter.cs
@@ -0,0 +1,138 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CodeAnalysis.Utils
+{
+ /// Custom formatter for
+ public class NamespaceQualifiedNameFormatter
+ : IFormatProvider
+ , ICustomFormatter
+ {
+ /// Initializes a new instance of the class.
+ /// Global prefix for the language this formatter will produce
+ /// Map of aliases for the language (Key is the full namespace name of a type, the values are the alias)
+ public NamespaceQualifiedNameFormatter( string globalPrefix, IReadOnlyDictionary aliasMap )
+ {
+ GlobalPrefix = globalPrefix ?? string.Empty;
+ AliasMap = aliasMap ?? throw new ArgumentNullException(nameof(aliasMap));
+ }
+
+ /// Gets the global prefix for the language formatting (example: "global::")
+ public string GlobalPrefix { get; }
+
+ /// Gets the map of type aliases this formatter uses to format an alias
+ ///
+ /// The keys for this map are a fully qualified type names and the values are the aliases
+ /// used in place of the full name.
+ ///
+ public IReadOnlyDictionary AliasMap { get; }
+
+ ///
+ public string Format( string format, object arg, IFormatProvider? formatProvider )
+ {
+ return arg is not NamespaceQualifiedName self
+ ? string.Empty
+ : Format(format, self, formatProvider);
+ }
+
+ /// Formats this instance according to the args
+ /// Format string for this instance (see remarks)
+ /// The value to format
+ /// [ignored]
+ /// Formatted string representation of this instance
+ ///
+ /// The supported values for are:
+ ///
+ /// ValueDescription
+ /// - AFormat as a language specific alias if possible
+ /// - GFormat with a language specific global prefix.
+ /// - AGFormat with a language specific alias if possible, otherwise include a global prefix.
+ /// - RThe raw full name without any qualifications
+ ///
+ ///
+ /// is not supported
+ public string Format( string format, NamespaceQualifiedName arg, IFormatProvider? formatProvider )
+ {
+ // default to global prefix or alias if not specified.
+ format ??= "R";
+
+ string rawName = string.Join( ".", arg.FullNameParts );
+ if(format == "R")
+ {
+ return rawName;
+ }
+
+ // Try an alias first as that might be the return value
+ if((format == "A" || format == "AG") && TryFormatLanguageAlias( rawName, out string? alias ))
+ {
+ return alias;
+ }
+
+ // not an alias so, try global prefix if requested.
+ // The only reason that won't add a prefix is if the GlobalPrefix is null, empty or all whitespace.
+ // In that case (or if the prefix isn't requested), the full name is returned.
+ return (format == "G" || format == "AG") && TryPrefixGlobal( rawName, out string? prefixedName )
+ ? prefixedName
+ : rawName;
+ }
+
+ ///
+ public object? GetFormat( Type formatType )
+ {
+ return formatType == typeof( ICustomFormatter )
+ ? this
+ : null;
+ }
+
+ private bool TryFormatLanguageAlias( string fullName, [MaybeNullWhen( false )] out string alias )
+ {
+ return AliasMap.TryGetValue( fullName, out alias );
+ }
+
+ private bool TryPrefixGlobal( string fullName, [MaybeNullWhen( false )] out string prefixedName )
+ {
+ prefixedName = null;
+ if(string.IsNullOrWhiteSpace( GlobalPrefix ))
+ {
+ return false;
+ }
+
+ prefixedName = $"{GlobalPrefix}{fullName}";
+ return true;
+ }
+
+ /// Gets a formatter for the C# language
+ public static NamespaceQualifiedNameFormatter CSharp { get; }
+ = new NamespaceQualifiedNameFormatter(
+ "global::",
+ /* see: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/built-in-types */
+ new Dictionary()
+ {
+ [ "System.Boolean" ] = "bool",
+ [ "System.Byte" ] = "byte",
+ [ "System.SByte" ] = "sbyte",
+ [ "System.Char" ] = "char",
+ [ "System.Decimal" ] = "decimal",
+ [ "System.Double" ] = "double",
+ [ "System.Single" ] = "float",
+ [ "System.Int32" ] = "int",
+ [ "System.UInt32" ] = "uint",
+
+ // These aren't quite an alias in C# 9 & 10, but C# 11 made them FULL aliases as used here
+ [ "System.IntPtr" ] = "nint",
+ [ "System.UIntPtr" ] = "nuint",
+
+ [ "System.Int64" ] = "long",
+ [ "System.UInt64" ] = "ulong",
+ [ "System.Int16" ] = "short",
+ [ "System.UInt16" ] = "ushort",
+
+ [ "System.Object" ] = "object",
+ [ "System.String" ] = "string",
+ [ "System.Delegate" ] = "delegate",
+
+ // ["????"] = "dynamic", // No way to map in this direction
+ }
+ );
+ }
+}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/NestedClassName.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/NestedClassName.cs
index 114c201..b371cf4 100644
--- a/src/Ubiquity.NET.CodeAnalysis.Utils/NestedClassName.cs
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/NestedClassName.cs
@@ -20,8 +20,8 @@ public sealed class NestedClassName
///
/// is normally one of ("class", "struct", "interface", "record [class|struct]?").
///
- public NestedClassName(string keyword, string name, string constraints, params NestedClassName[] children)
- : this( keyword, name, constraints, (IEnumerable)children)
+ public NestedClassName( string keyword, string name, string constraints, params NestedClassName[] children )
+ : this( keyword, name, constraints, (IEnumerable)children )
{
}
@@ -33,7 +33,7 @@ public NestedClassName(string keyword, string name, string constraints, params N
///
/// is normally one of ("class", "struct", "interface", "record [class|struct]?").
///
- public NestedClassName(string keyword, string name, string constraints, IEnumerable children)
+ public NestedClassName( string keyword, string name, string constraints, IEnumerable children )
#else
/// Initializes a new instance of the class.
/// Keyword for this declaration
@@ -49,11 +49,11 @@ public NestedClassName(string keyword, string name, string constraints, params I
Keyword = keyword;
Name = name;
Constraints = constraints;
- Children = children.ToImmutableArray().AsEquatableArray();
+ Children = [ .. children ];
}
/// Gets child nested types
- public EquatableArray Children { get; }
+ public ImmutableArray Children { get; }
/// Gets the keyword for this type
///
@@ -68,7 +68,7 @@ public NestedClassName(string keyword, string name, string constraints, params I
public string Constraints { get; }
/// Gets a value indicating whether this name contains constraints
- public bool HasConstraints => !string.IsNullOrWhiteSpace(Constraints);
+ public bool HasConstraints => !string.IsNullOrWhiteSpace( Constraints );
/// Compares this instance with another
/// Value to compare this instance with
@@ -78,34 +78,41 @@ public NestedClassName(string keyword, string name, string constraints, params I
/// the actual depth is statistically rather small and nearly always 0 (Children is empty).
/// Deeply nested type declarations is a VERY rare anti-pattern so not a real world problem.
///
- public bool Equals(NestedClassName other)
+ [SuppressMessage( "Style", "IDE0046:Convert to conditional expression", Justification = "NOT simpler" )]
+ public bool Equals( NestedClassName other )
{
- if (other == null)
+ if(other is null)
{
return false;
}
- if (ReferenceEquals(this, other))
+ if(ReferenceEquals( this, other ))
{
return true;
}
- // NOTE: This is a recursive O(n) operation!
- return Equals(Children, other.Children)
- && Name.Equals( other.Name, StringComparison.Ordinal )
- && Constraints.Equals( other.Constraints, StringComparison.Ordinal );
+ return Keyword == other.Keyword
+ && Name == other.Name
+ && Constraints == other.Constraints
+ && StructuralComparisons.StructuralEqualityComparer.Equals( Children, other.Children );
}
///
- public override bool Equals(object obj)
+ public override bool Equals( object obj )
{
- return obj is NestedClassName parentClass && Equals(parentClass);
+ return obj is NestedClassName other
+ && Equals( other );
}
///
- public override int GetHashCode()
+ public override int GetHashCode( )
{
- return HashCode.Combine(Children, Keyword, Name, Constraints);
+ return HashCode.Combine(
+ Keyword,
+ Name,
+ Constraints,
+ StructuralComparisons.StructuralEqualityComparer.GetHashCode( Children )
+ );
}
}
}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/Result.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/Result.cs
index 48258b4..020d7b9 100644
--- a/src/Ubiquity.NET.CodeAnalysis.Utils/Result.cs
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/Result.cs
@@ -78,7 +78,7 @@ public Result(T? value, ImmutableArray diagnostics)
/// Gets the diagnostics produced for this result (if any)
/// This may provide an empty array but is never
- public EquatableArray Diagnostics { get; init; } = ImmutableArray.Empty;
+ public ImmutableArray Diagnostics { get; init; } = [];
/// Gets a value indicating whether this result contains any diagnostics
/// This is a shorthand for testing the length of the property
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/StructuralTypedConstantComparer.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/StructuralTypedConstantComparer.cs
new file mode 100644
index 0000000..65ee204
--- /dev/null
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/StructuralTypedConstantComparer.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CodeAnalysis.Utils
+{
+ /// Performs structural equality on a
+ ///
+ /// While implements it fails to
+ /// account for structural equatability of the value, that is, it is ONLY a shallow equality
+ /// check and that isn't valid for a source Roslyn component that is collecting data that
+ /// requires caching.
+ ///
+ public class StructuralTypedConstantComparer
+ : IEqualityComparer
+ , IEqualityComparer
+ {
+ /// Performs structural equality of two values
+ /// First comparand
+ /// Second comparand
+ /// true if and are structurally equal
+ [SuppressMessage( "Style", "IDE0046:Convert to conditional expression", Justification = "Nested conditional is not simpler" )]
+ public bool Equals( TypedConstant x, TypedConstant y )
+ {
+ // handle fast checks first
+ if( (x.IsNull != y.IsNull) || (x.Kind != y.Kind) )
+ {
+ return false;
+ }
+
+ if(!SymbolEqualityComparer.Default.Equals( x.Type, y.Type ))
+ {
+ return false;
+ }
+
+ // Due to how the properties are not simple accessors for the fields
+ // this has to use distinct computations of equality for arrays vs. scalars
+ bool retVal = x.Kind == TypedConstantKind.Array
+ ? StructuralComparisons.StructuralEqualityComparer.Equals( x.Values, y.Values )
+ : StructuralComparisons.StructuralEqualityComparer.Equals( x.Value, y.Value );
+
+ return retVal;
+ }
+
+ ///
+ public int GetHashCode( TypedConstant obj )
+ {
+ return obj.Kind == TypedConstantKind.Array
+ ? HashCode.Combine(
+ obj.Kind,
+ SymbolEqualityComparer.IncludeNullability.GetHashCode( obj.Type ),
+ StructuralComparisons.StructuralEqualityComparer.GetHashCode( obj.Values )
+ )
+ : HashCode.Combine(
+ obj.Kind,
+ SymbolEqualityComparer.IncludeNullability.GetHashCode( obj.Type ),
+ StructuralComparisons.StructuralEqualityComparer.GetHashCode( obj.Value )
+ );
+ }
+
+ /// Determines if and are both and structurally equal
+ /// First comparand
+ /// Second comparand
+ /// true if if and are both and structurally equal; false if not
+ public new bool Equals( object x, object y )
+ {
+ return x is TypedConstant lhs
+ && y is TypedConstant rhs
+ && Equals( lhs, rhs );
+ }
+
+ ///
+ public int GetHashCode( object obj )
+ {
+ return obj is TypedConstant value
+ ? GetHashCode( value )
+ : obj?.GetHashCode() ?? 0;
+ }
+
+ /// Gets the default instance of this comparer
+ public static StructuralTypedConstantComparer Default { get; } = new();
+ }
+}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/StructurallyEquatableTypedConstant.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/StructurallyEquatableTypedConstant.cs
new file mode 100644
index 0000000..5af2580
--- /dev/null
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/StructurallyEquatableTypedConstant.cs
@@ -0,0 +1,101 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CodeAnalysis.Utils
+{
+ /// Wrapper for that implements structural (deep) equality
+ ///
+ /// While implements it does so with a shallow
+ /// depth if indicates an array. Use of this, type expresses intent
+ /// to use structural equality (deep) semantics. It is intentionally designed where the size is the
+ /// same as it is just functionality that is different.
+ ///
+ public readonly struct StructurallyEquatableTypedConstant
+ : IEquatable
+ {
+ /// Initializes a new instance of the struct.
+ /// to wrap
+ public StructurallyEquatableTypedConstant(TypedConstant other)
+ {
+ InnerConst = other;
+ }
+
+ ///
+ public TypedConstantKind Kind => InnerConst.Kind;
+
+ ///
+ public ITypeSymbol? Type => InnerConst.Type;
+
+ ///
+ public bool IsNull => InnerConst.IsNull;
+
+ ///
+ public object? Value => InnerConst.Value;
+
+ /// Gets the value for a array.
+ ///
+ /// ImmutableArray if was passed as the array value;
+ /// can be used to check for this.
+ ///
+ public ImmutableArray Values => [ .. InnerConst.Values.Select(e=> new StructurallyEquatableTypedConstant(e)) ];
+
+ /// Test if this instance is structurally equal to
+ /// Comparand
+ /// true if instance is structurally equal to ; false if not
+ public bool Equals( StructurallyEquatableTypedConstant other )
+ {
+ return StructuralTypedConstantComparer.Default.Equals(InnerConst, other.InnerConst);
+ }
+
+ /// Test if this instance is structurally equal to
+ /// Comparand
+ ///
+ /// true if obj is and this instance is structurally equal to ; false if not.
+ ///
+ public override bool Equals( object obj )
+ {
+ return obj is StructurallyEquatableTypedConstant other
+ && Equals( other );
+ }
+
+ ///
+ public override int GetHashCode( )
+ {
+ return StructuralTypedConstantComparer.Default.GetHashCode(InnerConst);
+ }
+
+ public static bool operator ==( StructurallyEquatableTypedConstant left, StructurallyEquatableTypedConstant right )
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=( StructurallyEquatableTypedConstant left, StructurallyEquatableTypedConstant right )
+ {
+ return !(left == right);
+ }
+
+ /// Gets the wrapped
+ /// The inner
+ ///
+ /// This is useful for getting access to extension methods for
+ /// that are otherwise unrelated to equality.
+ ///
+ public TypedConstant ToTypedConstant()
+ {
+ return InnerConst;
+ }
+
+ private readonly TypedConstant InnerConst;
+
+ [SuppressMessage( "Usage", "CA2225:Operator overloads have named alternates", Justification = "Simple alternate for existing constructor" )]
+ public static implicit operator StructurallyEquatableTypedConstant( TypedConstant other )
+ {
+ return new( other );
+ }
+
+ public static explicit operator TypedConstant( StructurallyEquatableTypedConstant other )
+ {
+ return other.ToTypedConstant();
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/SymbolExtensions.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/SymbolExtensions.cs
new file mode 100644
index 0000000..c2c980a
--- /dev/null
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/SymbolExtensions.cs
@@ -0,0 +1,92 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CodeAnalysis.Utils
+{
+ /// Utility class to host extensions for
+ public static class SymbolExtensions
+ {
+ /// Tries to find the first attribute with a matching name for an
+ /// Symbol to test
+ /// Sequence of valid names of attributes to match
+ /// for the attribute
+ /// is null
+ ///
+ /// As an optimization, this does not test the full namespace of the attribute until a match
+ /// of the simple name is found.
+ ///
+ public static AttributeData? FindFirstMatchingAttribute( this ISymbol self, IEnumerable names )
+ {
+ foreach(var attribute in MatchingAttributes( self, names ))
+ {
+ return attribute;
+ }
+
+ return null;
+ }
+
+ /// Tries to find the attributes with a matching name for an
+ /// Symbol to test
+ /// Sequence of valid names of attributes to match
+ /// for the attribute
+ /// is null
+ ///
+ /// As an optimization, this does not test the full namespace of the attribute until a match
+ /// of the simple name is found.
+ ///
+ public static ImmutableArray MatchingAttributes( this ISymbol self, IEnumerable names )
+ {
+ if(self is null)
+ {
+ throw new ArgumentNullException( nameof( self ) );
+ }
+
+ var existingAttributes = self.GetAttributes();
+ var retVal = ImmutableArray.CreateBuilder(existingAttributes.Length);
+ foreach(var attr in existingAttributes)
+ {
+ foreach(NamespaceQualifiedName name in names)
+ {
+ if(attr.IsFullNameMatch( name ))
+ {
+ retVal.Add(attr);
+ }
+ }
+ }
+
+ return retVal.ToImmutable();
+ }
+
+ /// Captures the attributes with a matching name for an
+ /// Symbol to test
+ /// Sequence of valid names of attributes to match
+ /// for the attribute
+ /// is null
+ ///
+ /// As an optimization, this does not test the full namespace of the attribute until a match
+ /// of the simple name is found.
+ ///
+ public static ImmutableArray CaptureMatchingAttributes( this ISymbol self, IEnumerable names )
+ {
+ if(self is null)
+ {
+ throw new ArgumentNullException( nameof( self ) );
+ }
+
+ var existingAttributes = self.GetAttributes();
+ var retVal = ImmutableArray.CreateBuilder(existingAttributes.Length);
+ foreach(var attr in existingAttributes)
+ {
+ foreach(NamespaceQualifiedName name in names)
+ {
+ if(attr.IsFullNameMatch( name ))
+ {
+ retVal.Add( attr );
+ }
+ }
+ }
+
+ return retVal.ToImmutable();
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/TypeSymbolExtensions.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/TypeSymbolExtensions.cs
new file mode 100644
index 0000000..17907d6
--- /dev/null
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/TypeSymbolExtensions.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CodeAnalysis.Utils
+{
+ /// Utility class to provide extensions for
+ public static class TypeSymbolExtensions
+ {
+ /// Gets the full name of the symbol (Including namespace names)
+ /// Symbol to get the name from
+ /// Enumerable collection of the name parts with outer most namespace first
+ public static IEnumerable GetFullName( this ITypeSymbol self )
+ {
+ foreach(string namespacePart in GetNamespaceNames( self ))
+ {
+ yield return namespacePart;
+ }
+
+ yield return self.Name;
+ }
+
+ /// Gets the sequence of namespaces names of the symbol (outermost to innermost)
+ /// Symbol to get the name from
+ /// Enumerable collection of the name parts with outer most namespace first
+ public static IEnumerable GetNamespaceNames( this ITypeSymbol self )
+ {
+ return new NamespacePartReverseIterator(self);
+ }
+
+ /// Gets the for a symbol
+ /// Symbol to get the name from
+ /// for the symbol
+ public static NamespaceQualifiedName GetNamespaceQualifiedName( this ITypeSymbol self )
+ {
+ return new( GetNamespaceNames( self ), self.Name );
+ }
+
+ // private iterator to defer the perf hit for reverse walk until the names
+ // are iterated. The call to GetEnumerator() will take the hit to reverse walk
+ // the names.
+ private class NamespacePartReverseIterator
+ : IEnumerable
+ {
+ public NamespacePartReverseIterator( ITypeSymbol symbol )
+ {
+ Symbol = symbol;
+ }
+
+ public IEnumerator GetEnumerator( )
+ {
+ // reverse the hierarchy using a stack as the first name isn't available
+ // until all are traversed.
+ var nameStack = new Stack( 64 );
+ for(ISymbol current = Symbol.ContainingNamespace; current is not null && !string.IsNullOrEmpty( current.Name ); current = current.ContainingNamespace)
+ {
+ nameStack.Push( current.Name );
+ }
+
+ return nameStack.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator( )
+ {
+ return GetEnumerator();
+ }
+
+ private readonly ITypeSymbol Symbol;
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/TypedConstantExtensions.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/TypedConstantExtensions.cs
new file mode 100644
index 0000000..e07e97c
--- /dev/null
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/TypedConstantExtensions.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CodeAnalysis.Utils
+{
+ /// Utility class to provide extensions for
+ public static class TypedConstantExtensions
+ {
+ /// Tests if a is a non null array
+ /// The constant to test
+ /// true if is not null and an array
+ public static bool IsNonNullArray( this TypedConstant self)
+ {
+ return !self.IsNull
+ && self.Kind == TypedConstantKind.Array;
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/Ubiquity.NET.CodeAnalysis.Utils.csproj b/src/Ubiquity.NET.CodeAnalysis.Utils/Ubiquity.NET.CodeAnalysis.Utils.csproj
index 5f11094..d025fac 100644
--- a/src/Ubiquity.NET.CodeAnalysis.Utils/Ubiquity.NET.CodeAnalysis.Utils.csproj
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/Ubiquity.NET.CodeAnalysis.Utils.csproj
@@ -6,11 +6,8 @@
12
@@ -28,6 +25,7 @@
Apache-2.0 WITH LLVM-exception
true
snupkg
+ True
@@ -37,9 +35,9 @@
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -49,4 +47,10 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
diff --git a/src/Ubiquity.NET.CommandLine.UT/ArgsParsingTests.cs b/src/Ubiquity.NET.CommandLine.UT/ArgsParsingTests.cs
index ab5fb3e..f5eea7c 100644
--- a/src/Ubiquity.NET.CommandLine.UT/ArgsParsingTests.cs
+++ b/src/Ubiquity.NET.CommandLine.UT/ArgsParsingTests.cs
@@ -15,13 +15,14 @@ public class ArgsParsingTests
[TestMethod]
public void ArgsParsing_Methods_handle_args_correctly( )
{
- var settings = new CmdLineSettings();
+ var settings = new CommandLineSettings();
var reporter = new TestReporter();
ParseResult parseResult = new RootCommand().Parse([]);
var nullArgEx = Assert.ThrowsExactly(()=> _ = ArgsParsing.Parse( null, null ) );
Assert.AreEqual( "args", nullArgEx.ParamName );
+#pragma warning disable CS0618 // Type or member is obsolete
// Overload 1
nullArgEx = Assert.ThrowsExactly( ( ) => _ = ArgsParsing.TryParse( null, out var _, out int _ ) );
Assert.AreEqual( "args", nullArgEx.ParamName );
@@ -31,7 +32,7 @@ public void ArgsParsing_Methods_handle_args_correctly( )
Assert.AreEqual( "args", nullArgEx.ParamName );
// Overload 2 [param 2]
- nullArgEx = Assert.ThrowsExactly( ( ) => _ = ArgsParsing.TryParse( [], (CmdLineSettings?)null, out var _, out int _ ) );
+ nullArgEx = Assert.ThrowsExactly( ( ) => _ = ArgsParsing.TryParse( [], (CommandLineSettings?)null, out var _, out int _ ) );
Assert.AreEqual( "settings", nullArgEx.ParamName );
// Overload 3
@@ -49,6 +50,7 @@ public void ArgsParsing_Methods_handle_args_correctly( )
// Overload 4 [param 3] (param 2 [settings] is nullable)
nullArgEx = Assert.ThrowsExactly( ( ) => _ = ArgsParsing.TryParse( [], null, null, out var _, out int _ ) );
Assert.AreEqual( "diagnosticReporter", nullArgEx.ParamName );
+#pragma warning restore CS0618 // Type or member is obsolete
}
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
@@ -66,10 +68,11 @@ public void TryParse_return_uses_corret_semantics( )
{
// semantics of the return is "should exit" to disambiguate from
// parsed correctly AND invoked default option (like help).
- var settings = new CmdLineSettings();
+ var settings = new CommandLineSettings();
var reporter = new TestReporter();
string[] args = ["--option1", "value"];
+#pragma warning disable CS0618 // Type or member is obsolete
bool shouldExit = ArgsParsing.TryParse( args, out TestOptions? options, out int exitCode );
Assert.IsFalse( shouldExit );
Assert.IsNotNull( options );
@@ -115,6 +118,7 @@ public void TryParse_return_uses_corret_semantics( )
Assert.IsTrue( shouldExit );
Assert.IsNull( options );
Assert.AreEqual( 0, exitCode );
+#pragma warning restore CS0618 // Type or member is obsolete
}
}
}
diff --git a/src/Ubiquity.NET.CommandLine.UT/CommandLineTests.cs b/src/Ubiquity.NET.CommandLine.UT/CommandLineTests.cs
index 8b79ace..78fdb9d 100644
--- a/src/Ubiquity.NET.CommandLine.UT/CommandLineTests.cs
+++ b/src/Ubiquity.NET.CommandLine.UT/CommandLineTests.cs
@@ -83,7 +83,7 @@ public void Extension_BuildRootCommand_succeeds( )
#if NET10_0_OR_GREATER
// With C# 14 extensions the type of options is known, so more inference is possible
- var cmd = TestOptions.BuildRootCommand( settings, o => { } );
+ var cmd = TestOptions.BuildRootCommand( settings, ( o ) => { } );
Assert.IsNotNull( cmd );
cmd = TestOptions.BuildRootCommand( settings, ( o ) => 1 );
@@ -95,16 +95,16 @@ public void Extension_BuildRootCommand_succeeds( )
cmd = TestOptions.BuildRootCommand( settings, ( o, ct ) => Task.FromResult(1) );
Assert.IsNotNull( cmd );
#else
- var cmd = CommandLineOptions.BuildRootCommand( settings, ( TestOptions o ) => { } );
+ var cmd = RootCommandBuilder.BuildRootCommand( settings, ( TestOptions o ) => { } );
Assert.IsNotNull( cmd );
- cmd = CommandLineOptions.BuildRootCommand( settings, ( TestOptions o ) => 1 );
+ cmd = RootCommandBuilder.BuildRootCommand( settings, ( TestOptions o ) => 1 );
Assert.IsNotNull( cmd );
- cmd = CommandLineOptions.BuildRootCommand( settings, ( TestOptions o, CancellationToken ct ) => Task.CompletedTask );
+ cmd = RootCommandBuilder.BuildRootCommand( settings, ( TestOptions o, CancellationToken ct ) => Task.CompletedTask );
Assert.IsNotNull( cmd );
- cmd = CommandLineOptions.BuildRootCommand( settings, ( TestOptions o, CancellationToken ct ) => Task.FromResult( 1 ) );
+ cmd = RootCommandBuilder.BuildRootCommand( settings, ( TestOptions o, CancellationToken ct ) => Task.FromResult( 1 ) );
Assert.IsNotNull( cmd );
#endif
}
@@ -147,31 +147,31 @@ public void Extentions_BuildRootCommand_throws_if_null( )
#else
// overload 1
- var ex = Assert.ThrowsExactly( ( ) =>_ = CommandLineOptions.BuildRootCommand( null, ( TestOptions o ) => { }));
+ var ex = Assert.ThrowsExactly( ( ) =>_ = RootCommandBuilder.BuildRootCommand( null, ( TestOptions o ) => { }));
Assert.AreEqual( "settings", ex.ParamName );
- ex = Assert.ThrowsExactly( ( ) => _ = CommandLineOptions.BuildRootCommand( settings, (Action?)null ) );
+ ex = Assert.ThrowsExactly( ( ) => _ = RootCommandBuilder.BuildRootCommand( settings, (Action?)null ) );
Assert.AreEqual( "action", ex.ParamName );
// overload 2
- ex = Assert.ThrowsExactly( ( ) => _ = CommandLineOptions.BuildRootCommand( null, ( TestOptions o ) => 1 ) );
+ ex = Assert.ThrowsExactly( ( ) => _ = RootCommandBuilder.BuildRootCommand( null, ( TestOptions o ) => 1 ) );
Assert.AreEqual( "settings", ex.ParamName );
- ex = Assert.ThrowsExactly( ( ) => _ = CommandLineOptions.BuildRootCommand( settings, (Func?)null ) );
+ ex = Assert.ThrowsExactly( ( ) => _ = RootCommandBuilder.BuildRootCommand( settings, (Func?)null ) );
Assert.AreEqual( "action", ex.ParamName );
// overload 3
- ex = Assert.ThrowsExactly( ( ) => _ = CommandLineOptions.BuildRootCommand( null, ( TestOptions o, CancellationToken ct ) => Task.CompletedTask ) );
+ ex = Assert.ThrowsExactly( ( ) => _ = RootCommandBuilder.BuildRootCommand( null, ( TestOptions o, CancellationToken ct ) => Task.CompletedTask ) );
Assert.AreEqual( "settings", ex.ParamName );
- ex = Assert.ThrowsExactly( ( ) => _ = CommandLineOptions.BuildRootCommand( settings, (Func?)null ) );
+ ex = Assert.ThrowsExactly( ( ) => _ = RootCommandBuilder.BuildRootCommand( settings, (Func?)null ) );
Assert.AreEqual( "action", ex.ParamName );
// overload 4
- ex = Assert.ThrowsExactly( ( ) => _ = CommandLineOptions.BuildRootCommand( null, ( TestOptions o, CancellationToken ct ) => Task.FromResult( 1 ) ) );
+ ex = Assert.ThrowsExactly( ( ) => _ = RootCommandBuilder.BuildRootCommand( null, ( TestOptions o, CancellationToken ct ) => Task.FromResult( 1 ) ) );
Assert.AreEqual( "settings", ex.ParamName );
- ex = Assert.ThrowsExactly( ( ) => _ = CommandLineOptions.BuildRootCommand( settings, (Func>?)null ) );
+ ex = Assert.ThrowsExactly( ( ) => _ = RootCommandBuilder.BuildRootCommand( settings, (Func>?)null ) );
Assert.AreEqual( "action", ex.ParamName );
#endif
}
@@ -192,7 +192,7 @@ public void Extensions_ParseAndInvokeResult_invokes_provided_action( )
var cmd = TestOptions.BuildRootCommand( settings, ( o ) => actionCalled = true);
Assert.IsNotNull( cmd );
int exitCode = cmd.ParseAndInvokeResult( reporter, settings, testArgs );
- Assert.IsTrue( actionCalled );
+ Assert.IsTrue( actionCalled, "action should be called" );
Assert.AreEqual( 0, exitCode );
// Overload 2
@@ -205,7 +205,7 @@ public void Extensions_ParseAndInvokeResult_invokes_provided_action( )
actionCalled = false;
exitCode = cmd.ParseAndInvokeResult( reporter, settings, testArgs );
- Assert.IsTrue( actionCalled );
+ Assert.IsTrue( actionCalled, "action should be called" );
Assert.AreEqual( 1, exitCode );
// Overload 3
@@ -218,7 +218,7 @@ public void Extensions_ParseAndInvokeResult_invokes_provided_action( )
actionCalled = false;
exitCode = cmd.ParseAndInvokeResult( reporter, settings, testArgs );
- Assert.IsTrue( actionCalled );
+ Assert.IsTrue( actionCalled, "action should be called" );
Assert.AreEqual( 0, exitCode );
// Overload 4
@@ -235,14 +235,14 @@ public void Extensions_ParseAndInvokeResult_invokes_provided_action( )
Assert.AreEqual( 1, exitCode );
#else
// Overload 1
- var cmd = CommandLineOptions.BuildRootCommand( settings, ( TestOptions o ) => actionCalled = true);
+ var cmd = RootCommandBuilder.BuildRootCommand( settings, ( TestOptions o ) => actionCalled = true);
Assert.IsNotNull( cmd );
int exitCode = cmd.ParseAndInvokeResult( reporter, settings, testArgs );
Assert.IsTrue( actionCalled );
Assert.AreEqual( 0, exitCode );
// Overload 2
- cmd = CommandLineOptions.BuildRootCommand( settings, ( TestOptions o ) =>
+ cmd = RootCommandBuilder.BuildRootCommand( settings, ( TestOptions o ) =>
{
actionCalled = true;
return 1;
@@ -255,7 +255,7 @@ public void Extensions_ParseAndInvokeResult_invokes_provided_action( )
Assert.AreEqual( 1, exitCode );
// Overload 3
- cmd = CommandLineOptions.BuildRootCommand( settings, ( TestOptions o, CancellationToken ct ) =>
+ cmd = RootCommandBuilder.BuildRootCommand( settings, ( TestOptions o, CancellationToken ct ) =>
{
actionCalled = true;
return Task.CompletedTask;
@@ -268,7 +268,7 @@ public void Extensions_ParseAndInvokeResult_invokes_provided_action( )
Assert.AreEqual( 0, exitCode );
// Overload 4
- cmd = CommandLineOptions.BuildRootCommand( settings, ( TestOptions o, CancellationToken ct ) =>
+ cmd = RootCommandBuilder.BuildRootCommand( settings, ( TestOptions o, CancellationToken ct ) =>
{
actionCalled = true;
return Task.FromResult( 1 );
@@ -340,14 +340,14 @@ public async Task Extensions_ParseAndInvokeResultAsync_invokes_provided_action(
Assert.AreEqual( 1, exitCode );
#else
// Overload 1
- var cmd = CommandLineOptions.BuildRootCommand( settings, ( TestOptions o ) => actionCalled = true);
+ var cmd = RootCommandBuilder.BuildRootCommand( settings, ( TestOptions o ) => actionCalled = true);
Assert.IsNotNull( cmd );
int exitCode = await cmd.ParseAndInvokeResultAsync( reporter, settings, TestContext.CancellationToken, testArgs );
Assert.IsTrue( actionCalled );
Assert.AreEqual( 0, exitCode );
// Overload 2
- cmd = CommandLineOptions.BuildRootCommand( settings, ( TestOptions o ) =>
+ cmd = RootCommandBuilder.BuildRootCommand( settings, ( TestOptions o ) =>
{
actionCalled = true;
return 1;
@@ -360,7 +360,7 @@ public async Task Extensions_ParseAndInvokeResultAsync_invokes_provided_action(
Assert.AreEqual( 1, exitCode );
// Overload 3
- cmd = CommandLineOptions.BuildRootCommand( settings, ( TestOptions o, CancellationToken ct ) =>
+ cmd = RootCommandBuilder.BuildRootCommand( settings, ( TestOptions o, CancellationToken ct ) =>
{
actionCalled = true;
return Task.CompletedTask;
@@ -373,7 +373,7 @@ public async Task Extensions_ParseAndInvokeResultAsync_invokes_provided_action(
Assert.AreEqual( 0, exitCode );
// Overload 4
- cmd = CommandLineOptions.BuildRootCommand( settings, ( TestOptions o, CancellationToken ct ) =>
+ cmd = RootCommandBuilder.BuildRootCommand( settings, ( TestOptions o, CancellationToken ct ) =>
{
actionCalled = true;
return Task.FromResult( 1 );
@@ -387,9 +387,9 @@ public async Task Extensions_ParseAndInvokeResultAsync_invokes_provided_action(
#endif
}
- internal static CmdLineSettings CreateTestSettings( DefaultOption defaultOptions = DefaultOption.Help | DefaultOption.Version )
+ internal static CommandLineSettings CreateTestSettings( DefaultOption defaultOptions = DefaultOption.Help | DefaultOption.Version )
{
- return new CmdLineSettings()
+ return new CommandLineSettings()
{
DefaultOptions = defaultOptions,
};
diff --git a/src/Ubiquity.NET.CommandLine.UT/TestOptions.g.cs b/src/Ubiquity.NET.CommandLine.UT/TestOptions.g.cs
index 531e436..20f9ad9 100644
--- a/src/Ubiquity.NET.CommandLine.UT/TestOptions.g.cs
+++ b/src/Ubiquity.NET.CommandLine.UT/TestOptions.g.cs
@@ -7,9 +7,10 @@
namespace Ubiquity.NET.CommandLine.UT
{
- // FUTURE: Generate this with source generator from attributes in other partial declaration
+ // FUTURE: TEST Generation of this with source generator from attributes in other partial declaration
internal partial class TestOptions
- : ICommandLineOptions
+ : IRootCommandBuilder
+ , ICommandBinder
{
public static TestOptions Bind( ParseResult parseResult )
{
@@ -19,7 +20,7 @@ public static TestOptions Bind( ParseResult parseResult )
};
}
- public static AppControlledDefaultsRootCommand BuildRootCommand( CmdLineSettings settings )
+ public static AppControlledDefaultsRootCommand Build( CommandLineSettings settings )
{
return new( settings, "Test option root command")
{
diff --git a/src/Ubiquity.NET.CommandLine/AppControlledDefaultsRootCommand.cs b/src/Ubiquity.NET.CommandLine/AppControlledDefaultsRootCommand.cs
index a785734..bd51d97 100644
--- a/src/Ubiquity.NET.CommandLine/AppControlledDefaultsRootCommand.cs
+++ b/src/Ubiquity.NET.CommandLine/AppControlledDefaultsRootCommand.cs
@@ -5,8 +5,9 @@ namespace Ubiquity.NET.CommandLine
{
/// Extension of that allows app control of defaults that are otherwise forced
///
- /// This type is derived from and offers no additional behavior beyond the construction.
- /// The constructor will adapt the command based on the provided. This moves the
+ /// This type is derived from and offers little additional behavior beyond the construction.
+ /// (Captures the settings for use with invocation, and the overloads to parse and invoke a result using the captured settings)
+ /// The constructor will adapt the command based on the provided. This moves the
/// hard coded defaults into an app controlled domain. The default constructed settings matches the behavior of
/// so there's no distinction. This allows an application to explicitly decide the behavior
/// and support of various defaults that could otherwise surprise the author/user. This is especially important when
@@ -21,10 +22,11 @@ public class AppControlledDefaultsRootCommand
/// Initializes a new instance of the class.
/// Description of this root command
/// Settings to apply for the command parsing
- public AppControlledDefaultsRootCommand( CmdLineSettings settings, string description = "" )
+ public AppControlledDefaultsRootCommand( CommandLineSettings settings, string description = "" )
: base( description )
{
ArgumentNullException.ThrowIfNull(settings);
+ Settings = settings;
// RootCommand constructor already adds HelpOption and VersionOption so remove them
// unless specified by caller.
@@ -58,5 +60,46 @@ public AppControlledDefaultsRootCommand( CmdLineSettings settings, string descri
Add( new EnvironmentVariablesDirective() );
}
}
+
+ /// Gets the settings used for creation and subsequent invocation
+ public CommandLineSettings Settings { get; }
+
+ /// Parses a root command and invokes the results
+ /// Diagnostic reporter to use for any errors or information in parsing
+ /// Command line arguments to parse
+ /// Exit code of the invocation
+ ///
+ /// If the is an asynchronous command action then this will
+ /// BLOCK the current thread until it completes. If that is NOT the desired behavior then
+ /// callers should use
+ /// instead for explicit async operation.
+ ///
+ public int ParseAndInvokeResult(
+ IDiagnosticReporter reporter,
+ params string[] args
+ )
+ {
+ return RootCommandExtensions.ParseAndInvokeResult(this, reporter, Settings, args);
+ }
+
+ /// Parses a root command and invokes the results
+ /// Diagnostic reporter to use for any errors or information in parsing
+ /// Cancellation token for the operation
+ /// Command line arguments to parse
+ /// Exit code of the invocation
+ ///
+ /// If the is an synchronous command action then this will
+ /// run the action asynchronously. If that is NOT the desired behavior then callers should
+ /// use
+ /// instead for an explicit synchronous operation.
+ ///
+ public Task ParseAndInvokeResultAsync(
+ IDiagnosticReporter reporter,
+ CancellationToken ct,
+ params string[] args
+ )
+ {
+ return RootCommandExtensions.ParseAndInvokeResultAsync( this, reporter, Settings, ct, args );
+ }
}
}
diff --git a/src/Ubiquity.NET.CommandLine/ArgsParsing.cs b/src/Ubiquity.NET.CommandLine/ArgsParsing.cs
index fc34b57..457d226 100644
--- a/src/Ubiquity.NET.CommandLine/ArgsParsing.cs
+++ b/src/Ubiquity.NET.CommandLine/ArgsParsing.cs
@@ -13,7 +13,7 @@ namespace Ubiquity.NET.CommandLine
public static class ArgsParsing
{
/// Parses the command line
- /// Type of value to bind the results to [Must implement ]
+ /// Type of value to bind the results to [Must implement AND ]
/// args array for the command line
/// Settings for the parse
/// Results of the parse
@@ -39,18 +39,25 @@ public static class ArgsParsing
/// implementation simply removes the actions and validation to leave both stages to the calling application as it keeps
/// things clearer when stages are unique
///
- ///
- public static ParseResult Parse( string[] args, CmdLineSettings? settings = null )
- where T : ICommandLineOptions
+ public static ParseResult Parse( string[] args, CommandLineSettings? settings = null )
+ where T : IRootCommandBuilder
{
ArgumentNullException.ThrowIfNull(args);
- settings ??= new CmdLineSettings();
- RootCommand rootCommand = T.BuildRootCommand(settings);
+ settings ??= new CommandLineSettings();
+ RootCommand rootCommand = T.Build(settings);
return rootCommand.Parse( args, settings );
}
- // FUTURE: Move these extension methods to ParseResultExtensions (ABI breaking change, but not source level)
+ ///
+ public static ParseResult Parse( string[] args )
+ where T : IRootCommandBuilderWithSettings
+ {
+ ArgumentNullException.ThrowIfNull( args );
+
+ RootCommand rootCommand = T.Build();
+ return rootCommand.Parse( args, T.Settings );
+ }
/// Invokes default options ( or )
/// Result of parse
@@ -60,16 +67,12 @@ public static ParseResult Parse( string[] args, CmdLineSettings? settings = n
///
/// The results of invoking defaults is a tuple of a flag indicating if the app should exit - default command handled,
/// and the exit code for the application. The exit code is undefined if the flag indicates the app should not exit (e.g., not
- /// handled). If it is defined, then that is what the app should return. It may be 0 if the command had no errors. But if there
- /// was an error with the execution of the default option.
- ///
- /// This is a back-compat entry point that provides behavior of original release that didn't have an overload to
- /// specify the use of the default handler or the timeout.
- ///
+ /// handled). If it is defined, then that is what the app should return. It may be 0 if the command had no errors. But won't be
+ /// if there was an error with the execution of the default option.
///
public static DefaultHandlerInvocationResult InvokeDefaultOptions(
this ParseResult parseResult,
- CmdLineSettings settings,
+ CommandLineSettings settings,
IDiagnosticReporter diagnosticReporter
)
{
@@ -118,10 +121,10 @@ public static bool ReportErrors( this ParseResult parseResult, IDiagnosticReport
///
/// In short, this wraps the following sequence of common operations and exiting on completion of
/// any operation with errors or successful invocation of default options:
- /// 1)
+ /// 1)
/// 2)
- /// 3)
- /// 4)
+ /// 3)
+ /// 4)
///
/// The is set to the exit code for the app on failures. This code indicates the
/// parse errors and is the result of invoking which, as of the current release,
@@ -130,8 +133,8 @@ public static bool ReportErrors( this ParseResult parseResult, IDiagnosticReport
/// is documented and stable.
///
/// It is recommended that applications NOT use this method (It will likely be obsoleted in the next release).
- /// Instead applications should use the
- /// or methods
+ /// Instead applications should use the
+ /// or methods
/// instead. These include invocation in the name and the result is more consistent with expectations.
/// This method has a confusing return code (and was even incorrect in some cases). Nullability of the
/// is not guaranteed based on the return value semantics. (It is null if parsed correctly AND the default options were handled).
@@ -139,21 +142,22 @@ public static bool ReportErrors( this ParseResult parseResult, IDiagnosticReport
///
///
///
+ [Obsolete( "Use RootCommandExtensions.ParseAndInvokeResult instead")]
public static bool TryParse(
string[] args,
- CmdLineSettings? settings,
+ CommandLineSettings? settings,
IDiagnosticReporter diagnosticReporter,
out T? boundValue,
out int exitCode
)
- where T : ICommandLineOptions
+ where T : IRootCommandBuilder, ICommandBinder
{
ArgumentNullException.ThrowIfNull( args );
ArgumentNullException.ThrowIfNull( diagnosticReporter );
- settings ??= new CmdLineSettings();
+ settings ??= new CommandLineSettings();
boundValue = default;
- RootCommand rootCommand = T.BuildRootCommand(settings);
+ RootCommand rootCommand = T.Build(settings);
ParseResult parseResult = rootCommand.Parse( args, settings );
// Special case the default options (Help/Version) before checking for reported errors
@@ -179,24 +183,27 @@ out int exitCode
return false; // return semantics is "should exit".
}
- ///
- public static bool TryParse( string[] args, CmdLineSettings settings, out T? boundValue, out int exitCode )
- where T : ICommandLineOptions
+ ///
+ [Obsolete( "Use RootCommandExtensions.ParseAndInvokeResult instead" )]
+ public static bool TryParse( string[] args, CommandLineSettings settings, out T? boundValue, out int exitCode )
+ where T : IRootCommandBuilder, ICommandBinder
{
ArgumentNullException.ThrowIfNull( settings );
return TryParse( args, settings, new ConsoleReporter( MsgLevel.Information ), out boundValue, out exitCode );
}
- ///
+ ///
+ [Obsolete( "Use RootCommandExtensions.ParseAndInvokeResult instead" )]
public static bool TryParse( string[] args, out T? boundValue, out int exitCode )
- where T : ICommandLineOptions
+ where T : IRootCommandBuilder, ICommandBinder
{
return TryParse( args, settings: null, new ConsoleReporter( MsgLevel.Information ), out boundValue, out exitCode );
}
- ///
+ ///
+ [Obsolete( "Use RootCommandExtensions.ParseAndInvokeResult instead" )]
public static bool TryParse( string[] args, IDiagnosticReporter diagnosticReporter, out T? boundValue, out int exitCode )
- where T : ICommandLineOptions
+ where T : IRootCommandBuilder, ICommandBinder
{
return TryParse( args, settings: null, diagnosticReporter, out boundValue, out exitCode );
}
diff --git a/src/Ubiquity.NET.CommandLine/CommandLineOptions.cs b/src/Ubiquity.NET.CommandLine/CommandLineOptions.cs
deleted file mode 100644
index b9d4c92..0000000
--- a/src/Ubiquity.NET.CommandLine/CommandLineOptions.cs
+++ /dev/null
@@ -1,168 +0,0 @@
-// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
-// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
-
-namespace Ubiquity.NET.CommandLine
-{
- /// Utility class to host extensions to
- ///
- ///
- /// If C# 14 and the `extension` everything feature is supported these will leverage it such that
- /// the class name is not needed. Otherwise, for older targets the type name is required.
- /// While default static methods on an interface is allowed by the language, use of them requires
- /// explicitly using the interface name and not the type. Additionally, since the interface is generic,
- /// this runs afoul of CA1000: Do not declare static members on generic types. Thus, this compromise
- /// is used to at least simplify the usage as much as possible. Direct use of these as static methods
- /// is valid syntax in both runtimes so is the safest approach for code that targets multiple runtimes.
- ///
- ///
- ///
- /// C# example (pre .NET 10):
- /// ( settings, (MyOptions o)=>1);
- /// ]]>
- ///
- ///
- /// C# example (.NET 10 or later):
- /// 1);
- ///
- /// // Legacy syntax is still valid
- /// CommandLineOptions.BuildRootCommand( settings, (MyOptions o)=>1);
- /// ]]>
- ///
-#if NET10_0_OR_GREATER
- [SuppressMessage("Design", "CA1034: Nested types should not be visible", Justification = "Extension, compiler generates nested types, deal with it")]
- [SuppressMessage( "Design", "CA1000:Do not declare static members on generic types", Justification = "Only Option for sensible default implementations" )]
-#endif
- public static class CommandLineOptions
- {
-#if NET10_0_OR_GREATER
- /// C# 14 static Extensions for common methods support
- /// Type to bind the options to
- extension( ICommandLineOptions )
- where T : ICommandLineOptions
- {
- /// Build a root command with synchronous handler
- /// Settings to use for the command
- /// Function to handle the command and returns an exit code
- /// built with as the handler.
- public static AppControlledDefaultsRootCommand BuildRootCommand( CmdLineSettings settings, Func action )
- {
- ArgumentNullException.ThrowIfNull( settings );
- ArgumentNullException.ThrowIfNull( action );
-
- var retVal = T.BuildRootCommand(settings);
- retVal.SetAction( pr => action( T.Bind( pr ) ) );
- return retVal;
- }
-
- /// Build a root command with synchronous handler
- /// Settings to use for the command
- /// Action to handle the command (Exceptions are treated as an exit code of 1; otherwise 0;)
- /// built with as the handler.
- public static AppControlledDefaultsRootCommand BuildRootCommand( CmdLineSettings settings, Action action )
- {
- ArgumentNullException.ThrowIfNull( settings );
- ArgumentNullException.ThrowIfNull( action );
-
- var retVal = T.BuildRootCommand(settings);
- retVal.SetAction( pr => action( T.Bind( pr ) ) );
- return retVal;
- }
-
- /// Build a root command with asynchronous handler
- /// Settings to use for the command
- /// Function to handle the command and returns an exit code
- /// built with as the handler.
- public static AppControlledDefaultsRootCommand BuildRootCommand( CmdLineSettings settings, Func> action )
- {
- ArgumentNullException.ThrowIfNull( settings );
- ArgumentNullException.ThrowIfNull( action );
-
- var retVal = T.BuildRootCommand(settings);
- retVal.SetAction( (pr, ct) => action( T.Bind( pr ), ct ) );
- return retVal;
- }
-
- /// Build a root command with asynchronous handler
- /// Settings to use for the command
- /// Function to handle the command (Exceptions are treated as an exit code of 1; otherwise 0;)
- /// built with as the handler.
- public static AppControlledDefaultsRootCommand BuildRootCommand( CmdLineSettings settings, Func action )
- {
- ArgumentNullException.ThrowIfNull( settings );
- ArgumentNullException.ThrowIfNull( action );
-
- var retVal = T.BuildRootCommand(settings);
- retVal.SetAction( (pr, ct) => action( T.Bind( pr ), ct ) );
- return retVal;
- }
- }
-#else
- /// Build a root command with a synchronous handler
- /// Type to bind the options to
- /// Settings to use for the command
- /// Action to handle the command and returns an exit code
- /// built with as the handler.
- public static AppControlledDefaultsRootCommand BuildRootCommand( CmdLineSettings settings, Func action )
- where T : ICommandLineOptions
- {
- ArgumentNullException.ThrowIfNull(settings);
- ArgumentNullException.ThrowIfNull(action);
-
- var retVal = T.BuildRootCommand(settings);
- retVal.SetAction( pr => action( T.Bind( pr ) ) );
- return retVal;
- }
-
- /// Build a root command with async handler that returns an application specific exit code (0 == Success/No Error)
- /// Type to bind the options to
- /// Settings to use for the command
- /// Async action to handle the command and returns an exit code (via )
- /// built with as the handler.
- public static AppControlledDefaultsRootCommand BuildRootCommand( CmdLineSettings settings, Func> action )
- where T : ICommandLineOptions
- {
- ArgumentNullException.ThrowIfNull(settings);
- ArgumentNullException.ThrowIfNull(action);
-
- var retVal = T.BuildRootCommand(settings);
- retVal.SetAction( (pr, ct) => action( T.Bind( pr ), ct ) );
- return retVal;
- }
-
- /// Build a root command with async handler
- /// Type to bind the options to
- /// Settings to use for the command
- /// Action to handle the command and returns an exit code
- /// built with as the handler.
- public static AppControlledDefaultsRootCommand BuildRootCommand( CmdLineSettings settings, Func action )
- where T : ICommandLineOptions
- {
- ArgumentNullException.ThrowIfNull(settings);
- ArgumentNullException.ThrowIfNull(action);
-
- var retVal = T.BuildRootCommand(settings);
- retVal.SetAction( (pr, ct) => action( T.Bind( pr ), ct ) );
- return retVal;
- }
-
- /// Build a root command with a synchronous handler
- /// Type to bind the options to
- /// Settings to use for the command
- /// Action to handle the command
- /// built with as the handler.
- public static AppControlledDefaultsRootCommand BuildRootCommand( CmdLineSettings settings, Action action )
- where T : ICommandLineOptions
- {
- ArgumentNullException.ThrowIfNull(settings);
- ArgumentNullException.ThrowIfNull(action);
-
- var retVal = T.BuildRootCommand(settings);
- retVal.SetAction( pr => action( T.Bind( pr ) ) );
- return retVal;
- }
-#endif
- }
-}
diff --git a/src/Ubiquity.NET.CommandLine/CmdLineSettings.cs b/src/Ubiquity.NET.CommandLine/CommandLineSettings.cs
similarity index 95%
rename from src/Ubiquity.NET.CommandLine/CmdLineSettings.cs
rename to src/Ubiquity.NET.CommandLine/CommandLineSettings.cs
index fe1f2cd..e24f665 100644
--- a/src/Ubiquity.NET.CommandLine/CmdLineSettings.cs
+++ b/src/Ubiquity.NET.CommandLine/CommandLineSettings.cs
@@ -46,11 +46,11 @@ public enum DefaultDirective
///