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:
committed by
Jessica Janiuk
parent
47270d9e63
commit
ba084857ea
@@ -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) {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user