Skip to content
Closed
18 changes: 18 additions & 0 deletions src/System.Management.Automation/engine/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2185,5 +2185,23 @@ internal object TransformInternal(
public virtual bool TransformNullOptionalParameters { get => true; }
}

/// <summary>
/// Passes in the NullLiteral.Value singleton to a parameter that explicitly
/// supports a null literal. This allows the command to distinguish between a
/// null value (e.g. null assigned to a variable that is passed into a parameter)
/// and a null literal, so that special processing can be performed.
/// </summary>
/// <remarks>
/// The Where-Object -Value parameter is an example of where this is used.
/// </remarks>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
internal sealed class SupportsNullLiteralAttribute : CmdletMetadataAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="SupportsNullLiteralAttribute"/> class.
/// </summary>
internal SupportsNullLiteralAttribute() { }
}

#endregion Data Generation Attributes
}
22 changes: 19 additions & 3 deletions src/System.Management.Automation/engine/CommandParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ private class Argument
internal Ast ast;
internal object value;
internal bool splatted;
internal bool nullLiteral;
}

private Parameter _parameter;
Expand Down Expand Up @@ -116,6 +117,14 @@ internal bool ArgumentSplatted
get { return _argument != null ? _argument.splatted : false; }
}

/// <summary>
/// Gets a value indicating whether or not an argument is a null literal.
/// </summary>
internal bool ArgumentNullLiteral
{
get { return _argument != null ? _argument.nullLiteral : false; }
}

