Skip to content

Commit 2579c00

Browse files
adityapatwardhandaxian-dbw
authored andcommitted
Support null-conditional operators ?. and ?[] in PowerShell language (PowerShell#10960)
1 parent 163cba4 commit 2579c00

11 files changed

Lines changed: 349 additions & 36 deletions

File tree

src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ internal List<CompletionResult> GetResultHelper(CompletionContext completionCont
404404

405405
case TokenKind.Dot:
406406
case TokenKind.ColonColon:
407+
case TokenKind.QuestionDot:
407408
replacementIndex += tokenAtCursor.Text.Length;
408409
replacementLength = 0;
409410
result = CompletionCompleters.CompleteMember(completionContext, @static: tokenAtCursor.Kind == TokenKind.ColonColon);

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ static ExperimentalFeature()
127127
new ExperimentalFeature(
128128
name: "PSPipelineChainOperators",
129129
description: "Allow use of && and || as operators between pipeline invocations"),
130+
new ExperimentalFeature(
131+
name: "PSNullConditionalOperators",
132+
description: "Support the null conditional member access operators in PowerShell language")
130133
};
131134
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);
132135

src/System.Management.Automation/engine/parser/Compiler.cs

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6269,15 +6269,14 @@ public object VisitMemberExpression(MemberExpressionAst memberExpressionAst)
62696269
}
62706270

62716271
var target = CompileExpressionOperand(memberExpressionAst.Expression);
6272+
6273+
// If the ?. operator is used for null conditional check, add the null conditional expression.
62726274
var memberNameAst = memberExpressionAst.Member as StringConstantExpressionAst;
6273-
if (memberNameAst != null)
6274-
{
6275-
string name = memberNameAst.Value;
6276-
return DynamicExpression.Dynamic(PSGetMemberBinder.Get(name, _memberFunctionType, memberExpressionAst.Static), typeof(object), target);
6277-
}
6275+
Expression memberAccessExpr = memberNameAst != null
6276+
? DynamicExpression.Dynamic(PSGetMemberBinder.Get(memberNameAst.Value, _memberFunctionType, memberExpressionAst.Static), typeof(object), target)
6277+
: DynamicExpression.Dynamic(PSGetDynamicMemberBinder.Get(_memberFunctionType, memberExpressionAst.Static), typeof(object), target, Compile(memberExpressionAst.Member));
62786278

6279-
var memberNameExpr = Compile(memberExpressionAst.Member);
6280-
return DynamicExpression.Dynamic(PSGetDynamicMemberBinder.Get(_memberFunctionType, memberExpressionAst.Static), typeof(object), target, memberNameExpr);
6279+
return memberExpressionAst.NullConditional ? GetNullConditionalWrappedExpression(target, memberAccessExpr) : memberAccessExpr;
62816280
}
62826281

62836282
internal static PSMethodInvocationConstraints GetInvokeMemberConstraints(InvokeMemberExpressionAst invokeMemberExpressionAst)
@@ -6314,14 +6313,18 @@ internal Expression InvokeMember(
63146313
Expression target,
63156314
IEnumerable<Expression> args,
63166315
bool @static,
6317-
bool propertySet)
6316+
bool propertySet,
6317+
bool nullConditional = false)
63186318
{
63196319
var callInfo = new CallInfo(args.Count());
63206320
var classScope = _memberFunctionType != null ? _memberFunctionType.Type : null;
63216321
var binder = name.Equals("new", StringComparison.OrdinalIgnoreCase) && @static
63226322
? (CallSiteBinder)PSCreateInstanceBinder.Get(callInfo, constraints, publicTypeOnly: true)
63236323
: PSInvokeMemberBinder.Get(name, callInfo, @static, propertySet, constraints, classScope);
6324-
return DynamicExpression.Dynamic(binder, typeof(object), args.Prepend(target));
6324+
6325+
var dynamicExprFromBinder = DynamicExpression.Dynamic(binder, typeof(object), args.Prepend(target));
6326+
6327+
return nullConditional ? GetNullConditionalWrappedExpression(target, dynamicExprFromBinder) : dynamicExprFromBinder;
63256328
}
63266329

