Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 77 additions & 72 deletions src/System.Management.Automation/engine/LanguagePrimitives.cs
Original file line number Diff line number Diff line change
Expand Up @@ -591,9 +591,7 @@ public static PSDataCollection<PSObject> GetPSDataCollection(object inputValue)
/// <param name="second">Object to compare first to.</param>
/// <returns>True if first is equal to the second.</returns>
public static new bool Equals(object first, object second)
{
return Equals(first, second, false, CultureInfo.InvariantCulture);
}
=> Equals(first, second, false, CultureInfo.InvariantCulture);

/// <summary>
/// Used to compare two objects for equality converting the second to the type of the first, if required.
Expand All @@ -604,9 +602,7 @@ public static PSDataCollection<PSObject> GetPSDataCollection(object inputValue)
/// to specify the type of string comparison </param>
/// <returns>True if first is equal to the second.</returns>
public static bool Equals(object first, object second, bool ignoreCase)
{
return Equals(first, second, ignoreCase, CultureInfo.InvariantCulture);
}
=> Equals(first, second, ignoreCase, CultureInfo.InvariantCulture);

/// <summary>
/// Used to compare two objects for equality converting the second to the type of the first, if required.
Expand Down Expand Up @@ -644,25 +640,28 @@ public static bool Equals(object first, object second, bool ignoreCase, IFormatP

if (first == null)
{
if (second == null) return true;
return false;
return IsNullLike(second);
}

if (second == null)
{
return false; // first is not null
return IsNullLike(first);
}

string firstString = first as string;
string secondString;
if (firstString != null)
if (first is string firstString)
{
secondString = second as string ?? (string)LanguagePrimitives.ConvertTo(second, typeof(string), culture);
return (culture.CompareInfo.Compare(firstString, secondString,
ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None) == 0);
return culture.CompareInfo.Compare(
firstString,
secondString,
ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None) == 0;
}

if (first.Equals(second)) return true;
if (first.Equals(second))
{
return true;
}

Type firstType = first.GetType();
Type secondType = second.GetType();
Expand Down Expand Up @@ -706,24 +705,24 @@ public static bool Equals(object first, object second, bool ignoreCase, IFormatP
/// Helper method for [Try]Compare to determine object ordering with null.
/// </summary>
/// <param name="value">The numeric value to compare to null.</param>
/// <param name="numberIsRightHandSide">True if the number to compare is on the right hand side if the comparison.</param>
/// <param name="numberIsRightHandSide">True if the number to compare is on the right hand side in the comparison.</param>
private static int CompareObjectToNull(object value, bool numberIsRightHandSide)
{
var i = numberIsRightHandSide ? -1 : 1;

// If it's a positive number, including 0, it's greater than null
// for everything else it's less than zero...
switch (value)
{
case Int16 i16: return Math.Sign(i16) < 0 ? -i : i;
case Int32 i32: return Math.Sign(i32) < 0 ? -i : i;
case Int64 i64: return Math.Sign(i64) < 0 ? -i : i;
case sbyte sby: return Math.Sign(sby) < 0 ? -i : i;
case float f: return Math.Sign(f) < 0 ? -i : i;
case double d: return Math.Sign(d) < 0 ? -i : i;
case decimal de: return Math.Sign(de) < 0 ? -i : i;
default: return i;
}
return value switch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch expression!!! Yay :)

{
Int16 i16 => Math.Sign(i16) < 0 ? -i : i,
Int32 i32 => Math.Sign(i32) < 0 ? -i : i,
Int64 i64 => Math.Sign(i64) < 0 ? -i : i,
sbyte s => Math.Sign(s) < 0 ? -i : i,
float f => Math.Sign(f) < 0 ? -i : i,
double d => Math.Sign(d) < 0 ? -i : i,
decimal m => Math.Sign(m) < 0 ? -i : i,
_ => IsNullLike(value) ? 0 : i
};
}

/// <summary>
Expand All @@ -739,9 +738,7 @@ private static int CompareObjectToNull(object value, bool numberIsRightHandSide)
/// to the type of <paramref name="first"/>.
/// </exception>
public static int Compare(object first, object second)
{
return LanguagePrimitives.Compare(first, second, false, CultureInfo.InvariantCulture);
}
=> LanguagePrimitives.Compare(first, second, false, CultureInfo.InvariantCulture);

/// <summary>
/// Compare first and second, converting second to the
Expand All @@ -757,9 +754,7 @@ public static int Compare(object first, object second)
/// to the type of <paramref name="first"/>.
/// </exception>
public static int Compare(object first, object second, bool ignoreCase)
{
return LanguagePrimitives.Compare(first, second, ignoreCase, CultureInfo.InvariantCulture);
}
=> LanguagePrimitives.Compare(first, second, ignoreCase, CultureInfo.InvariantCulture);

/// <summary>
/// Compare first and second, converting second to the
Expand All @@ -777,23 +772,20 @@ public static int Compare(object first, object second, bool ignoreCase)
/// </exception>
public static int Compare(object first, object second, bool ignoreCase, IFormatProvider formatProvider)
{
if (formatProvider == null)
{
formatProvider = CultureInfo.InvariantCulture;
}
formatProvider ??= CultureInfo.InvariantCulture;

var culture = formatProvider as CultureInfo;
if (culture == null)
{
throw PSTraceSource.NewArgumentException("formatProvider");
throw PSTraceSource.NewArgumentException(nameof(formatProvider));
}

first = PSObject.Base(first);
second = PSObject.Base(second);

if (first == null)
{
return second == null ? 0 : CompareObjectToNull(second, true);
return CompareObjectToNull(second, true);
}

if (second == null)
Expand All @@ -803,7 +795,7 @@ public static int Compare(object first, object second, bool ignoreCase, IFormatP

if (first is string firstString)
{
string secondString = second as string;
var secondString = second as string;
if (secondString == null)
{
try
Expand All @@ -812,19 +804,26 @@ public static int Compare(object first, object second, bool ignoreCase, IFormatP
}
catch (PSInvalidCastException e)
{
throw PSTraceSource.NewArgumentException("second", ExtendedTypeSystem.ComparisonFailure,
first.ToString(), second.ToString(), e.Message);
throw PSTraceSource.NewArgumentException(
nameof(second),
ExtendedTypeSystem.ComparisonFailure,
first.ToString(),
second.ToString(),
e.Message);
}
}

return culture.CompareInfo.Compare(firstString, secondString,
ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None);
return culture.CompareInfo.Compare(
firstString,
secondString,
ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None);
}

Type firstType = first.GetType();
Type secondType = second.GetType();
int firstIndex = LanguagePrimitives.TypeTableIndex(firstType);
int secondIndex = LanguagePrimitives.TypeTableIndex(secondType);

if ((firstIndex != -1) && (secondIndex != -1))
{
return LanguagePrimitives.NumericCompare(first, second, firstIndex, secondIndex);
Expand All @@ -837,8 +836,12 @@ public static int Compare(object first, object second, bool ignoreCase, IFormatP
}
catch (PSInvalidCastException e)
{
throw PSTraceSource.NewArgumentException("second", ExtendedTypeSystem.ComparisonFailure,
first.ToString(), second.ToString(), e.Message);
throw PSTraceSource.NewArgumentException(
nameof(second),
ExtendedTypeSystem.ComparisonFailure,
first.ToString(),
second.ToString(),
e.Message);
}

if (first is IComparable firstComparable)
Expand All @@ -853,7 +856,7 @@ public static int Compare(object first, object second, bool ignoreCase, IFormatP

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

/// <summary>
Expand All @@ -866,9 +869,7 @@ public static int Compare(object first, object second, bool ignoreCase, IFormatP
/// zero if it is greater or zero if they are the same.</param>
/// <returns>True if the comparison was successful, false otherwise.</returns>
public static bool TryCompare(object first, object second, out int result)
{
return TryCompare(first, second, ignoreCase: false, CultureInfo.InvariantCulture, out result);
}
=> TryCompare(first, second, ignoreCase: false, CultureInfo.InvariantCulture, out result);

/// <summary>
/// Tries to compare first and second, converting second to the type of the first, if necessary.
Expand All @@ -880,9 +881,7 @@ public static bool TryCompare(object first, object second, out int result)
/// <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>
/// <returns>True if the comparison was successful, false otherwise.</returns>
public static bool TryCompare(object first, object second, bool ignoreCase, out int result)
{
return TryCompare(first, second, ignoreCase, CultureInfo.InvariantCulture, out result);
}
=> TryCompare(first, second, ignoreCase, CultureInfo.InvariantCulture, out result);

/// <summary>
/// Tries to compare first and second, converting second to the type of the first, if necessary.
Expand All @@ -898,10 +897,7 @@ public static bool TryCompare(object first, object second, bool ignoreCase, out
public static bool TryCompare(object first, object second, bool ignoreCase, IFormatProvider formatProvider, out int result)
{
result = 0;
if (formatProvider == null)
{
formatProvider = CultureInfo.InvariantCulture;
}
formatProvider ??= CultureInfo.InvariantCulture;

if (!(formatProvider is CultureInfo culture))
{
Expand Down Expand Up @@ -986,8 +982,10 @@ public static bool TryCompare(object first, object second, bool ignoreCase, IFor
public static bool IsTrue(object obj)
{
// null is a valid argument - it converts to false...
if (obj == null || obj == AutomationNull.Value)
if (IsNullLike(obj))
{
return false;
}

obj = PSObject.Base(obj);

Expand All @@ -1013,8 +1011,7 @@ public static bool IsTrue(object obj)
if (objType == typeof(SwitchParameter))
return ((SwitchParameter)obj).ToBool();

IList objectArray = obj as IList;
if (objectArray != null)
if (obj is IList objectArray)
{
return IsTrue(objectArray);
}
Expand Down Expand Up @@ -1059,15 +1056,20 @@ internal static bool IsTrue(IList objectArray)
}
}

/// <summary>
/// Internal routine that determines if an object meets any of our criteria for true null.
/// </summary>
/// <param name="obj">The object to test.</param>
/// <returns>True if the object is null.</returns>
public static bool IsNull(object obj) => obj == null || obj == AutomationNull.Value;

/// <summary>
/// Internal routine that determines if an object meets any of our criteria for null.
/// This method additionally checks for <see cref="NullString.Value"/> and <see cref="DBNull.Value"/>
/// </summary>
/// <param name="obj">The object to test.</param>
/// <returns>True if the object is null.</returns>
internal static bool IsNull(object obj)
{
return (obj == null || obj == AutomationNull.Value);
}
public static bool IsNullLike(object obj) => obj == DBNull.Value || obj == NullString.Value || IsNull(obj);

/// <summary>
/// Auxiliary for the cases where we want a new PSObject or null.
Expand Down Expand Up @@ -3098,15 +3100,17 @@ private static object ConvertToVoid(object valueToConvert,
return AutomationNull.Value;
}

private static bool ConvertClassToBool(object valueToConvert,
Type resultType,
bool recursion,
PSObject originalValueToConvert,
IFormatProvider formatProvider,
TypeTable backupTable)
private static bool ConvertClassToBool(
object valueToConvert,
Type resultType,
bool recursion,
PSObject originalValueToConvert,
IFormatProvider formatProvider,
TypeTable backupTable)
{
typeConversion.WriteLine("Converting ref to boolean.");
return valueToConvert != null;
// Both NullString and DBNull should be treated the same as true nulls for the purposes of this conversion.
return !IsNullLike(valueToConvert);
}

private static bool ConvertValueToBool(object valueToConvert,
Expand Down Expand Up @@ -4711,10 +4715,11 @@ internal static IConversionData FigureConversion(object valueToConvert, Type res
{
PSObject valueAsPsObj;
Type originalType;
if (valueToConvert == null || valueToConvert == AutomationNull.Value)

if (IsNull(valueToConvert))
{
valueAsPsObj = null;
originalType = typeof(Null);
valueAsPsObj = null;
}
else
{
Expand Down
17 changes: 13 additions & 4 deletions src/System.Management.Automation/engine/runtime/Binding/Binders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3028,15 +3028,19 @@ private DynamicMetaObject CompareEQ(DynamicMetaObject target,
if (target.Value == null)
{
return new DynamicMetaObject(
arg.Value == null ? ExpressionCache.BoxedTrue : ExpressionCache.BoxedFalse,
LanguagePrimitives.IsNullLike(arg.Value)
? ExpressionCache.BoxedTrue
: ExpressionCache.BoxedFalse,
target.CombineRestrictions(arg));
}

var enumerable = PSEnumerableBinder.IsEnumerable(target);
if (enumerable == null && arg.Value == null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't handle the collection comparison case:

$s = @($null, $null, $null, [System.DBNull]::Value, [NullString]::Value)
$p = $s -eq $null
$p.Count

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daxian-dbw looks like we both spoke too soon. I have a lot of difficulty understanding all those code paths, but even without any editing yet, this works:

image

So... I would guess that somewhere in those paths we hook back into either LanguagePrimitives methods (which I've already handled) or back into the binder methods themselves as scalar comparisons.

I'll add some tests to make sure we don't regress. 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right :) I should have compile your branch and tried it myself.

// If the target is enumerable, the we generate an object[] result with elements matching.
// The iteration will be done in a pre-compiled method, but the comparison is done with
// a dynamically generated lambda that uses a binder

Thanks for adding the collection comparison test!

{
return new DynamicMetaObject(
ExpressionCache.BoxedFalse,
LanguagePrimitives.IsNullLike(target.Value)
? ExpressionCache.BoxedTrue
: ExpressionCache.BoxedFalse,
target.CombineRestrictions(arg));
}

Expand All @@ -3051,14 +3055,19 @@ private DynamicMetaObject CompareNE(DynamicMetaObject target,
if (target.Value == null)
{
return new DynamicMetaObject(
arg.Value == null ? ExpressionCache.BoxedFalse : ExpressionCache.BoxedTrue,
LanguagePrimitives.IsNullLike(arg.Value)
? ExpressionCache.BoxedFalse
: ExpressionCache.BoxedTrue,
target.CombineRestrictions(arg));
}

var enumerable = PSEnumerableBinder.IsEnumerable(target);
if (enumerable == null && arg.Value == null)
{
return new DynamicMetaObject(ExpressionCache.BoxedTrue,
return new DynamicMetaObject(
LanguagePrimitives.IsNullLike(target.Value)
? ExpressionCache.BoxedFalse
: ExpressionCache.BoxedTrue,
target.CombineRestrictions(arg));
}

Expand Down
Loading