/// <summary>
/// Set the argument value and ast.
/// </summary>
Expand Down Expand Up @@ -169,10 +178,13 @@ internal static CommandParameterInternal CreateParameter(
/// <param name="value">The argument value.</param>
/// <param name="ast">The ast of the argument value in the script.</param>
/// <param name="splatted">True if the argument value is to be splatted, false otherwise.</param>
/// <param name="nullLiteral">True if the argument value is a null literal, false otherwise.</param>
/// <returns>A new <see cref="CommandParameterInternal" /> instance.</returns>
internal static CommandParameterInternal CreateArgument(
object value,
Ast ast = null,
bool splatted = false)
bool splatted = false,
bool nullLiteral = false)
{
return new CommandParameterInternal
{
Expand All @@ -181,6 +193,7 @@ internal static CommandParameterInternal CreateArgument(
value = value,
ast = ast,
splatted = splatted,
nullLiteral = nullLiteral,
}
};
}
Expand All @@ -201,18 +214,21 @@ internal static CommandParameterInternal CreateArgument(
/// <param name="argumentAst">The ast of the argument value in the script.</param>
/// <param name="value">The argument value.</param>
/// <param name="spaceAfterParameter">Used in native commands to correctly handle -foo:bar vs. -foo: bar.</param>
/// <param name="nullLiteral">True if the argument value is a null literal; false otherwise.</param>
/// <returns>A new <see cref="CommandParameterInternal" /> instance.</returns>
internal static CommandParameterInternal CreateParameterWithArgument(
Ast parameterAst,
string parameterName,
string parameterText,
Ast argumentAst,
object value,
bool spaceAfterParameter)
bool spaceAfterParameter,
bool nullLiteral = false)
{
return new CommandParameterInternal
{
_parameter = new Parameter { ast = parameterAst, parameterName = parameterName, parameterText = parameterText },
_argument = new Argument { ast = argumentAst, value = value },
_argument = new Argument { ast = argumentAst, value = value, nullLiteral = nullLiteral },
_spaceAfterParameter = spaceAfterParameter
};
}
Expand Down
134 changes: 65 additions & 69 deletions src/System.Management.Automation/engine/CompiledCommandParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,17 @@ internal CompiledCommandParameter(MemberInfo member, bool processingDynamicParam
/// </summary>
internal bool CannotBeNull { get; private set; }

/// <summary>
/// Gets a value indicating whether the parameter has special handling for null
/// literal values. If true, the NullLiteral.Value singleton is bound to the
/// parameter when the parameter is invoked with $null.
/// </summary>
/// <remarks>
/// This allows commands that need to distinguish between a null value (such as
/// a variable whose value is $null) and a null literal to do so.
/// </remarks>
internal bool SupportsNullLiteralArgument { get; private set; }

/// <summary>
/// If true, an empty string can be bound to the string parameter
/// even if the parameter is mandatory.
Expand Down Expand Up @@ -426,91 +437,76 @@ private void ProcessAttribute(
ref string[] aliases)
{
if (attribute == null)
{
return;
}

CompiledAttributes.Add(attribute);

// Now process the attribute based on it's type
if (attribute is ParameterAttribute paramAttr)
switch (attribute)
{
ProcessParameterAttribute(memberName, paramAttr);
return;
}
case ParameterAttribute paramAttr:
ProcessParameterAttribute(memberName, paramAttr);
return;

case ValidateArgumentsAttribute validateAttr:
validationAttributes ??= new Collection<ValidateArgumentsAttribute>();
validationAttributes.Add(validateAttr);
if ((attribute is ValidateNotNullAttribute) || (attribute is ValidateNotNullOrEmptyAttribute))
{
CannotBeNull = true;
}

ValidateArgumentsAttribute validateAttr = attribute as ValidateArgumentsAttribute;
if (validateAttr != null)
{
if (validationAttributes == null)
validationAttributes = new Collection<ValidateArgumentsAttribute>();
validationAttributes.Add(validateAttr);
if ((attribute is ValidateNotNullAttribute) || (attribute is ValidateNotNullOrEmptyAttribute))
{
this.CannotBeNull = true;
}
return;

return;
}
case AliasAttribute aliasAttr:
if (aliases == null)
{
aliases = aliasAttr.aliasNames;
}
else
{
var prevAliasNames = aliases;
var newAliasNames = aliasAttr.aliasNames;
aliases = new string[prevAliasNames.Length + newAliasNames.Length];
Array.Copy(prevAliasNames, aliases, prevAliasNames.Length);
Array.Copy(newAliasNames, 0, aliases, prevAliasNames.Length, newAliasNames.Length);
}

AliasAttribute aliasAttr = attribute as AliasAttribute;
if (aliasAttr != null)
{
if (aliases == null)
{
aliases = aliasAttr.aliasNames;
}
else
{
var prevAliasNames = aliases;
var newAliasNames = aliasAttr.aliasNames;
aliases = new string[prevAliasNames.Length + newAliasNames.Length];
Array.Copy(prevAliasNames, aliases, prevAliasNames.Length);
Array.Copy(newAliasNames, 0, aliases, prevAliasNames.Length, newAliasNames.Length);
}
return;

return;
}
case ArgumentTransformationAttribute argumentAttr:
argTransformationAttributes ??= new Collection<ArgumentTransformationAttribute>();
argTransformationAttributes.Add(argumentAttr);
return;

ArgumentTransformationAttribute argumentAttr = attribute as ArgumentTransformationAttribute;
if (argumentAttr != null)
{
if (argTransformationAttributes == null)
argTransformationAttributes = new Collection<ArgumentTransformationAttribute>();
argTransformationAttributes.Add(argumentAttr);
return;
}
case AllowNullAttribute _:
AllowsNullArgument = true;
return;

AllowNullAttribute allowNullAttribute = attribute as AllowNullAttribute;
if (allowNullAttribute != null)
{
this.AllowsNullArgument = true;
return;
}
case AllowEmptyStringAttribute _:
AllowsEmptyStringArgument = true;
return;

AllowEmptyStringAttribute allowEmptyStringAttribute = attribute as AllowEmptyStringAttribute;
if (allowEmptyStringAttribute != null)
{
this.AllowsEmptyStringArgument = true;
return;
}
case AllowEmptyCollectionAttribute _:
AllowsEmptyCollectionArgument = true;
return;

AllowEmptyCollectionAttribute allowEmptyCollectionAttribute = attribute as AllowEmptyCollectionAttribute;
if (allowEmptyCollectionAttribute != null)
{
this.AllowsEmptyCollectionArgument = true;
return;
}
case ObsoleteAttribute obsoleteAttr:
ObsoleteAttribute = obsoleteAttr;
return;

ObsoleteAttribute obsoleteAttr = attribute as ObsoleteAttribute;
if (obsoleteAttr != null)
{
ObsoleteAttribute = obsoleteAttr;
return;
}
case PSTypeNameAttribute psTypeNameAttribute:
PSTypeName = psTypeNameAttribute.PSTypeName;
return;

PSTypeNameAttribute psTypeNameAttribute = attribute as PSTypeNameAttribute;
if (psTypeNameAttribute != null)
{
this.PSTypeName = psTypeNameAttribute.PSTypeName;
case SupportsNullLiteralAttribute _:
SupportsNullLiteralArgument = true;
return;

default:
return;
}
}

Expand Down
9 changes: 8 additions & 1 deletion src/System.Management.Automation/engine/InternalCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1394,6 +1394,7 @@ public string Property
[Parameter(Position = 1, ParameterSetName = "CaseSensitiveNotInSet")]
[Parameter(Position = 1, ParameterSetName = "IsSet")]
[Parameter(Position = 1, ParameterSetName = "IsNotSet")]
[SupportsNullLiteral]
public object Value
{
get
Expand Down Expand Up @@ -2172,6 +2173,7 @@ protected override void BeginProcessing()

break;
}

case TokenKind.Is:
_operationDelegate = (lval, rval) => ParserOps.IsOperator(Context, PositionUtilities.EmptyExtent, lval, rval);
break;
Expand All @@ -2180,7 +2182,12 @@ protected override void BeginProcessing()
break;
}