63276330
private Expression InvokeBaseCtorMethod(PSMethodInvocationConstraints constraints, Expression target, IEnumerable<Expression> args)
@@ -6337,10 +6340,13 @@ internal Expression InvokeDynamicMember(
63376340
Expression target,
63386341
IEnumerable<Expression> args,
63396342
bool @static,
6340-
bool propertySet)
6343+
bool propertySet,
6344+
bool nullConditional = false)
63416345
{
63426346
var binder = PSInvokeDynamicMemberBinder.Get(new CallInfo(args.Count()), _memberFunctionType, @static, propertySet, constraints);
6343-
return DynamicExpression.Dynamic(binder, typeof(object), args.Prepend(memberNameExpr).Prepend(target));
6347+
var dynamicExprFromBinder = DynamicExpression.Dynamic(binder, typeof(object), args.Prepend(memberNameExpr).Prepend(target));
6348+
6349+
return nullConditional ? GetNullConditionalWrappedExpression(target, dynamicExprFromBinder) : dynamicExprFromBinder;
63446350
}
63456351

63466352
public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst)
@@ -6353,11 +6359,18 @@ public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMember
63536359
var memberNameAst = invokeMemberExpressionAst.Member as StringConstantExpressionAst;
63546360
if (memberNameAst != null)
63556361
{
6356-
return InvokeMember(memberNameAst.Value, constraints, target, args, invokeMemberExpressionAst.Static, false);
6362+
return InvokeMember(
6363+
memberNameAst.Value,
6364+
constraints,
6365+
target,
6366+
args,
6367+
invokeMemberExpressionAst.Static,
6368+
propertySet: false,
6369+
invokeMemberExpressionAst.NullConditional);
63576370
}
63586371

63596372
var memberNameExpr = Compile(invokeMemberExpressionAst.Member);
6360-
return InvokeDynamicMember(memberNameExpr, constraints, target, args, invokeMemberExpressionAst.Static, false);
6373+
return InvokeDynamicMember(memberNameExpr, constraints, target, args, invokeMemberExpressionAst.Static, propertySet: false, invokeMemberExpressionAst.NullConditional);
63616374
}
63626375

63636376
public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst)
@@ -6517,15 +6530,26 @@ public object VisitIndexExpression(IndexExpressionAst indexExpressionAst)
65176530
// In the former case, the user is requesting an array slice. In the latter case, they index expression is likely
65186531
// an array (dynamically determined) and they don't want an array slice, they want to use the array as the index
65196532
// expression.
6520-
if (arrayLiteral != null && arrayLiteral.Elements.Count > 1)
6521-
{
6522-
return DynamicExpression.Dynamic(
6523-
PSGetIndexBinder.Get(arrayLiteral.Elements.Count, constraints),
6524-
typeof(object),
6525-
arrayLiteral.Elements.Select(CompileExpressionOperand).Prepend(targetExpr));
6526-
}
6533+
Expression indexingExpr = arrayLiteral != null && arrayLiteral.Elements.Count > 1
6534+
? DynamicExpression.Dynamic(
6535+
PSGetIndexBinder.Get(arrayLiteral.Elements.Count, constraints),
6536+
typeof(object),
6537+
arrayLiteral.Elements.Select(CompileExpressionOperand).Prepend(targetExpr))
6538+
: DynamicExpression.Dynamic(
6539+
PSGetIndexBinder.Get(argCount: 1, constraints),
6540+
typeof(object),
6541+
targetExpr,
6542+
CompileExpressionOperand(index));
65276543

