Skip to content

Commit b34e331

Browse files
vexx32daxian-dbw
authored andcommitted
Consider DBNull.Value and NullString.Value the same as $null when comparing with $null and casting to bool (PowerShell#9794)
- Adds `LanguagePrimitives.IsNullLike()` method to account for `DBNull.Value` and `NullString.Value` so that they can be considered the same as a null value where sensible in PowerShell. - Updates `-ne` and `-eq` binders to treat `DBNull.Value` and `NullString.Value` as equal to null/AutomationNull. - Update code paths for comparing objects in LanguagePrimitives to ensure consistency with how the `-eq` and `-ne` binders work when calling LanguagePrimitives methods to do the comparisons. - Make `LanguagePrimitives.IsNull()` and `LanguagePrimitives.IsNullLike()` public methods. - Added tests for null behaviours in `NullRepresentatives.Tests.ps1`
1 parent f3a3922 commit b34e331

File tree

4 files changed

+209
-105
lines changed

4 files changed

+209
-105
lines changed

src/System.Management.Automation/engine/LanguagePrimitives.cs

Lines changed: 77 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -593,9 +593,7 @@ public static PSDataCollection<PSObject> GetPSDataCollection(object inputValue)
593593
/// <param name="second">Object to compare first to.</param>
594594
/// <returns>True if first is equal to the second.</returns>
595595
public static new bool Equals(object first, object second)
596-
{
597-
return Equals(first, second, false, CultureInfo.InvariantCulture);
598-
}
596+
=> Equals(first, second, false, CultureInfo.InvariantCulture);
599597

600598
/// <summary>
601599
/// Used to compare two objects for equality converting the second to the type of the first, if required.
@@ -606,9 +604,7 @@ public static PSDataCollection<PSObject> GetPSDataCollection(object inputValue)
606604
/// to specify the type of string comparison </param>
607605
/// <returns>True if first is equal to the second.</returns>
608606
public static bool Equals(object first, object second, bool ignoreCase)
609-
{
610-
return Equals(first, second, ignoreCase, CultureInfo.InvariantCulture);
611-
}
607+
=> Equals(first, second, ignoreCase, CultureInfo.InvariantCulture);
612608

613609
/// <summary>
614610
/// Used to compare two objects for equality converting the second to the type of the first, if required.
@@ -646,25 +642,28 @@ public static bool Equals(object first, object second, bool ignoreCase, IFormatP
646642

647643
if (first == null)
648644
{
649-
if (second == null) return true;
650-
return false;
645+
return IsNullLike(second);
651646
}
652647

653648
if (second == null)
654649
{
655-
return false; // first is not null
650+
return IsNullLike(first);
656651
}
657652

658-
string firstString = first as string;
659653
string secondString;
660-
if (firstString != null)
654+
if (first is string firstString)
661655
{
662656
secondString = second as string ?? (string)LanguagePrimitives.ConvertTo(second, typeof(string), culture);
663-
return (culture.CompareInfo.Compare(firstString, secondString,
664-
ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None) == 0);
657+
return culture.CompareInfo.Compare(
658+
firstString,
659+
secondString,
660+
ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None) == 0;
665661
}
666662

667-
if (first.Equals(second)) return true;
663+
if (first.Equals(second))
664+
{
665+
return true;
666+
}
668667

669668
Type firstType = first.GetType();
670669
Type secondType = second.GetType();
@@ -708,24 +707,24 @@ public static bool Equals(object first, object second, bool ignoreCase, IFormatP
708707
/// Helper method for [Try]Compare to determine object ordering with null.
709708
/// </summary>
710709
/// <param name="value">The numeric value to compare to null.</param>
711-
/// <param name="numberIsRightHandSide">True if the number to compare is on the right hand side if the comparison.</param>
710+
/// <param name="numberIsRightHandSide">True if the number to compare is on the right hand side in the comparison.</param>
712711
private static int CompareObjectToNull(object value, bool numberIsRightHandSide)
713712
{
714713
var i = numberIsRightHandSide ? -1 : 1;
715714

716715
// If it's a positive number, including 0, it's greater than null
717716
// for everything else it's less than zero...
718-
switch (value)
719-
{
720-
case Int16 i16: return Math.Sign(i16) < 0 ? -i : i;
721-
case Int32 i32: return Math.Sign(i32) < 0 ? -i : i;
722-
case Int64 i64: return Math.Sign(i64) < 0 ? -i : i;
723-
case sbyte sby: return Math.Sign(sby) < 0 ? -i : i;
724-
case float f: return Math.Sign(f) < 0 ? -i : i;
725-
case double d: return Math.Sign(d) < 0 ? -i : i;
726-
case decimal de: return Math.Sign(de) < 0 ? -i : i;
727-
default: return i;
728-
}
717+
return value switch
718+
{
719+
Int16 i16 => Math.Sign(i16) < 0 ? -i : i,
720+
Int32 i32 => Math.Sign(i32) < 0 ? -i : i,
721+
Int64 i64 => Math.Sign(i64) < 0 ? -i : i,
722+
sbyte s => Math.Sign(s) < 0 ? -i : i,
723+
float f => Math.Sign(f) < 0 ? -i : i,
724+
double d => Math.Sign(d) < 0 ? -i : i,
725+
decimal m => Math.Sign(m) < 0 ? -i : i,
726+
_ => IsNullLike(value) ? 0 : i
727+
};
729728
}
730729

731730
/// <summary>
@@ -741,9 +740,7 @@ private static int CompareObjectToNull(object value, bool numberIsRightHandSide)
741740
/// to the type of <paramref name="first"/>.
742741
/// </exception>
743742
public static int Compare(object first, object second)
744-
{
745-
return LanguagePrimitives.Compare(first, second, false, CultureInfo.InvariantCulture);
746-
}
743+
=> LanguagePrimitives.Compare(first, second, false, CultureInfo.InvariantCulture);
747744

748745
/// <summary>
749746
/// Compare first and second, converting second to the
@@ -759,9 +756,7 @@ public static int Compare(object first, object second)
759756
/// to the type of <paramref name="first"/>.
760757
/// </exception>
761758
public static int Compare(object first, object second, bool ignoreCase)
762-
{
763-
return LanguagePrimitives.Compare(first, second, ignoreCase, CultureInfo.InvariantCulture);
764-
}
759+
=> LanguagePrimitives.Compare(first, second, ignoreCase, CultureInfo.InvariantCulture);
765760

766761
/// <summary>
767762
/// Compare first and second, converting second to the
@@ -779,23 +774,20 @@ public static int Compare(object first, object second, bool ignoreCase)
779774
/// </exception>
780775
public static int Compare(object first, object second, bool ignoreCase, IFormatProvider formatProvider)
781776
{
782-
if (formatProvider == null)
783-
{
784-
formatProvider = CultureInfo.InvariantCulture;
785-
}
777+
formatProvider ??= CultureInfo.InvariantCulture;
786778

787779
var culture = formatProvider as CultureInfo;
788780
if (culture == null)
789781
{
790-
throw PSTraceSource.NewArgumentException("formatProvider");
782+
throw PSTraceSource.NewArgumentException(nameof(formatProvider));
791783
}
792784

793785
first = PSObject.Base(first);
794786
second = PSObject.Base(second);
795787

796788
if (first == null)
797789
{
798-
return second == null ? 0 : CompareObjectToNull(second, true);
790+
return CompareObjectToNull(second, true);
799791
}
800792

801793
if (second == null)
@@ -805,7 +797,7 @@ public static int Compare(object first, object second, bool ignoreCase, IFormatP
805797

806798
if (first is string firstString)
807799
{
808-
string secondString = second as string;
800+
var secondString = second as string;
809801
if (secondString == null)
810802
{
811803
try
@@ -814,19 +806,26 @@ public static int Compare(object first, object second, bool ignoreCase, IFormatP
814806
}
815807
catch (PSInvalidCastException e)
816808
{
817-
throw PSTraceSource.NewArgumentException("second", ExtendedTypeSystem.ComparisonFailure,
818-
first.ToString(), second.ToString(), e.Message);
809+
throw PSTraceSource.NewArgumentException(
810+
nameof(second),
811+
ExtendedTypeSystem.ComparisonFailure,
812+
first.ToString(),
813+
second.ToString(),
814+
e.Message);
819815
}
820816
}
821817

822-
return culture.CompareInfo.Compare(firstString, secondString,
823-
ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None);
818+
return culture.CompareInfo.Compare(
819+
firstString,
820+
secondString,
821+
ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None);
824822
}
825823

826824
Type firstType = first.GetType();
827825
Type secondType = second.GetType();
828826
int firstIndex = LanguagePrimitives.TypeTableIndex(firstType);
829827
int secondIndex = LanguagePrimitives.TypeTableIndex(secondType);
828+
830829
if ((firstIndex != -1) && (secondIndex != -1))
831830
{
832831
return LanguagePrimitives.NumericCompare(first, second, firstIndex, secondIndex);
@@ -839,8 +838,12 @@ public static int Compare(object first, object second, bool ignoreCase, IFormatP
839838
}
840839
catch (PSInvalidCastException e)
841840
{
842-
throw PSTraceSource.NewArgumentException("second", ExtendedTypeSystem.ComparisonFailure,
843-
first.ToString(), second.ToString(), e.Message);
841+
throw PSTraceSource.NewArgumentException(
842+
nameof(second),
843+
ExtendedTypeSystem.ComparisonFailure,
844+
first.ToString(),
845+
second.ToString(),
846+
e.Message);
844847
}
845848

846849
if (first is IComparable firstComparable)
@@ -855,7 +858,7 @@ public static int Compare(object first, object second, bool ignoreCase, IFormatP
855858

856859
// At this point, we know that they aren't equal but we have no way of
857860
// knowing which should compare greater than the other so we throw an exception.
858-
throw PSTraceSource.NewArgumentException("first", ExtendedTypeSystem.NotIcomparable, first.ToString());
861+
throw PSTraceSource.NewArgumentException(nameof(first), ExtendedTypeSystem.NotIcomparable, first.ToString());
859862
}
860863

861864
/// <summary>
@@ -868,9 +871,7 @@ public static int Compare(object first, object second, bool ignoreCase, IFormatP
868871
/// zero if it is greater or zero if they are the same.</param>
869872
/// <returns>True if the comparison was successful, false otherwise.</returns>
870873
public static bool TryCompare(object first, object second, out int result)
871-
{
872-
return TryCompare(first, second, ignoreCase: false, CultureInfo.InvariantCulture, out result);
873-
}
874+
=> TryCompare(first, second, ignoreCase: false, CultureInfo.InvariantCulture, out result);
874875

875876
/// <summary>
876877
/// Tries to compare first and second, converting second to the type of the first, if necessary.
@@ -882,9 +883,7 @@ public static bool TryCompare(object first, object second, out int result)
882883
/// <param name="result">Less than zero if first is smaller than second, more than zero if it is greater or zero if they are the same.</param>
883884
/// <returns>True if the comparison was successful, false otherwise.</returns>
884885
public static bool TryCompare(object first, object second, bool ignoreCase, out int result)
885-
{
886-
return TryCompare(first, second, ignoreCase, CultureInfo.InvariantCulture, out result);
887-
}
886+
=> TryCompare(first, second, ignoreCase, CultureInfo.InvariantCulture, out result);
888887

889888
/// <summary>
890889
/// Tries to compare first and second, converting second to the type of the first, if necessary.
@@ -900,10 +899,7 @@ public static bool TryCompare(object first, object second, bool ignoreCase, out
900899
public static bool TryCompare(object first, object second, bool ignoreCase, IFormatProvider formatProvider, out int result)
901900
{
902901
result = 0;
903-
if (formatProvider == null)
904-
{
905-
formatProvider = CultureInfo.InvariantCulture;
906-
}
902+
formatProvider ??= CultureInfo.InvariantCulture;
907903

908904
if (!(formatProvider is CultureInfo culture))
909905
{
@@ -988,8 +984,10 @@ public static bool TryCompare(object first, object second, bool ignoreCase, IFor
988984
public static bool IsTrue(object obj)
989985
{
990986
// null is a valid argument - it converts to false...
991-
if (obj == null || obj == AutomationNull.Value)
987+
if (IsNullLike(obj))
988+
{
992989
return false;
990+
}
993991

994992
obj = PSObject.Base(obj);
995993

@@ -1015,8 +1013,7 @@ public static bool IsTrue(object obj)
10151013
if (objType == typeof(SwitchParameter))
10161014
return ((SwitchParameter)obj).ToBool();
10171015

1018-
IList objectArray = obj as IList;
1019-
if (objectArray != null)
1016+
if (obj is IList objectArray)
10201017
{
10211018
return IsTrue(objectArray);
10221019
}
@@ -1061,15 +1058,20 @@ internal static bool IsTrue(IList objectArray)
10611058
}
10621059
}
10631060

1061+
/// <summary>
1062+
/// Internal routine that determines if an object meets any of our criteria for true null.
1063+
/// </summary>
1064+
/// <param name="obj">The object to test.</param>
1065+
/// <returns>True if the object is null.</returns>
1066+
public static bool IsNull(object obj) => obj == null || obj == AutomationNull.Value;
1067+
10641068
/// <summary>
10651069
/// Internal routine that determines if an object meets any of our criteria for null.
1070+
/// This method additionally checks for <see cref="NullString.Value"/> and <see cref="DBNull.Value"/>
10661071
/// </summary>
10671072
/// <param name="obj">The object to test.</param>
10681073
/// <returns>True if the object is null.</returns>
1069-
internal static bool IsNull(object obj)
1070-
{
1071-
return (obj == null || obj == AutomationNull.Value);
1072-
}
1074+
public static bool IsNullLike(object obj) => obj == DBNull.Value || obj == NullString.Value || IsNull(obj);
10731075

10741076
/// <summary>
10751077
/// Auxiliary for the cases where we want a new PSObject or null.
@@ -3100,15 +3102,17 @@ private static object ConvertToVoid(object valueToConvert,
31003102
return AutomationNull.Value;
31013103
}
31023104

3103-
private static bool ConvertClassToBool(object valueToConvert,
3104-
Type resultType,
3105-
bool recursion,
3106-
PSObject originalValueToConvert,
3107-
IFormatProvider formatProvider,
3108-
TypeTable backupTable)
3105+
private static bool ConvertClassToBool(
3106+
object valueToConvert,
3107+
Type resultType,
3108+
bool recursion,
3109+
PSObject originalValueToConvert,
3110+
IFormatProvider formatProvider,
3111+
TypeTable backupTable)
31093112
{
31103113
typeConversion.WriteLine("Converting ref to boolean.");
3111-
return valueToConvert != null;
3114+
// Both NullString and DBNull should be treated the same as true nulls for the purposes of this conversion.
3115+
return !IsNullLike(valueToConvert);
31123116
}
31133117

31143118
private static bool ConvertValueToBool(object valueToConvert,
@@ -4724,10 +4728,11 @@ internal static IConversionData FigureConversion(object valueToConvert, Type res
47244728
{
47254729
PSObject valueAsPsObj;
47264730
Type originalType;
4727-
if (valueToConvert == null || valueToConvert == AutomationNull.Value)
4731+
4732+
if (IsNull(valueToConvert))
47284733
{
4729-
valueAsPsObj = null;
47304734
originalType = typeof(Null);
4735+
valueAsPsObj = null;
47314736
}
47324737
else
47334738
{

src/System.Management.Automation/engine/runtime/Binding/Binders.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3028,15 +3028,19 @@ private DynamicMetaObject CompareEQ(DynamicMetaObject target,
30283028
if (target.Value == null)
30293029
{
30303030
return new DynamicMetaObject(
3031-
arg.Value == null ? ExpressionCache.BoxedTrue : ExpressionCache.BoxedFalse,
3031+
LanguagePrimitives.IsNullLike(arg.Value)
3032+
? ExpressionCache.BoxedTrue
3033+
: ExpressionCache.BoxedFalse,
30323034
target.CombineRestrictions(arg));
30333035
}
30343036

30353037
var enumerable = PSEnumerableBinder.IsEnumerable(target);
30363038
if (enumerable == null && arg.Value == null)
30373039
{
30383040
return new DynamicMetaObject(
3039-
ExpressionCache.BoxedFalse,
3041+
LanguagePrimitives.IsNullLike(target.Value)
3042+
? ExpressionCache.BoxedTrue
3043+
: ExpressionCache.BoxedFalse,
30403044
target.CombineRestrictions(arg));
30413045
}
30423046

@@ -3051,14 +3055,19 @@ private DynamicMetaObject CompareNE(DynamicMetaObject target,
30513055
if (target.Value == null)
30523056
{
30533057
return new DynamicMetaObject(
3054-
arg.Value == null ? ExpressionCache.BoxedFalse : ExpressionCache.BoxedTrue,
3058+
LanguagePrimitives.IsNullLike(arg.Value)
3059+
? ExpressionCache.BoxedFalse
3060+
: ExpressionCache.BoxedTrue,
30553061
target.CombineRestrictions(arg));
30563062
}
30573063

30583064
var enumerable = PSEnumerableBinder.IsEnumerable(target);
30593065
if (enumerable == null && arg.Value == null)
30603066
{
3061-
return new DynamicMetaObject(ExpressionCache.BoxedTrue,
3067+
return new DynamicMetaObject(
3068+
LanguagePrimitives.IsNullLike(target.Value)
3069+
? ExpressionCache.BoxedFalse
3070+
: ExpressionCache.BoxedTrue,
30623071
target.CombineRestrictions(arg));
30633072
}
30643073

0 commit comments

Comments
 (0)