_convertedValue = _value;
// NullLiteral values are only handled by the -is and -isnot operators.
// For all other operators, convert a NullLiteral _value back to null.
_convertedValue = _value is NullLiteral && _binaryOperator != TokenKind.Is && _binaryOperator != TokenKind.IsNot
? null
: _value;

if (!_valueNotSpecified)
{
switch (_binaryOperator)
Expand Down
42 changes: 42 additions & 0 deletions src/System.Management.Automation/engine/NullLiteral.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace System.Management.Automation.Internal
{
/// <summary>
/// This singleton provides a way to identify when a null literal was
/// passed into a cmdlet that has explicit support for null literals
/// (e.g. Where-Object when used with -is/-isnot).
/// </summary>
internal class NullLiteral
{
/// <summary>
/// This overrides ToString() method.
/// </summary>
/// <returns>
/// This command always returns null.
/// </returns>
/// <remarks>
/// Since this object represents null, we want <see cref="ToString"/> to
/// always return null.
/// </remarks>
public override string ToString()
{
return null;
}

/// <summary>
/// Gets the singleton instance of NullLiteral.
/// </summary>
internal static NullLiteral Value { get; } = new NullLiteral();

/// <summary>
/// Prevents a default instance of the <see cref="NullLiteral"/> class from
/// being created by any source other than the <see cref="Value"/> method's
/// default assignment.
/// </summary>
private NullLiteral()
{
}
}
}
12 changes: 10 additions & 2 deletions src/System.Management.Automation/engine/ParameterBinderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
using System.Management.Automation.Internal;
using System.Management.Automation.Language;
using System.Reflection;

using Microsoft.PowerShell.Commands;
using Dbg = System.Management.Automation.Diagnostics;

namespace System.Management.Automation
Expand Down Expand Up @@ -576,7 +576,15 @@ internal virtual bool BindParameter(

try
{
BindParameter(parameter.ParameterName, parameterValue, parameterMetadata);
// After transformations and validations, if the parameter value is still null,
// if it was originally a null literal, and if the parameter has explicit support
// for a null literal, bind the NullLiteral singleton instead.
BindParameter(
parameter.ParameterName,
parameterValue == null && parameter.ArgumentNullLiteral && parameterMetadata.SupportsNullLiteralArgument
? NullLiteral.Value
: parameterValue,
parameterMetadata);
result = true;
}
catch (SetValueException setValueException)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Management.Automation.Internal;
using System.Management.Automation.Language;
using System.Text;

Expand Down Expand Up @@ -234,7 +235,11 @@ internal void ReparseUnboundArguments()

++index;
argument.ParameterName = matchingParameter.Parameter.Name;
argument.SetArgumentValue(nextArgument.ArgumentAst, nextArgument.ArgumentValue);
argument.SetArgumentValue(
nextArgument.ArgumentAst,
nextArgument.ArgumentNullLiteral && matchingParameter.Parameter.SupportsNullLiteralArgument
? NullLiteral.Value
: nextArgument.ArgumentValue);
result.Add(argument);
}
else
Expand Down Expand Up @@ -803,7 +808,8 @@ out ParameterBindingException bindingException
CommandParameterInternal.CreateParameterWithArgument(
/*parameterAst*/null, parameterName, "-" + parameterName + ":",
argument.ArgumentAst, argument.ArgumentValue,
false);
false,
argument.ArgumentNullLiteral);

bindResult =
BindParameter(
Expand Down
Loading