6528-
return DynamicExpression.Dynamic(PSGetIndexBinder.Get(1, constraints), typeof(object), targetExpr, CompileExpressionOperand(index));
6544+
return indexExpressionAst.NullConditional ? GetNullConditionalWrappedExpression(targetExpr, indexingExpr) : indexingExpr;
6545+
}
6546+
6547+
private static Expression GetNullConditionalWrappedExpression(Expression targetExpr, Expression memberAccessExpression)
6548+
{
6549+
return Expression.Condition(
6550+
Expression.Call(CachedReflectionInfo.LanguagePrimitives_IsNullLike, targetExpr.Cast(typeof(object))),
6551+
ExpressionCache.NullConstant,
6552+
memberAccessExpression);
65296553
}
65306554

65316555
public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst)

src/System.Management.Automation/engine/parser/Parser.cs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7347,11 +7347,11 @@ private ExpressionAst CheckPostPrimaryExpressionOperators(Token token, Expressio
73477347
// To support fluent style programming, allow newlines after the member access operator.
73487348
SkipNewlines();
73497349

7350-
if (token.Kind == TokenKind.Dot || token.Kind == TokenKind.ColonColon)
7350+
if (token.Kind == TokenKind.Dot || token.Kind == TokenKind.ColonColon || token.Kind == TokenKind.QuestionDot)
73517351
{
73527352
expr = MemberAccessRule(expr, token);
73537353
}
7354-
else if (token.Kind == TokenKind.LBracket)
7354+
else if (token.Kind == TokenKind.LBracket || token.Kind == TokenKind.QuestionLBracket)
73557355
{
73567356
expr = ElementAccessRule(expr, token);
73577357
}
@@ -7772,8 +7772,12 @@ private ExpressionAst MemberAccessRule(ExpressionAst targetExpr, Token operatorT
77727772
}
77737773
}
77747774

7775-
return new MemberExpressionAst(ExtentOf(targetExpr, member),
7776-
targetExpr, member, operatorToken.Kind == TokenKind.ColonColon);
7775+
return new MemberExpressionAst(
7776+
ExtentOf(targetExpr, member),
7777+
targetExpr,
7778+
member,
7779+
@static: operatorToken.Kind == TokenKind.ColonColon,
7780+
nullConditional: operatorToken.Kind == TokenKind.QuestionDot);
77777781
}
77787782

77797783
private ExpressionAst MemberInvokeRule(ExpressionAst targetExpr, Token lBracket, Token operatorToken, CommandElementAst member)
@@ -7801,7 +7805,13 @@ private ExpressionAst MemberInvokeRule(ExpressionAst targetExpr, Token lBracket,
78017805
lastExtent = argument.Extent;
78027806
}
78037807

7804-
return new InvokeMemberExpressionAst(ExtentOf(targetExpr, lastExtent), targetExpr, member, arguments, operatorToken.Kind == TokenKind.ColonColon);
7808+
return new InvokeMemberExpressionAst(
7809+
ExtentOf(targetExpr, lastExtent),
7810+
targetExpr,
7811+
member,
7812+
arguments,
7813+
operatorToken.Kind == TokenKind.ColonColon,
7814+
operatorToken.Kind == TokenKind.QuestionDot);
78057815
}
78067816

78077817
private List<ExpressionAst> InvokeParamParenListRule(Token lParen, out IScriptExtent lastExtent)
@@ -7923,7 +7933,7 @@ private ExpressionAst ElementAccessRule(ExpressionAst primaryExpression, Token l
79237933
rBracket = null;
79247934
}
79257935

7926-
return new IndexExpressionAst(ExtentOf(primaryExpression, ExtentFromFirstOf(rBracket, indexExpr)), primaryExpression, indexExpr);
7936+
return new IndexExpressionAst(ExtentOf(primaryExpression, ExtentFromFirstOf(rBracket, indexExpr)), primaryExpression, indexExpr, lBracket.Kind == TokenKind.QuestionLBracket);
79277937
}
79287938

79297939
#endregion Expressions

