Support null-conditional operators ?. and ?[] in PowerShell language (#10960)

This commit is contained in:
Aditya Patwardhan 2019-11-15 16:39:53 -08:00 committed by Dongbo Wang
parent 163cba4336
commit 2579c00a20
11 changed files with 350 additions and 37 deletions

View file

@ -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);

View file

@ -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);

View file

@ -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)

View file

@ -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

View file

@ -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].

View file

@ -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

View file

@ -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,

View file

@ -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;
}

View file

@ -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('

View file

@ -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
}
}
}

View file

@ -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