feat(compiler): support safe keyed read expressions (#41911)

Currently we support safe property (`a?.b`) and method (`a?.b()`) accesses, but we don't handle safe keyed reads (`a?.[0]`) which is inconsistent. These changes expand the compiler in order to support safe key read expressions as well.

PR Close #41911
This commit is contained in:
Kristiyan Kostadinov
2021-06-03 13:22:41 -07:00
committed by Jessica Janiuk
parent 47270d9e63
commit ba084857ea
21 changed files with 385 additions and 50 deletions
+41 -10
View File
@@ -156,7 +156,8 @@ export class SafePropertyRead extends ASTWithName {
}
export class KeyedRead extends AST {
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public obj: AST, public key: AST) {
constructor(
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public receiver: AST, public key: AST) {
super(span, sourceSpan);
}
visit(visitor: AstVisitor, context: any = null): any {
@@ -164,9 +165,19 @@ export class KeyedRead extends AST {
}
}
export class SafeKeyedRead extends AST {
constructor(
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public receiver: AST, public key: AST) {
super(span, sourceSpan);
}
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitSafeKeyedRead(this, context);
}
}
export class KeyedWrite extends AST {
constructor(
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public obj: AST, public key: AST,
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public receiver: AST, public key: AST,
public value: AST) {
super(span, sourceSpan);
}
@@ -453,6 +464,7 @@ export interface AstVisitor {
visitQuote(ast: Quote, context: any): any;
visitSafeMethodCall(ast: SafeMethodCall, context: any): any;
visitSafePropertyRead(ast: SafePropertyRead, context: any): any;
visitSafeKeyedRead(ast: SafeKeyedRead, context: any): any;
visitASTWithSource?(ast: ASTWithSource, context: any): any;
/**
* This function is optionally defined to allow classes that implement this
@@ -501,11 +513,11 @@ export class RecursiveAstVisitor implements AstVisitor {
this.visitAll(ast.expressions, context);
}
visitKeyedRead(ast: KeyedRead, context: any): any {
this.visit(ast.obj, context);
this.visit(ast.receiver, context);
this.visit(ast.key, context);
}
visitKeyedWrite(ast: KeyedWrite, context: any): any {
this.visit(ast.obj, context);
this.visit(ast.receiver, context);
this.visit(ast.key, context);
this.visit(ast.value, context);
}
@@ -540,6 +552,10 @@ export class RecursiveAstVisitor implements AstVisitor {
this.visit(ast.receiver, context);
this.visitAll(ast.args, context);
}
visitSafeKeyedRead(ast: SafeKeyedRead, context: any): any {
this.visit(ast.receiver, context);
this.visit(ast.key, context);
}
visitQuote(ast: Quote, context: any): any {}
// This is not part of the AstVisitor interface, just a helper method
visitAll(asts: AST[], context: any): any {
@@ -644,12 +660,13 @@ export class AstTransformer implements AstVisitor {
}
visitKeyedRead(ast: KeyedRead, context: any): AST {
return new KeyedRead(ast.span, ast.sourceSpan, ast.obj.visit(this), ast.key.visit(this));
return new KeyedRead(ast.span, ast.sourceSpan, ast.receiver.visit(this), ast.key.visit(this));
}
visitKeyedWrite(ast: KeyedWrite, context: any): AST {
return new KeyedWrite(
ast.span, ast.sourceSpan, ast.obj.visit(this), ast.key.visit(this), ast.value.visit(this));
ast.span, ast.sourceSpan, ast.receiver.visit(this), ast.key.visit(this),
ast.value.visit(this));
}
visitAll(asts: any[]): any[] {
@@ -668,6 +685,11 @@ export class AstTransformer implements AstVisitor {
return new Quote(
ast.span, ast.sourceSpan, ast.prefix, ast.uninterpretedExpression, ast.location);
}
visitSafeKeyedRead(ast: SafeKeyedRead, context: any): AST {
return new SafeKeyedRead(
ast.span, ast.sourceSpan, ast.receiver.visit(this), ast.key.visit(this));
}
}
// A transformer that only creates new nodes if the transformer makes a change or
@@ -822,19 +844,19 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
}
visitKeyedRead(ast: KeyedRead, context: any): AST {
const obj = ast.obj.visit(this);
const obj = ast.receiver.visit(this);
const key = ast.key.visit(this);
if (obj !== ast.obj || key !== ast.key) {
if (obj !== ast.receiver || key !== ast.key) {
return new KeyedRead(ast.span, ast.sourceSpan, obj, key);
}
return ast;
}
visitKeyedWrite(ast: KeyedWrite, context: any): AST {
const obj = ast.obj.visit(this);
const obj = ast.receiver.visit(this);
const key = ast.key.visit(this);
const value = ast.value.visit(this);
if (obj !== ast.obj || key !== ast.key || value !== ast.value) {
if (obj !== ast.receiver || key !== ast.key || value !== ast.value) {
return new KeyedWrite(ast.span, ast.sourceSpan, obj, key, value);
}
return ast;
@@ -863,6 +885,15 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
visitQuote(ast: Quote, context: any): AST {
return ast;
}
visitSafeKeyedRead(ast: SafeKeyedRead, context: any): AST {
const obj = ast.receiver.visit(this);
const key = ast.key.visit(this);
if (obj !== ast.receiver || key !== ast.key) {
return new SafeKeyedRead(ast.span, ast.sourceSpan, obj, key);
}
return ast;
}
}
// Bindings
@@ -9,7 +9,7 @@
import * as chars from '../chars';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, ExpressionBinding, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParserError, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, TemplateBinding, TemplateBindingIdentifier, ThisReceiver, Unary, VariableBinding} from './ast';
import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, ExpressionBinding, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParserError, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeKeyedRead, SafeMethodCall, SafePropertyRead, TemplateBinding, TemplateBindingIdentifier, ThisReceiver, Unary, VariableBinding} from './ast';
import {EOF, isIdentifier, isQuote, Lexer, Token, TokenType} from './lexer';
export interface InterpolationPiece {
@@ -822,24 +822,11 @@ export class _ParseAST {
result = this.parseAccessMemberOrMethodCall(result, start, false);
} else if (this.consumeOptionalOperator('?.')) {
result = this.parseAccessMemberOrMethodCall(result, start, true);
result = this.consumeOptionalCharacter(chars.$LBRACKET) ?
this.parseKeyedReadOrWrite(result, start, true) :
this.parseAccessMemberOrMethodCall(result, start, true);
} else if (this.consumeOptionalCharacter(chars.$LBRACKET)) {
this.withContext(ParseContextFlags.Writable, () => {
this.rbracketsExpected++;
const key = this.parsePipe();
if (key instanceof EmptyExpr) {
this.error(`Key access cannot be empty`);
}
this.rbracketsExpected--;
this.expectCharacter(chars.$RBRACKET);
if (this.consumeOptionalOperator('=')) {
const value = this.parseConditional();
result = new KeyedWrite(this.span(start), this.sourceSpan(start), result, key, value);
} else {
result = new KeyedRead(this.span(start), this.sourceSpan(start), result, key);
}
});
result = this.parseKeyedReadOrWrite(result, start, false);
} else if (this.consumeOptionalCharacter(chars.$LPAREN)) {
this.rparensExpected++;
const args = this.parseCallArguments();
@@ -954,7 +941,7 @@ export class _ParseAST {
return new LiteralMap(this.span(start), this.sourceSpan(start), keys, values);
}
parseAccessMemberOrMethodCall(receiver: AST, start: number, isSafe: boolean = false): AST {
parseAccessMemberOrMethodCall(receiver: AST, start: number, isSafe: boolean): AST {
const nameStart = this.inputIndex;
const id = this.withContext(ParseContextFlags.Writable, () => {
const id = this.expectIdentifierOrKeyword() ?? '';
@@ -1095,6 +1082,31 @@ export class _ParseAST {
return new TemplateBindingParseResult(bindings, [] /* warnings */, this.errors);
}
parseKeyedReadOrWrite(receiver: AST, start: number, isSafe: boolean): AST {
return this.withContext(ParseContextFlags.Writable, () => {
this.rbracketsExpected++;
const key = this.parsePipe();
if (key instanceof EmptyExpr) {
this.error(`Key access cannot be empty`);
}
this.rbracketsExpected--;
this.expectCharacter(chars.$RBRACKET);
if (this.consumeOptionalOperator('=')) {
if (isSafe) {
this.error('The \'?.\' operator cannot be used in the assignment');
} else {
const value = this.parseConditional();
return new KeyedWrite(this.span(start), this.sourceSpan(start), receiver, key, value);
}
} else {
return isSafe ? new SafeKeyedRead(this.span(start), this.sourceSpan(start), receiver, key) :
new KeyedRead(this.span(start), this.sourceSpan(start), receiver, key);
}
return new EmptyExpr(this.span(start), this.sourceSpan(start));
});
}
/**
* Parse a directive keyword, followed by a mandatory expression.
* For example, "of items", "trackBy: func".
@@ -1333,6 +1345,8 @@ class SimpleExpressionChecker implements AstVisitor {
visitChain(ast: Chain, context: any) {}
visitQuote(ast: Quote, context: any) {}
visitSafeKeyedRead(ast: SafeKeyedRead, context: any) {}
}
/**