src/System.Management.Automation/engine/parser/SemanticChecks.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,14 @@ private void CheckAssignmentTarget(ExpressionAst ast, bool simpleAssignment, Act
762762
{
763763
errorAst = ast;
764764
}
765+
else if (ast is MemberExpressionAst memberExprAst && memberExprAst.NullConditional)
766+
{
767+
errorAst = ast;
768+
}
769+
else if (ast is IndexExpressionAst indexExprAst && indexExprAst.NullConditional)
770+
{
771+
errorAst = ast;
772+
}
765773
else if (ast is AttributedExpressionAst)
766774
{
767775
// Check for multiple types combined with [ref].

src/System.Management.Automation/engine/parser/ast.cs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7851,6 +7851,26 @@ public MemberExpressionAst(IScriptExtent extent, ExpressionAst expression, Comma
78517851
this.Static = @static;
78527852
}
78537853

7854+
/// <summary>
7855+
/// Initializes a new instance of the <see cref="MemberExpressionAst"/> class.
7856+
/// </summary>
7857+
/// <param name="extent">
7858+
/// The extent of the expression, starting with the expression before the operator '.', '::' or '?.' and ending after
7859+
/// membername or expression naming the member.
7860+
/// </param>
7861+
/// <param name="expression">The expression before the member access operator '.', '::' or '?.'.</param>
7862+
/// <param name="member">The name or expression naming the member to access.</param>
7863+
/// <param name="static">True if the '::' operator was used, false if '.' or '?.' is used.</param>
7864+
/// <param name="nullConditional">True if '?.' used.</param>
7865+
/// <exception cref="PSArgumentNullException">
7866+
/// If <paramref name="extent"/>, <paramref name="expression"/>, or <paramref name="member"/> is null.
7867+
/// </exception>
7868+
public MemberExpressionAst(IScriptExtent extent, ExpressionAst expression, CommandElementAst member, bool @static, bool nullConditional)
7869+
: this(extent, expression, member, @static)
7870+
{
7871+
this.NullConditional = nullConditional;
7872+
}
7873+
78547874
/// <summary>
78557875
/// The expression that produces the value to retrieve the member from. This property is never null.
78567876
/// </summary>
@@ -7866,14 +7886,19 @@ public MemberExpressionAst(IScriptExtent extent, ExpressionAst expression, Comma
78667886
/// </summary>
78677887
public bool Static { get; private set; }
78687888

7889+
/// <summary>
7890+
/// Gets a value indicating true if the operator used is ?. or ?[].
7891+
/// </summary>
7892+
public bool NullConditional { get; protected set; }
7893+
78697894
/// <summary>
78707895
/// Copy the MemberExpressionAst instance.
78717896
/// </summary>
78727897
public override Ast Copy()
78737898
{
78747899
var newExpression = CopyElement(this.Expression);
78757900
var newMember = CopyElement(this.Member);
7876-
return new MemberExpressionAst(this.Extent, newExpression, newMember, this.Static);
7901+
return new MemberExpressionAst(this.Extent, newExpression, newMember, this.Static, this.NullConditional);
78777902
}
78787903

78797904
#region Visitors
@@ -7915,7 +7940,7 @@ public class InvokeMemberExpressionAst : MemberExpressionAst, ISupportsAssignmen
79157940
/// The extent of the expression, starting with the expression before the invocation operator and ending with the
79167941
/// closing paren after the arguments.
79177942
/// </param>
7918-
/// <param name="expression">The expression before the invocation operator ('.' or '::').</param>
7943+
/// <param name="expression">The expression before the invocation operator ('.', '::').</param>
79197944
/// <param name="method">The method to invoke.</param>
79207945
/// <param name="arguments">The arguments to pass to the method.</param>
79217946
/// <param name="static">
@@ -7934,6 +7959,29 @@ public InvokeMemberExpressionAst(IScriptExtent extent, ExpressionAst expression,
79347959
}
79357960
}
79367961

7962+
/// <summary>
7963+
/// Initializes a new instance of the <see cref="InvokeMemberExpressionAst"/> class.
7964+
/// </summary>
7965+
/// <param name="extent">
7966+
/// The extent of the expression, starting with the expression before the invocation operator and ending with the
7967+
/// closing paren after the arguments.
7968+
/// </param>
7969+
/// <param name="expression">The expression before the invocation operator ('.', '::' or '?.').</param>
7970+
/// <param name="method">The method to invoke.</param>
7971+
/// <param name="arguments">The arguments to pass to the method.</param>
7972+
/// <param name="static">
7973+
/// True if the invocation is for a static method, using '::', false if invoking a method on an instance using '.' or '?.'.
7974+
/// </param>
7975+
/// <param name="nullConditional">True if the operator used is '?.'.</param>
7976+
/// <exception cref="PSArgumentNullException">
7977+
/// If <paramref name="extent"/> is null.
7978+
/// </exception>
7979+
public InvokeMemberExpressionAst(IScriptExtent extent, ExpressionAst expression, CommandElementAst method, IEnumerable<ExpressionAst> arguments, bool @static, bool nullConditional)
7980+
: this(extent, expression, method, arguments, @static)
7981+
{
7982+
this.NullConditional = nullConditional;
7983+
}
7984+
79377985
/// <summary>
79387986
/// The non-empty collection of arguments to pass when invoking the method, or null if no arguments were specified.
79397987
/// </summary>
@@ -7947,7 +7995,7 @@ public override Ast Copy()
79477995
var newExpression = CopyElement(this.Expression);
79487996
var newMethod = CopyElement(this.Member);
79497997
var newArguments = CopyElements(this.Arguments);
7950-
return new InvokeMemberExpressionAst(this.Extent, newExpression, newMethod, newArguments, this.Static);
7998+
return new InvokeMemberExpressionAst(this.Extent, newExpression, newMethod, newArguments, this.Static, this.NullConditional);
79517999
}
79528000

79538001
#region Visitors
@@ -10220,6 +10268,22 @@ public IndexExpressionAst(IScriptExtent extent, ExpressionAst target, Expression
1022010268
SetParent(index);
1022110269
}
1022210270

10271+
/// <summary>
10272+
/// Initializes a new instance of the <see cref="IndexExpressionAst"/> class.
10273+
/// </summary>
10274+
/// <param name="extent">The extent of the expression.</param>
10275+
/// <param name="target">The expression being indexed.</param>
10276+
/// <param name="index">The index expression.</param>
10277+
/// <param name="nullConditional">Access the index only if the target is not null.</param>
10278+
/// <exception cref="PSArgumentNullException">
10279+
/// If <paramref name="extent"/>, <paramref name="target"/>, or <paramref name="index"/> is null.
10280+
/// </exception>
10281+
public IndexExpressionAst(IScriptExtent extent, ExpressionAst target, ExpressionAst index, bool nullConditional)
10282+
: this(extent, target, index)
10283+
{
10284+
this.NullConditional = nullConditional;
10285+
}
10286+
1022310287
/// <summary>
1022410288
/// Return the ast for the expression being indexed. This value is never null.
1022510289
/// </summary>
@@ -10230,14 +10294,19 @@ public IndexExpressionAst(IScriptExtent extent, ExpressionAst target, Expression
1023010294
/// </summary>
1023110295
public ExpressionAst Index { get; private set; }
1023210296

10297+
/// <summary>
10298+
/// Gets a value indicating whether ?[] operator is being used.
10299+
/// </summary>
10300+
public bool NullConditional { get; private set; }
10301+
1023310302
/// <summary>
1023410303
/// Copy the IndexExpressionAst instance.
1023510304
/// </summary>
1023610305
public override Ast Copy()
1023710306
{
1023810307
var newTarget = CopyElement(this.Target);
1023910308
var newIndex = CopyElement(this.Index);
10240-
return new IndexExpressionAst(this.Extent, newTarget, newIndex);
10309+
return new IndexExpressionAst(this.Extent, newTarget, newIndex, this.NullConditional);
1024110310
}
1024210311

1024310312
#region Visitors

0 commit comments

Comments
 (0)