Support null-conditional operators ?.
and ?[]
in PowerShell language (#10960)
This commit is contained in:
parent
163cba4336
commit
2579c00a20
|
@ -404,6 +404,7 @@ namespace System.Management.Automation
|
|||
|
||||
case TokenKind.Dot:
|
||||
case TokenKind.ColonColon:
|
||||
case TokenKind.QuestionDot:
|
||||
replacementIndex += tokenAtCursor.Text.Length;
|
||||
replacementLength = 0;
|
||||
result = CompletionCompleters.CompleteMember(completionContext, @static: tokenAtCursor.Kind == TokenKind.ColonColon);
|
||||
|
|
|
@ -127,6 +127,9 @@ namespace System.Management.Automation
|
|||
new ExperimentalFeature(
|
||||
name: "PSPipelineChainOperators",
|
||||
description: "Allow use of && and || as operators between pipeline invocations"),
|
||||
new ExperimentalFeature(
|
||||
name: "PSNullConditionalOperators",
|
||||
description: "Support the null conditional member access operators in PowerShell language")
|
||||
};
|
||||
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);
|
||||
|
||||
|
|
|
@ -6269,15 +6269,14 @@ namespace System.Management.Automation.Language
|
|||
}
|
||||
|
||||
var target = CompileExpressionOperand(memberExpressionAst.Expression);
|
||||
var memberNameAst = memberExpressionAst.Member as StringConstantExpressionAst;
|
||||
if (memberNameAst != null)
|
||||
{
|
||||
string name = memberNameAst.Value;
|
||||
return DynamicExpression.Dynamic(PSGetMemberBinder.Get(name, _memberFunctionType, memberExpressionAst.Static), typeof(object), target);
|
||||
}
|
||||
|
||||
var memberNameExpr = Compile(memberExpressionAst.Member);
|
||||
return DynamicExpression.Dynamic(PSGetDynamicMemberBinder.Get(_memberFunctionType, memberExpressionAst.Static), typeof(object), target, memberNameExpr);
|
||||
// If the ?. operator is used for null conditional check, add the null conditional expression.
|
||||
var memberNameAst = memberExpressionAst.Member as StringConstantExpressionAst;
|
||||
Expression memberAccessExpr = memberNameAst != null
|
||||
? DynamicExpression.Dynamic(PSGetMemberBinder.Get(memberNameAst.Value, _memberFunctionType, memberExpressionAst.Static), typeof(object), target)
|
||||
: DynamicExpression.Dynamic(PSGetDynamicMemberBinder.Get(_memberFunctionType, memberExpressionAst.Static), typeof(object), target, Compile(memberExpressionAst.Member));
|
||||
|
||||
return memberExpressionAst.NullConditional ? GetNullConditionalWrappedExpression(target, memberAccessExpr) : memberAccessExpr;
|
||||
}
|
||||
|
||||
internal static PSMethodInvocationConstraints GetInvokeMemberConstraints(InvokeMemberExpressionAst invokeMemberExpressionAst)
|
||||
|
@ -6314,14 +6313,18 @@ namespace System.Management.Automation.Language
|
|||
Expression target,
|
||||
IEnumerable<Expression> args,
|
||||
bool @static,
|
||||
bool propertySet)
|
||||
bool propertySet,
|
||||
bool nullConditional = false)
|
||||
{
|
||||
var callInfo = new CallInfo(args.Count());
|
||||
var classScope = _memberFunctionType != null ? _memberFunctionType.Type : null;
|
||||
var binder = name.Equals("new", StringComparison.OrdinalIgnoreCase) && @static
|
||||
? (CallSiteBinder)PSCreateInstanceBinder.Get(callInfo, constraints, publicTypeOnly: true)
|
||||
: PSInvokeMemberBinder.Get(name, callInfo, @static, propertySet, constraints, classScope);
|
||||
return DynamicExpression.Dynamic(binder, typeof(object), args.Prepend(target));
|
||||
|
||||
var dynamicExprFromBinder = DynamicExpression.Dynamic(binder, typeof(object), args.Prepend(target));
|
||||
|
||||
return nullConditional ? GetNullConditionalWrappedExpression(target, dynamicExprFromBinder) : dynamicExprFromBinder;
|
||||
}
|
||||
|
||||
private Expression InvokeBaseCtorMethod(PSMethodInvocationConstraints constraints, Expression target, IEnumerable<Expression> args)
|
||||
|
@ -6337,10 +6340,13 @@ namespace System.Management.Automation.Language
|
|||
Expression target,
|
||||
IEnumerable<Expression> args,
|
||||
bool @static,
|
||||
bool propertySet)
|
||||
bool propertySet,
|
||||
bool nullConditional = false)
|
||||
{
|
||||
var binder = PSInvokeDynamicMemberBinder.Get(new CallInfo(args.Count()), _memberFunctionType, @static, propertySet, constraints);
|
||||
return DynamicExpression.Dynamic(binder, typeof(object), args.Prepend(memberNameExpr).Prepend(target));
|
||||
var dynamicExprFromBinder = DynamicExpression.Dynamic(binder, typeof(object), args.Prepend(memberNameExpr).Prepend(target));
|
||||
|
||||
return nullConditional ? GetNullConditionalWrappedExpression(target, dynamicExprFromBinder) : dynamicExprFromBinder;
|
||||
}
|
||||
|
||||
public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst)
|
||||
|
@ -6353,11 +6359,18 @@ namespace System.Management.Automation.Language
|
|||
var memberNameAst = invokeMemberExpressionAst.Member as StringConstantExpressionAst;
|
||||
if (memberNameAst != null)
|
||||
{
|
||||
return InvokeMember(memberNameAst.Value, constraints, target, args, invokeMemberExpressionAst.Static, false);
|
||||
return InvokeMember(
|
||||
memberNameAst.Value,
|
||||
constraints,
|
||||
target,
|
||||
args,
|
||||
invokeMemberExpressionAst.Static,
|
||||
propertySet: false,
|
||||
invokeMemberExpressionAst.NullConditional);
|
||||
}
|
||||
|
||||
var memberNameExpr = Compile(invokeMemberExpressionAst.Member);
|
||||
return InvokeDynamicMember(memberNameExpr, constraints, target, args, invokeMemberExpressionAst.Static, false);
|
||||
return InvokeDynamicMember(memberNameExpr, constraints, target, args, invokeMemberExpressionAst.Static, propertySet: false, invokeMemberExpressionAst.NullConditional);
|
||||
}
|
||||
|
||||
public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst)
|
||||
|
@ -6517,15 +6530,26 @@ namespace System.Management.Automation.Language
|
|||
// In the former case, the user is requesting an array slice. In the latter case, they index expression is likely
|
||||
// an array (dynamically determined) and they don't want an array slice, they want to use the array as the index
|
||||
// expression.
|
||||
if (arrayLiteral != null && arrayLiteral.Elements.Count > 1)
|
||||
{
|
||||
return DynamicExpression.Dynamic(
|
||||
PSGetIndexBinder.Get(arrayLiteral.Elements.Count, constraints),
|
||||
typeof(object),
|
||||
arrayLiteral.Elements.Select(CompileExpressionOperand).Prepend(targetExpr));
|
||||
}
|
||||
Expression indexingExpr = arrayLiteral != null && arrayLiteral.Elements.Count > 1
|
||||
? DynamicExpression.Dynamic(
|
||||
PSGetIndexBinder.Get(arrayLiteral.Elements.Count, constraints),
|
||||
typeof(object),
|
||||
arrayLiteral.Elements.Select(CompileExpressionOperand).Prepend(targetExpr))
|
||||
: DynamicExpression.Dynamic(
|
||||
PSGetIndexBinder.Get(argCount: 1, constraints),
|
||||
typeof(object),
|
||||
targetExpr,
|
||||
CompileExpressionOperand(index));
|
||||
|
||||
return DynamicExpression.Dynamic(PSGetIndexBinder.Get(1, constraints), typeof(object), targetExpr, CompileExpressionOperand(index));
|
||||
return indexExpressionAst.NullConditional ? GetNullConditionalWrappedExpression(targetExpr, indexingExpr) : indexingExpr;
|
||||
}
|
||||
|
||||
private static Expression GetNullConditionalWrappedExpression(Expression targetExpr, Expression memberAccessExpression)
|
||||
{
|
||||
return Expression.Condition(
|
||||
Expression.Call(CachedReflectionInfo.LanguagePrimitives_IsNullLike, targetExpr.Cast(typeof(object))),
|
||||
ExpressionCache.NullConstant,
|
||||
memberAccessExpression);
|
||||
}
|
||||
|
||||
public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst)
|
||||
|
|
|
@ -7347,11 +7347,11 @@ namespace System.Management.Automation.Language
|
|||
// To support fluent style programming, allow newlines after the member access operator.
|
||||
SkipNewlines();
|
||||
|
||||
if (token.Kind == TokenKind.Dot || token.Kind == TokenKind.ColonColon)
|
||||
if (token.Kind == TokenKind.Dot || token.Kind == TokenKind.ColonColon || token.Kind == TokenKind.QuestionDot)
|
||||
{
|
||||
expr = MemberAccessRule(expr, token);
|
||||
}
|
||||
else if (token.Kind == TokenKind.LBracket)
|
||||
else if (token.Kind == TokenKind.LBracket || token.Kind == TokenKind.QuestionLBracket)
|
||||
{
|
||||
expr = ElementAccessRule(expr, token);
|
||||
}
|
||||
|
@ -7772,8 +7772,12 @@ namespace System.Management.Automation.Language
|
|||
}
|
||||
}
|
||||
|
||||
return new MemberExpressionAst(ExtentOf(targetExpr, member),
|
||||
targetExpr, member, operatorToken.Kind == TokenKind.ColonColon);
|
||||
return new MemberExpressionAst(
|
||||
ExtentOf(targetExpr, member),
|
||||
targetExpr,
|
||||
member,
|
||||
@static: operatorToken.Kind == TokenKind.ColonColon,
|
||||
nullConditional: operatorToken.Kind == TokenKind.QuestionDot);
|
||||
}
|
||||
|
||||
private ExpressionAst MemberInvokeRule(ExpressionAst targetExpr, Token lBracket, Token operatorToken, CommandElementAst member)
|
||||
|
@ -7801,7 +7805,13 @@ namespace System.Management.Automation.Language
|
|||
lastExtent = argument.Extent;
|
||||
}
|
||||
|
||||
return new InvokeMemberExpressionAst(ExtentOf(targetExpr, lastExtent), targetExpr, member, arguments, operatorToken.Kind == TokenKind.ColonColon);
|
||||
return new InvokeMemberExpressionAst(
|
||||
ExtentOf(targetExpr, lastExtent),
|
||||
targetExpr,
|
||||
member,
|
||||
arguments,
|
||||
operatorToken.Kind == TokenKind.ColonColon,
|
||||
operatorToken.Kind == TokenKind.QuestionDot);
|
||||
}
|
||||
|
||||
private List<ExpressionAst> InvokeParamParenListRule(Token lParen, out IScriptExtent lastExtent)
|
||||
|
@ -7923,7 +7933,7 @@ namespace System.Management.Automation.Language
|
|||
rBracket = null;
|
||||
}
|
||||
|
||||
return new IndexExpressionAst(ExtentOf(primaryExpression, ExtentFromFirstOf(rBracket, indexExpr)), primaryExpression, indexExpr);
|
||||
return new IndexExpressionAst(ExtentOf(primaryExpression, ExtentFromFirstOf(rBracket, indexExpr)), primaryExpression, indexExpr, lBracket.Kind == TokenKind.QuestionLBracket);
|
||||
}
|
||||
|
||||
#endregion Expressions
|
||||
|
|
|
@ -762,6 +762,14 @@ namespace System.Management.Automation.Language
|
|||
{
|
||||
errorAst = ast;
|
||||
}
|
||||
else if (ast is MemberExpressionAst memberExprAst && memberExprAst.NullConditional)
|
||||
{
|
||||
errorAst = ast;
|
||||
}
|
||||
else if (ast is IndexExpressionAst indexExprAst && indexExprAst.NullConditional)
|
||||
{
|
||||
errorAst = ast;
|
||||
}
|
||||
else if (ast is AttributedExpressionAst)
|
||||
{
|
||||
// Check for multiple types combined with [ref].
|
||||
|
|
|
@ -7851,6 +7851,26 @@ namespace System.Management.Automation.Language
|
|||
this.Static = @static;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemberExpressionAst"/> class.
|
||||
/// </summary>
|
||||
/// <param name="extent">
|
||||
/// The extent of the expression, starting with the expression before the operator '.', '::' or '?.' and ending after
|
||||
/// membername or expression naming the member.
|
||||
/// </param>
|
||||
/// <param name="expression">The expression before the member access operator '.', '::' or '?.'.</param>
|
||||
/// <param name="member">The name or expression naming the member to access.</param>
|
||||
/// <param name="static">True if the '::' operator was used, false if '.' or '?.' is used.</param>
|
||||
/// <param name="nullConditional">True if '?.' used.</param>
|
||||
/// <exception cref="PSArgumentNullException">
|
||||
/// If <paramref name="extent"/>, <paramref name="expression"/>, or <paramref name="member"/> is null.
|
||||
/// </exception>
|
||||
public MemberExpressionAst(IScriptExtent extent, ExpressionAst expression, CommandElementAst member, bool @static, bool nullConditional)
|
||||
: this(extent, expression, member, @static)
|
||||
{
|
||||
this.NullConditional = nullConditional;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The expression that produces the value to retrieve the member from. This property is never null.
|
||||
/// </summary>
|
||||
|
@ -7866,6 +7886,11 @@ namespace System.Management.Automation.Language
|
|||
/// </summary>
|
||||
public bool Static { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating true if the operator used is ?. or ?[].
|
||||
/// </summary>
|
||||
public bool NullConditional { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Copy the MemberExpressionAst instance.
|
||||
/// </summary>
|
||||
|
@ -7873,7 +7898,7 @@ namespace System.Management.Automation.Language
|
|||
{
|
||||
var newExpression = CopyElement(this.Expression);
|
||||
var newMember = CopyElement(this.Member);
|
||||
return new MemberExpressionAst(this.Extent, newExpression, newMember, this.Static);
|
||||
return new MemberExpressionAst(this.Extent, newExpression, newMember, this.Static, this.NullConditional);
|
||||
}
|
||||
|
||||
#region Visitors
|
||||
|
@ -7915,7 +7940,7 @@ namespace System.Management.Automation.Language
|
|||
/// The extent of the expression, starting with the expression before the invocation operator and ending with the
|
||||
/// closing paren after the arguments.
|
||||
/// </param>
|
||||
/// <param name="expression">The expression before the invocation operator ('.' or '::').</param>
|
||||
/// <param name="expression">The expression before the invocation operator ('.', '::').</param>
|
||||
/// <param name="method">The method to invoke.</param>
|
||||
/// <param name="arguments">The arguments to pass to the method.</param>
|
||||
/// <param name="static">
|
||||
|
@ -7934,6 +7959,29 @@ namespace System.Management.Automation.Language
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvokeMemberExpressionAst"/> class.
|
||||
/// </summary>
|
||||
/// <param name="extent">
|
||||
/// The extent of the expression, starting with the expression before the invocation operator and ending with the
|
||||
/// closing paren after the arguments.
|
||||
/// </param>
|
||||
/// <param name="expression">The expression before the invocation operator ('.', '::' or '?.').</param>
|
||||
/// <param name="method">The method to invoke.</param>
|
||||
/// <param name="arguments">The arguments to pass to the method.</param>
|
||||
/// <param name="static">
|
||||
/// True if the invocation is for a static method, using '::', false if invoking a method on an instance using '.' or '?.'.
|
||||
/// </param>
|
||||
/// <param name="nullConditional">True if the operator used is '?.'.</param>
|
||||
/// <exception cref="PSArgumentNullException">
|
||||
/// If <paramref name="extent"/> is null.
|
||||
/// </exception>
|
||||
public InvokeMemberExpressionAst(IScriptExtent extent, ExpressionAst expression, CommandElementAst method, IEnumerable<ExpressionAst> arguments, bool @static, bool nullConditional)
|
||||
: this(extent, expression, method, arguments, @static)
|
||||
{
|
||||
this.NullConditional = nullConditional;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The non-empty collection of arguments to pass when invoking the method, or null if no arguments were specified.
|
||||
/// </summary>
|
||||
|
@ -7947,7 +7995,7 @@ namespace System.Management.Automation.Language
|
|||
var newExpression = CopyElement(this.Expression);
|
||||
var newMethod = CopyElement(this.Member);
|
||||
var newArguments = CopyElements(this.Arguments);
|
||||
return new InvokeMemberExpressionAst(this.Extent, newExpression, newMethod, newArguments, this.Static);
|
||||
return new InvokeMemberExpressionAst(this.Extent, newExpression, newMethod, newArguments, this.Static, this.NullConditional);
|
||||
}
|
||||
|
||||
#region Visitors
|
||||
|
@ -10220,6 +10268,22 @@ namespace System.Management.Automation.Language
|
|||
SetParent(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IndexExpressionAst"/> class.
|
||||
/// </summary>
|
||||
/// <param name="extent">The extent of the expression.</param>
|
||||
/// <param name="target">The expression being indexed.</param>
|
||||
/// <param name="index">The index expression.</param>
|
||||
/// <param name="nullConditional">Access the index only if the target is not null.</param>
|
||||
/// <exception cref="PSArgumentNullException">
|
||||
/// If <paramref name="extent"/>, <paramref name="target"/>, or <paramref name="index"/> is null.
|
||||
/// </exception>
|
||||
public IndexExpressionAst(IScriptExtent extent, ExpressionAst target, ExpressionAst index, bool nullConditional)
|
||||
: this(extent, target, index)
|
||||
{
|
||||
this.NullConditional = nullConditional;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the ast for the expression being indexed. This value is never null.
|
||||
/// </summary>
|
||||
|
@ -10230,6 +10294,11 @@ namespace System.Management.Automation.Language
|
|||
/// </summary>
|
||||
public ExpressionAst Index { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether ?[] operator is being used.
|
||||
/// </summary>
|
||||
public bool NullConditional { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Copy the IndexExpressionAst instance.
|
||||
/// </summary>
|
||||
|
@ -10237,7 +10306,7 @@ namespace System.Management.Automation.Language
|
|||
{
|
||||
var newTarget = CopyElement(this.Target);
|
||||
var newIndex = CopyElement(this.Index);
|
||||
return new IndexExpressionAst(this.Extent, newTarget, newIndex);
|
||||
return new IndexExpressionAst(this.Extent, newTarget, newIndex, this.NullConditional);
|
||||
}
|
||||
|
||||
#region Visitors
|
||||
|
|
|
@ -422,6 +422,12 @@ namespace System.Management.Automation.Language
|
|||
/// <summary>The null coalesce operator '??'.</summary>
|
||||
QuestionQuestion = 102,
|
||||
|
||||
/// <summary>The null conditional member access operator '?.'.</summary>
|
||||
QuestionDot = 103,
|
||||
|
||||
/// <summary>The null conditional index access operator '?[]'.</summary>
|
||||
QuestionLBracket = 104,
|
||||
|
||||
#endregion Operators
|
||||
|
||||
#region Keywords
|
||||
|
@ -867,8 +873,8 @@ namespace System.Management.Automation.Language
|
|||
/* QuestionMark */ TokenFlags.TernaryOperator | TokenFlags.DisallowedInRestrictedMode,
|
||||
/* QuestionQuestionEquals */ TokenFlags.AssignmentOperator,
|
||||
/* QuestionQuestion */ TokenFlags.BinaryOperator | TokenFlags.BinaryPrecedenceCoalesce,
|
||||
/* Reserved slot 5 */ TokenFlags.None,
|
||||
/* Reserved slot 6 */ TokenFlags.None,
|
||||
/* QuestionDot */ TokenFlags.SpecialOperator | TokenFlags.DisallowedInRestrictedMode,
|
||||
/* QuestionLBracket */ TokenFlags.None,
|
||||
/* Reserved slot 7 */ TokenFlags.None,
|
||||
/* Reserved slot 8 */ TokenFlags.None,
|
||||
/* Reserved slot 9 */ TokenFlags.None,
|
||||
|
@ -1065,8 +1071,8 @@ namespace System.Management.Automation.Language
|
|||
/* QuestionMark */ "?",
|
||||
/* QuestionQuestionEquals */ "??=",
|
||||
/* QuestionQuestion */ "??",
|
||||
/* Reserved slot 5 */ string.Empty,
|
||||
/* Reserved slot 6 */ string.Empty,
|
||||
/* QuestionDot */ "?.",
|
||||
/* QuestionLBracket */ "?[",
|
||||
/* Reserved slot 7 */ string.Empty,
|
||||
/* Reserved slot 8 */ string.Empty,
|
||||
/* Reserved slot 9 */ string.Empty,
|
||||
|
|
|
@ -4217,6 +4217,27 @@ namespace System.Management.Automation.Language
|
|||
return NewToken(TokenKind.LBracket);
|
||||
}
|
||||
|
||||
|
||||
if (ExperimentalFeature.IsEnabled("PSNullConditionalOperators") && c == '?')
|
||||
{
|
||||
_tokenStart = _currentIndex;
|
||||
SkipChar();
|
||||
c = PeekChar();
|
||||
if (c == '.')
|
||||
{
|
||||
SkipChar();
|
||||
return NewToken(TokenKind.QuestionDot);
|
||||
}
|
||||
else if (c == '[' && allowLBracket)
|
||||
{
|
||||
SkipChar();
|
||||
return NewToken(TokenKind.QuestionLBracket);
|
||||
}
|
||||
|
||||
UngetChar();
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,16 @@ Describe "TabCompletion" -Tags CI {
|
|||
$res.CompletionMatches[0].CompletionText | Should -BeExactly 'ToString('
|
||||
}
|
||||
|
||||
It 'Should complete dotnet method with null conditional operator' {
|
||||
$res = TabExpansion2 -inputScript '(1)?.ToSt' -cursorColumn '(1)?.ToSt'.Length
|
||||
$res.CompletionMatches[0].CompletionText | Should -BeExactly 'ToString('
|
||||
}
|
||||
|
||||
It 'Should complete dotnet method with null conditional operator without first letter' {
|
||||
$res = TabExpansion2 -inputScript '(1)?.' -cursorColumn '(1)?.'.Length
|
||||
$res.CompletionMatches[0].CompletionText | Should -BeExactly 'CompareTo('
|
||||
}
|
||||
|
||||
It 'Should complete Magic foreach' {
|
||||
$res = TabExpansion2 -inputScript '(1..10).Fo' -cursorColumn '(1..10).Fo'.Length
|
||||
$res.CompletionMatches[0].CompletionText | Should -BeExactly 'ForEach('
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
Describe 'NullConditionalOperations' -Tags 'CI' {
|
||||
Describe 'NullCoalesceOperations' -Tags 'CI' {
|
||||
BeforeAll {
|
||||
|
||||
$skipTest = -not $EnabledExperimentalFeatures.Contains('PSCoalescingOperators')
|
||||
|
@ -264,3 +264,133 @@ Describe 'NullConditionalOperations' -Tags 'CI' {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'NullConditionalMemberAccess' -Tag 'CI' {
|
||||
|
||||
BeforeAll {
|
||||
$skipTest = -not $EnabledExperimentalFeatures.Contains('PSNullConditionalOperators')
|
||||
|
||||
if ($skipTest) {
|
||||
Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'PSNullConditionalOperators' to be enabled." -Verbose
|
||||
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
|
||||
$PSDefaultParameterValues["it:skip"] = $true
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
if ($skipTest) {
|
||||
$global:PSDefaultParameterValues = $originalDefaultParameterValues
|
||||
}
|
||||
}
|
||||
|
||||
Context '?. operator tests' {
|
||||
BeforeAll {
|
||||
$psObj = [psobject]::new()
|
||||
$psObj | Add-Member -Name 'name' -Value 'value' -MemberType NoteProperty
|
||||
$psObj | Add-Member -Name 'nested' -Value @{name = 'valuenested'} -MemberType NoteProperty
|
||||
|
||||
$psobj2 = [psobject]::new()
|
||||
$psobj2 | Add-Member -Name 'GetHello' -Value { "hello" } -MemberType ScriptMethod
|
||||
$psObj | Add-Member -Name 'nestedMethod' -Value $psobj2 -MemberType NoteProperty
|
||||
|
||||
$array = 1..3
|
||||
$hash = @{ a = 1; b = 2}
|
||||
|
||||
$null = New-Item -ItemType File -Path "$TestDrive/testfile.txt" -Force
|
||||
}
|
||||
|
||||
It 'Can get member value of a non-null variable' {
|
||||
${psObj}?.name | Should -BeExactly 'value'
|
||||
${array}?.length | Should -Be 3
|
||||
${hash}?.a | Should -Be 1
|
||||
|
||||
(Get-Process -Id $pid)?.Name | Should -BeLike "pwsh*"
|
||||
(Get-Item $TestDrive)?.EnumerateFiles()?.Name | Should -BeExactly 'testfile.txt'
|
||||
|
||||
[int32]::MaxValue?.ToString() | Should -BeExactly '2147483647'
|
||||
}
|
||||
|
||||
It 'Can get null when variable is null' {
|
||||
${nonExistent}?.name | Should -BeNullOrEmpty
|
||||
${nonExistent}?.MyMethod() | Should -BeNullOrEmpty
|
||||
|
||||
(get-process -Name doesnotexist -ErrorAction SilentlyContinue)?.Id | Should -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'Use ?. operator multiple times in statement' {
|
||||
${psObj}?.name?.nonExistent | Should -BeNullOrEmpty
|
||||
${psObj}?.nonExistent?.nonExistent | Should -BeNullOrEmpty
|
||||
${nonExistent}?.nonExistent?.nonExistent | Should -BeNullOrEmpty
|
||||
|
||||
${psObj}?.nested?.name | Should -BeExactly 'valuenested'
|
||||
${psObj}?.nestedMethod?.GetHello() | Should -BeExactly 'hello'
|
||||
}
|
||||
|
||||
It 'Use ?. on a dynamic method name' {
|
||||
$methodName = 'ToLongDateString'
|
||||
(Get-Date '11/11/2019')?.$methodName() | Should -BeExactly 'Monday, November 11, 2019'
|
||||
|
||||
${doesNotExist}?.$methodName() | Should -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'Use ?. on a dynamic method name that does not exist' {
|
||||
$methodName = 'DoesNotExist'
|
||||
{ (Get-Date '11/11/2019')?.$methodName() } | Should -Throw -ErrorId 'MethodNotFound'
|
||||
}
|
||||
|
||||
It 'Use ?. on a dynamic method name that does not exist' {
|
||||
$methodName = $null
|
||||
{ (Get-Date '11/11/2019')?.$methodName() } | Should -Throw -ErrorId 'MethodNotFound'
|
||||
}
|
||||
|
||||
It 'Use ?. on a dynamic property name' {
|
||||
$propName = 'Name'
|
||||
(Get-Process -Id $pid)?.$propName | Should -BeLike 'pwsh*'
|
||||
|
||||
${doesNotExist}?.$propName() | Should -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'Should throw error when method does not exist' {
|
||||
{ ${psObj}?.nestedMethod?.NonExistent() } | Should -Throw -ErrorId 'MethodNotFound'
|
||||
}
|
||||
}
|
||||
|
||||
Context '?[] operator tests' {
|
||||
BeforeAll {
|
||||
$array = 1..3
|
||||
$hash = @{ a = 1; b = 2}
|
||||
|
||||
$dateArray = @(
|
||||
(Get-Date '11/1/2019'),
|
||||
(Get-Date '11/2/2019'),
|
||||
(Get-Date '11/3/2019'))
|
||||
}
|
||||
|
||||
It 'Can index can call properties' {
|
||||
${array}?[0] | Should -Be 1
|
||||
${array}?[0,1] | Should -Be @(1,2)
|
||||
${array}?[0..2] | Should -Be @(1,2,3)
|
||||
${array}?[-2] | Should -Be 2
|
||||
|
||||
${hash}?['a'] | Should -Be 1
|
||||
}
|
||||
|
||||
It 'Indexing in null items should be null' {
|
||||
${doesnotExist}?[0] | Should -BeNullOrEmpty
|
||||
${doesnotExist}?[0,1] | Should -BeNullOrEmpty
|
||||
${doesnotExist}?[0..2] | Should -BeNullOrEmpty
|
||||
${doesnotExist}?[-2] | Should -BeNullOrEmpty
|
||||
|
||||
${doesnotExist}?['a'] | Should -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'Can call methods on indexed items' {
|
||||
${dateArray}?[0]?.ToLongDateString() | Should -BeExactly 'Friday, November 1, 2019'
|
||||
}
|
||||
|
||||
It 'Calling a method on nonexistent item give null' {
|
||||
${dateArray}?[1234]?.ToLongDateString() | Should -BeNullOrEmpty
|
||||
${doesNotExist}?[0]?.MyGetMethod() | Should -BeNullOrEmpty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -308,6 +308,37 @@ Describe 'null coalescing statement parsing' -Tag "CI" {
|
|||
ShouldBeParseError '$hello ??? $what' ExpectedValueExpression,MissingColonInTernaryExpression 9,17
|
||||
}
|
||||
|
||||
Describe 'null conditional member access statement parsing' -Tag 'CI' {
|
||||
BeforeAll {
|
||||
$skipTest = -not $EnabledExperimentalFeatures.Contains('PSNullConditionalOperators')
|
||||
|
||||
if ($skipTest) {
|
||||
Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'PSNullConditionalOperators' to be enabled." -Verbose
|
||||
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
|
||||
$PSDefaultParameterValues["it:skip"] = $true
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
if ($skipTest) {
|
||||
$global:PSDefaultParameterValues = $originalDefaultParameterValues
|
||||
}
|
||||
}
|
||||
|
||||
ShouldBeParseError '[datetime]?::now' ExpectedValueExpression,UnexpectedToken 11,11
|
||||
ShouldBeParseError '$x ?.name' ExpectedValueExpression,UnexpectedToken 4,4
|
||||
ShouldBeParseError 'Get-Date ?.ToString()' ExpectedExpression 20
|
||||
ShouldBeParseError '${x}?.' MissingPropertyName 6
|
||||
ShouldBeParseError '${x}?.name = "value"' InvalidLeftHandSide 0
|
||||
|
||||
ShouldBeParseError '[datetime]?[0]' MissingTypename,ExpectedValueExpression,UnexpectedToken 12,11,11
|
||||
ShouldBeParseError '${x} ?[1]' MissingTypename,ExpectedValueExpression,UnexpectedToken 7,6,6
|
||||
ShouldBeParseError '${x}?[]' MissingArrayIndexExpression 6
|
||||
ShouldBeParseError '${x}?[-]' MissingExpressionAfterOperator 7
|
||||
ShouldBeParseError '${x}?[ ]' MissingArrayIndexExpression 6
|
||||
ShouldBeParseError '${x}?[0] = 1' InvalidLeftHandSide 0
|
||||
}
|
||||
|
||||
Describe 'splatting parsing' -Tags "CI" {
|
||||
ShouldBeParseError '@a' SplattingNotPermitted 0
|
||||
ShouldBeParseError 'foreach (@a in $b) {}' SplattingNotPermitted 9
|
||||
|
|
Loading…
Reference in a new issue