feat(compiler): record absolute span of template expressions in parser (#31897)
Currently, the spans of expressions are recorded only relative to the template node that they reside in, not their source file. Introduce a `sourceSpan` property on expression ASTs that records the location of an expression relative to the entire source code file that it is in. This may allow for reducing duplication of effort in ngtsc/typecheck/src/diagnostics later on as well. Child of #31898 PR Close #31897
This commit is contained in:
parent
68f06c8dd6
commit
b04488d692
|
@ -270,5 +270,5 @@ function parseParseSpanComment(commentText: string): ParseSpan|null {
|
|||
return null;
|
||||
}
|
||||
|
||||
return {start: +match[1], end: +match[2]};
|
||||
return new ParseSpan(+match[1], +match[2]);
|
||||
}
|
||||
|
|
|
@ -286,18 +286,20 @@ class _BuiltinAstConverter extends cdAst.AstTransformer {
|
|||
visitPipe(ast: cdAst.BindingPipe, context: any): any {
|
||||
const args = [ast.exp, ...ast.args].map(ast => ast.visit(this, context));
|
||||
return new BuiltinFunctionCall(
|
||||
ast.span, args, this._converterFactory.createPipeConverter(ast.name, args.length));
|
||||
ast.span, ast.sourceSpan, args,
|
||||
this._converterFactory.createPipeConverter(ast.name, args.length));
|
||||
}
|
||||
visitLiteralArray(ast: cdAst.LiteralArray, context: any): any {
|
||||
const args = ast.expressions.map(ast => ast.visit(this, context));
|
||||
return new BuiltinFunctionCall(
|
||||
ast.span, args, this._converterFactory.createLiteralArrayConverter(ast.expressions.length));
|
||||
ast.span, ast.sourceSpan, args,
|
||||
this._converterFactory.createLiteralArrayConverter(ast.expressions.length));
|
||||
}
|
||||
visitLiteralMap(ast: cdAst.LiteralMap, context: any): any {
|
||||
const args = ast.values.map(ast => ast.visit(this, context));
|
||||
|
||||
return new BuiltinFunctionCall(
|
||||
ast.span, args, this._converterFactory.createLiteralMapConverter(ast.keys));
|
||||
ast.span, ast.sourceSpan, args, this._converterFactory.createLiteralMapConverter(ast.keys));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -642,13 +644,14 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
|
|||
// leftMostNode with its unguarded version in the call to `this.visit()`.
|
||||
if (leftMostSafe instanceof cdAst.SafeMethodCall) {
|
||||
this._nodeMap.set(
|
||||
leftMostSafe,
|
||||
new cdAst.MethodCall(
|
||||
leftMostSafe.span, leftMostSafe.receiver, leftMostSafe.name, leftMostSafe.args));
|
||||
leftMostSafe, new cdAst.MethodCall(
|
||||
leftMostSafe.span, leftMostSafe.sourceSpan, leftMostSafe.receiver,
|
||||
leftMostSafe.name, leftMostSafe.args));
|
||||
} else {
|
||||
this._nodeMap.set(
|
||||
leftMostSafe,
|
||||
new cdAst.PropertyRead(leftMostSafe.span, leftMostSafe.receiver, leftMostSafe.name));
|
||||
leftMostSafe, new cdAst.PropertyRead(
|
||||
leftMostSafe.span, leftMostSafe.sourceSpan, leftMostSafe.receiver,
|
||||
leftMostSafe.name));
|
||||
}
|
||||
|
||||
// Recursively convert the node now without the guarded member access.
|
||||
|
@ -812,7 +815,9 @@ function convertStmtIntoExpression(stmt: o.Statement): o.Expression|null {
|
|||
}
|
||||
|
||||
export class BuiltinFunctionCall extends cdAst.FunctionCall {
|
||||
constructor(span: cdAst.ParseSpan, public args: cdAst.AST[], public converter: BuiltinConverter) {
|
||||
super(span, null, args);
|
||||
constructor(
|
||||
span: cdAst.ParseSpan, sourceSpan: cdAst.AbsoluteSourceSpan, public args: cdAst.AST[],
|
||||
public converter: BuiltinConverter) {
|
||||
super(span, sourceSpan, null, args);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,10 +19,18 @@ export class ParserError {
|
|||
|
||||
export class ParseSpan {
|
||||
constructor(public start: number, public end: number) {}
|
||||
toAbsolute(absoluteOffset: number): AbsoluteSourceSpan {
|
||||
return new AbsoluteSourceSpan(absoluteOffset + this.start, absoluteOffset + this.end);
|
||||
}
|
||||
}
|
||||
|
||||
export class AST {
|
||||
constructor(public span: ParseSpan) {}
|
||||
constructor(
|
||||
public span: ParseSpan,
|
||||
/**
|
||||
* Absolute location of the expression AST in a source code file.
|
||||
*/
|
||||
public sourceSpan: Readonly<AbsoluteSourceSpan>) {}
|
||||
visit(visitor: AstVisitor, context: any = null): any { return null; }
|
||||
toString(): string { return 'AST'; }
|
||||
}
|
||||
|
@ -42,9 +50,9 @@ export class AST {
|
|||
*/
|
||||
export class Quote extends AST {
|
||||
constructor(
|
||||
span: ParseSpan, public prefix: string, public uninterpretedExpression: string,
|
||||
public location: any) {
|
||||
super(span);
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public prefix: string,
|
||||
public uninterpretedExpression: string, public location: any) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitQuote(this, context); }
|
||||
toString(): string { return 'Quote'; }
|
||||
|
@ -66,13 +74,17 @@ export class ImplicitReceiver extends AST {
|
|||
* Multiple expressions separated by a semicolon.
|
||||
*/
|
||||
export class Chain extends AST {
|
||||
constructor(span: ParseSpan, public expressions: any[]) { super(span); }
|
||||
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public expressions: any[]) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitChain(this, context); }
|
||||
}
|
||||
|
||||
export class Conditional extends AST {
|
||||
constructor(span: ParseSpan, public condition: AST, public trueExp: AST, public falseExp: AST) {
|
||||
super(span);
|
||||
constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public condition: AST, public trueExp: AST,
|
||||
public falseExp: AST) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitConditional(this, context);
|
||||
|
@ -80,15 +92,20 @@ export class Conditional extends AST {
|
|||
}
|
||||
|
||||
export class PropertyRead extends AST {
|
||||
constructor(span: ParseSpan, public receiver: AST, public name: string) { super(span); }
|
||||
constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public receiver: AST, public name: string) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitPropertyRead(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class PropertyWrite extends AST {
|
||||
constructor(span: ParseSpan, public receiver: AST, public name: string, public value: AST) {
|
||||
super(span);
|
||||
constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public receiver: AST, public name: string,
|
||||
public value: AST) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitPropertyWrite(this, context);
|
||||
|
@ -96,42 +113,57 @@ export class PropertyWrite extends AST {
|
|||
}
|
||||
|
||||
export class SafePropertyRead extends AST {
|
||||
constructor(span: ParseSpan, public receiver: AST, public name: string) { super(span); }
|
||||
constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public receiver: AST, public name: string) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitSafePropertyRead(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyedRead extends AST {
|
||||
constructor(span: ParseSpan, public obj: AST, public key: AST) { super(span); }
|
||||
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public obj: AST, public key: AST) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitKeyedRead(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyedWrite extends AST {
|
||||
constructor(span: ParseSpan, public obj: AST, public key: AST, public value: AST) { super(span); }
|
||||
constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public obj: AST, public key: AST,
|
||||
public value: AST) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitKeyedWrite(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class BindingPipe extends AST {
|
||||
constructor(span: ParseSpan, public exp: AST, public name: string, public args: any[]) {
|
||||
super(span);
|
||||
constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public exp: AST, public name: string,
|
||||
public args: any[]) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitPipe(this, context); }
|
||||
}
|
||||
|
||||
export class LiteralPrimitive extends AST {
|
||||
constructor(span: ParseSpan, public value: any) { super(span); }
|
||||
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public value: any) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitLiteralPrimitive(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class LiteralArray extends AST {
|
||||
constructor(span: ParseSpan, public expressions: any[]) { super(span); }
|
||||
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public expressions: any[]) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitLiteralArray(this, context);
|
||||
}
|
||||
|
@ -142,22 +174,32 @@ export type LiteralMapKey = {
|
|||
};
|
||||
|
||||
export class LiteralMap extends AST {
|
||||
constructor(span: ParseSpan, public keys: LiteralMapKey[], public values: any[]) { super(span); }
|
||||
constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public keys: LiteralMapKey[],
|
||||
public values: any[]) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitLiteralMap(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class Interpolation extends AST {
|
||||
constructor(span: ParseSpan, public strings: any[], public expressions: any[]) { super(span); }
|
||||
constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public strings: any[],
|
||||
public expressions: any[]) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitInterpolation(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class Binary extends AST {
|
||||
constructor(span: ParseSpan, public operation: string, public left: AST, public right: AST) {
|
||||
super(span);
|
||||
constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public operation: string, public left: AST,
|
||||
public right: AST) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitBinary(this, context);
|
||||
|
@ -165,22 +207,28 @@ export class Binary extends AST {
|
|||
}
|
||||
|
||||
export class PrefixNot extends AST {
|
||||
constructor(span: ParseSpan, public expression: AST) { super(span); }
|
||||
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public expression: AST) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitPrefixNot(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class NonNullAssert extends AST {
|
||||
constructor(span: ParseSpan, public expression: AST) { super(span); }
|
||||
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public expression: AST) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitNonNullAssert(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class MethodCall extends AST {
|
||||
constructor(span: ParseSpan, public receiver: AST, public name: string, public args: any[]) {
|
||||
super(span);
|
||||
constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public receiver: AST, public name: string,
|
||||
public args: any[]) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitMethodCall(this, context);
|
||||
|
@ -188,8 +236,10 @@ export class MethodCall extends AST {
|
|||
}
|
||||
|
||||
export class SafeMethodCall extends AST {
|
||||
constructor(span: ParseSpan, public receiver: AST, public name: string, public args: any[]) {
|
||||
super(span);
|
||||
constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public receiver: AST, public name: string,
|
||||
public args: any[]) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitSafeMethodCall(this, context);
|
||||
|
@ -197,7 +247,11 @@ export class SafeMethodCall extends AST {
|
|||
}
|
||||
|
||||
export class FunctionCall extends AST {
|
||||
constructor(span: ParseSpan, public target: AST|null, public args: any[]) { super(span); }
|
||||
constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public target: AST|null,
|
||||
public args: any[]) {
|
||||
super(span, sourceSpan);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitFunctionCall(this, context);
|
||||
}
|
||||
|
@ -208,16 +262,17 @@ export class FunctionCall extends AST {
|
|||
* starting and ending byte offsets, respectively, of the text span in a source file.
|
||||
*/
|
||||
export class AbsoluteSourceSpan {
|
||||
constructor(public start: number, public end: number) {}
|
||||
constructor(public readonly start: number, public readonly end: number) {}
|
||||
}
|
||||
|
||||
export class ASTWithSource extends AST {
|
||||
public sourceSpan: AbsoluteSourceSpan;
|
||||
constructor(
|
||||
public ast: AST, public source: string|null, public location: string, absoluteOffset: number,
|
||||
public errors: ParserError[]) {
|
||||
super(new ParseSpan(0, source == null ? 0 : source.length));
|
||||
this.sourceSpan = new AbsoluteSourceSpan(absoluteOffset, absoluteOffset + this.span.end);
|
||||
super(
|
||||
new ParseSpan(0, source === null ? 0 : source.length),
|
||||
new AbsoluteSourceSpan(
|
||||
absoluteOffset, source === null ? absoluteOffset : absoluteOffset + source.length));
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
if (visitor.visitASTWithSource) {
|
||||
|
@ -230,8 +285,8 @@ export class ASTWithSource extends AST {
|
|||
|
||||
export class TemplateBinding {
|
||||
constructor(
|
||||
public span: ParseSpan, public key: string, public keyIsVar: boolean, public name: string,
|
||||
public expression: ASTWithSource|null) {}
|
||||
public span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public key: string,
|
||||
public keyIsVar: boolean, public name: string, public expression: ASTWithSource|null) {}
|
||||
}
|
||||
|
||||
export interface AstVisitor {
|
||||
|
@ -365,74 +420,80 @@ export class AstTransformer implements AstVisitor {
|
|||
visitImplicitReceiver(ast: ImplicitReceiver, context: any): AST { return ast; }
|
||||
|
||||
visitInterpolation(ast: Interpolation, context: any): AST {
|
||||
return new Interpolation(ast.span, ast.strings, this.visitAll(ast.expressions));
|
||||
return new Interpolation(ast.span, ast.sourceSpan, ast.strings, this.visitAll(ast.expressions));
|
||||
}
|
||||
|
||||
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): AST {
|
||||
return new LiteralPrimitive(ast.span, ast.value);
|
||||
return new LiteralPrimitive(ast.span, ast.sourceSpan, ast.value);
|
||||
}
|
||||
|
||||
visitPropertyRead(ast: PropertyRead, context: any): AST {
|
||||
return new PropertyRead(ast.span, ast.receiver.visit(this), ast.name);
|
||||
return new PropertyRead(ast.span, ast.sourceSpan, ast.receiver.visit(this), ast.name);
|
||||
}
|
||||
|
||||
visitPropertyWrite(ast: PropertyWrite, context: any): AST {
|
||||
return new PropertyWrite(ast.span, ast.receiver.visit(this), ast.name, ast.value.visit(this));
|
||||
return new PropertyWrite(
|
||||
ast.span, ast.sourceSpan, ast.receiver.visit(this), ast.name, ast.value.visit(this));
|
||||
}
|
||||
|
||||
visitSafePropertyRead(ast: SafePropertyRead, context: any): AST {
|
||||
return new SafePropertyRead(ast.span, ast.receiver.visit(this), ast.name);
|
||||
return new SafePropertyRead(ast.span, ast.sourceSpan, ast.receiver.visit(this), ast.name);
|
||||
}
|
||||
|
||||
visitMethodCall(ast: MethodCall, context: any): AST {
|
||||
return new MethodCall(ast.span, ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
|
||||
return new MethodCall(
|
||||
ast.span, ast.sourceSpan, ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
|
||||
}
|
||||
|
||||
visitSafeMethodCall(ast: SafeMethodCall, context: any): AST {
|
||||
return new SafeMethodCall(
|
||||
ast.span, ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
|
||||
ast.span, ast.sourceSpan, ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
|
||||
}
|
||||
|
||||
visitFunctionCall(ast: FunctionCall, context: any): AST {
|
||||
return new FunctionCall(ast.span, ast.target !.visit(this), this.visitAll(ast.args));
|
||||
return new FunctionCall(
|
||||
ast.span, ast.sourceSpan, ast.target !.visit(this), this.visitAll(ast.args));
|
||||
}
|
||||
|
||||
visitLiteralArray(ast: LiteralArray, context: any): AST {
|
||||
return new LiteralArray(ast.span, this.visitAll(ast.expressions));
|
||||
return new LiteralArray(ast.span, ast.sourceSpan, this.visitAll(ast.expressions));
|
||||
}
|
||||
|
||||
visitLiteralMap(ast: LiteralMap, context: any): AST {
|
||||
return new LiteralMap(ast.span, ast.keys, this.visitAll(ast.values));
|
||||
return new LiteralMap(ast.span, ast.sourceSpan, ast.keys, this.visitAll(ast.values));
|
||||
}
|
||||
|
||||
visitBinary(ast: Binary, context: any): AST {
|
||||
return new Binary(ast.span, ast.operation, ast.left.visit(this), ast.right.visit(this));
|
||||
return new Binary(
|
||||
ast.span, ast.sourceSpan, ast.operation, ast.left.visit(this), ast.right.visit(this));
|
||||
}
|
||||
|
||||
visitPrefixNot(ast: PrefixNot, context: any): AST {
|
||||
return new PrefixNot(ast.span, ast.expression.visit(this));
|
||||
return new PrefixNot(ast.span, ast.sourceSpan, ast.expression.visit(this));
|
||||
}
|
||||
|
||||
visitNonNullAssert(ast: NonNullAssert, context: any): AST {
|
||||
return new NonNullAssert(ast.span, ast.expression.visit(this));
|
||||
return new NonNullAssert(ast.span, ast.sourceSpan, ast.expression.visit(this));
|
||||
}
|
||||
|
||||
visitConditional(ast: Conditional, context: any): AST {
|
||||
return new Conditional(
|
||||
ast.span, ast.condition.visit(this), ast.trueExp.visit(this), ast.falseExp.visit(this));
|
||||
ast.span, ast.sourceSpan, ast.condition.visit(this), ast.trueExp.visit(this),
|
||||
ast.falseExp.visit(this));
|
||||
}
|
||||
|
||||
visitPipe(ast: BindingPipe, context: any): AST {
|
||||
return new BindingPipe(ast.span, ast.exp.visit(this), ast.name, this.visitAll(ast.args));
|
||||
return new BindingPipe(
|
||||
ast.span, ast.sourceSpan, ast.exp.visit(this), ast.name, this.visitAll(ast.args));
|
||||
}
|
||||
|
||||
visitKeyedRead(ast: KeyedRead, context: any): AST {
|
||||
return new KeyedRead(ast.span, ast.obj.visit(this), ast.key.visit(this));
|
||||
return new KeyedRead(ast.span, ast.sourceSpan, ast.obj.visit(this), ast.key.visit(this));
|
||||
}
|
||||
|
||||
visitKeyedWrite(ast: KeyedWrite, context: any): AST {
|
||||
return new KeyedWrite(
|
||||
ast.span, ast.obj.visit(this), ast.key.visit(this), ast.value.visit(this));
|
||||
ast.span, ast.sourceSpan, ast.obj.visit(this), ast.key.visit(this), ast.value.visit(this));
|
||||
}
|
||||
|
||||
visitAll(asts: any[]): any[] {
|
||||
|
@ -444,11 +505,12 @@ export class AstTransformer implements AstVisitor {
|
|||
}
|
||||
|
||||
visitChain(ast: Chain, context: any): AST {
|
||||
return new Chain(ast.span, this.visitAll(ast.expressions));
|
||||
return new Chain(ast.span, ast.sourceSpan, this.visitAll(ast.expressions));
|
||||
}
|
||||
|
||||
visitQuote(ast: Quote, context: any): AST {
|
||||
return new Quote(ast.span, ast.prefix, ast.uninterpretedExpression, ast.location);
|
||||
return new Quote(
|
||||
ast.span, ast.sourceSpan, ast.prefix, ast.uninterpretedExpression, ast.location);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -460,7 +522,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
visitInterpolation(ast: Interpolation, context: any): Interpolation {
|
||||
const expressions = this.visitAll(ast.expressions);
|
||||
if (expressions !== ast.expressions)
|
||||
return new Interpolation(ast.span, ast.strings, expressions);
|
||||
return new Interpolation(ast.span, ast.sourceSpan, ast.strings, expressions);
|
||||
return ast;
|
||||
}
|
||||
|
||||
|
@ -469,7 +531,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
visitPropertyRead(ast: PropertyRead, context: any): AST {
|
||||
const receiver = ast.receiver.visit(this);
|
||||
if (receiver !== ast.receiver) {
|
||||
return new PropertyRead(ast.span, receiver, ast.name);
|
||||
return new PropertyRead(ast.span, ast.sourceSpan, receiver, ast.name);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -478,7 +540,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
const receiver = ast.receiver.visit(this);
|
||||
const value = ast.value.visit(this);
|
||||
if (receiver !== ast.receiver || value !== ast.value) {
|
||||
return new PropertyWrite(ast.span, receiver, ast.name, value);
|
||||
return new PropertyWrite(ast.span, ast.sourceSpan, receiver, ast.name, value);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -486,7 +548,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
visitSafePropertyRead(ast: SafePropertyRead, context: any): AST {
|
||||
const receiver = ast.receiver.visit(this);
|
||||
if (receiver !== ast.receiver) {
|
||||
return new SafePropertyRead(ast.span, receiver, ast.name);
|
||||
return new SafePropertyRead(ast.span, ast.sourceSpan, receiver, ast.name);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -495,7 +557,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
const receiver = ast.receiver.visit(this);
|
||||
const args = this.visitAll(ast.args);
|
||||
if (receiver !== ast.receiver || args !== ast.args) {
|
||||
return new MethodCall(ast.span, receiver, ast.name, args);
|
||||
return new MethodCall(ast.span, ast.sourceSpan, receiver, ast.name, args);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -504,7 +566,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
const receiver = ast.receiver.visit(this);
|
||||
const args = this.visitAll(ast.args);
|
||||
if (receiver !== ast.receiver || args !== ast.args) {
|
||||
return new SafeMethodCall(ast.span, receiver, ast.name, args);
|
||||
return new SafeMethodCall(ast.span, ast.sourceSpan, receiver, ast.name, args);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -513,7 +575,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
const target = ast.target && ast.target.visit(this);
|
||||
const args = this.visitAll(ast.args);
|
||||
if (target !== ast.target || args !== ast.args) {
|
||||
return new FunctionCall(ast.span, target, args);
|
||||
return new FunctionCall(ast.span, ast.sourceSpan, target, args);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -521,7 +583,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
visitLiteralArray(ast: LiteralArray, context: any): AST {
|
||||
const expressions = this.visitAll(ast.expressions);
|
||||
if (expressions !== ast.expressions) {
|
||||
return new LiteralArray(ast.span, expressions);
|
||||
return new LiteralArray(ast.span, ast.sourceSpan, expressions);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -529,7 +591,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
visitLiteralMap(ast: LiteralMap, context: any): AST {
|
||||
const values = this.visitAll(ast.values);
|
||||
if (values !== ast.values) {
|
||||
return new LiteralMap(ast.span, ast.keys, values);
|
||||
return new LiteralMap(ast.span, ast.sourceSpan, ast.keys, values);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -538,7 +600,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
const left = ast.left.visit(this);
|
||||
const right = ast.right.visit(this);
|
||||
if (left !== ast.left || right !== ast.right) {
|
||||
return new Binary(ast.span, ast.operation, left, right);
|
||||
return new Binary(ast.span, ast.sourceSpan, ast.operation, left, right);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -546,7 +608,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
visitPrefixNot(ast: PrefixNot, context: any): AST {
|
||||
const expression = ast.expression.visit(this);
|
||||
if (expression !== ast.expression) {
|
||||
return new PrefixNot(ast.span, expression);
|
||||
return new PrefixNot(ast.span, ast.sourceSpan, expression);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -554,7 +616,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
visitNonNullAssert(ast: NonNullAssert, context: any): AST {
|
||||
const expression = ast.expression.visit(this);
|
||||
if (expression !== ast.expression) {
|
||||
return new NonNullAssert(ast.span, expression);
|
||||
return new NonNullAssert(ast.span, ast.sourceSpan, expression);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -564,7 +626,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
const trueExp = ast.trueExp.visit(this);
|
||||
const falseExp = ast.falseExp.visit(this);
|
||||
if (condition !== ast.condition || trueExp !== ast.trueExp || falseExp !== ast.falseExp) {
|
||||
return new Conditional(ast.span, condition, trueExp, falseExp);
|
||||
return new Conditional(ast.span, ast.sourceSpan, condition, trueExp, falseExp);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -573,7 +635,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
const exp = ast.exp.visit(this);
|
||||
const args = this.visitAll(ast.args);
|
||||
if (exp !== ast.exp || args !== ast.args) {
|
||||
return new BindingPipe(ast.span, exp, ast.name, args);
|
||||
return new BindingPipe(ast.span, ast.sourceSpan, exp, ast.name, args);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -582,7 +644,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
const obj = ast.obj.visit(this);
|
||||
const key = ast.key.visit(this);
|
||||
if (obj !== ast.obj || key !== ast.key) {
|
||||
return new KeyedRead(ast.span, obj, key);
|
||||
return new KeyedRead(ast.span, ast.sourceSpan, obj, key);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -592,7 +654,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
const key = ast.key.visit(this);
|
||||
const value = ast.value.visit(this);
|
||||
if (obj !== ast.obj || key !== ast.key || value !== ast.value) {
|
||||
return new KeyedWrite(ast.span, obj, key, value);
|
||||
return new KeyedWrite(ast.span, ast.sourceSpan, obj, key, value);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
@ -612,7 +674,7 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
visitChain(ast: Chain, context: any): AST {
|
||||
const expressions = this.visitAll(ast.expressions);
|
||||
if (expressions !== ast.expressions) {
|
||||
return new Chain(ast.span, expressions);
|
||||
return new Chain(ast.span, ast.sourceSpan, expressions);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import * as chars from '../chars';
|
|||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
|
||||
import {escapeRegExp} from '../util';
|
||||
|
||||
import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParseSpan, ParserError, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, TemplateBinding} from './ast';
|
||||
import {AST, ASTWithSource, AbsoluteSourceSpan, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParseSpan, ParserError, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, TemplateBinding} from './ast';
|
||||
import {EOF, Lexer, Token, TokenType, isIdentifier, isQuote} from './lexer';
|
||||
|
||||
export class SplitInterpolation {
|
||||
|
@ -74,7 +74,7 @@ export class Parser {
|
|||
interpolationConfig: InterpolationConfig): AST {
|
||||
// Quotes expressions use 3rd-party expression language. We don't want to use
|
||||
// our lexer or parser for that, so we check for that ahead of time.
|
||||
const quote = this._parseQuote(input, location);
|
||||
const quote = this._parseQuote(input, location, absoluteOffset);
|
||||
|
||||
if (quote != null) {
|
||||
return quote;
|
||||
|
@ -89,14 +89,16 @@ export class Parser {
|
|||
.parseChain();
|
||||
}
|
||||
|
||||
private _parseQuote(input: string|null, location: any): AST|null {
|
||||
private _parseQuote(input: string|null, location: any, absoluteOffset: number): AST|null {
|
||||
if (input == null) return null;
|
||||
const prefixSeparatorIndex = input.indexOf(':');
|
||||
if (prefixSeparatorIndex == -1) return null;
|
||||
const prefix = input.substring(0, prefixSeparatorIndex).trim();
|
||||
if (!isIdentifier(prefix)) return null;
|
||||
const uninterpretedExpression = input.substring(prefixSeparatorIndex + 1);
|
||||
return new Quote(new ParseSpan(0, input.length), prefix, uninterpretedExpression, location);
|
||||
const span = new ParseSpan(0, input.length);
|
||||
return new Quote(
|
||||
span, span.toAbsolute(absoluteOffset), prefix, uninterpretedExpression, location);
|
||||
}
|
||||
|
||||
parseTemplateBindings(tplKey: string, tplValue: string, location: any, absoluteOffset: number):
|
||||
|
@ -126,10 +128,10 @@ export class Parser {
|
|||
expressions.push(ast);
|
||||
}
|
||||
|
||||
const span = new ParseSpan(0, input == null ? 0 : input.length);
|
||||
return new ASTWithSource(
|
||||
new Interpolation(
|
||||
new ParseSpan(0, input == null ? 0 : input.length), split.strings, expressions),
|
||||
input, location, absoluteOffset, this.errors);
|
||||
new Interpolation(span, span.toAbsolute(absoluteOffset), split.strings, expressions), input,
|
||||
location, absoluteOffset, this.errors);
|
||||
}
|
||||
|
||||
splitInterpolation(
|
||||
|
@ -169,9 +171,10 @@ export class Parser {
|
|||
}
|
||||
|
||||
wrapLiteralPrimitive(input: string|null, location: any, absoluteOffset: number): ASTWithSource {
|
||||
const span = new ParseSpan(0, input == null ? 0 : input.length);
|
||||
return new ASTWithSource(
|
||||
new LiteralPrimitive(new ParseSpan(0, input == null ? 0 : input.length), input), input,
|
||||
location, absoluteOffset, this.errors);
|
||||
new LiteralPrimitive(span, span.toAbsolute(absoluteOffset), input), input, location,
|
||||
absoluteOffset, this.errors);
|
||||
}
|
||||
|
||||
private _stripComments(input: string): string {
|
||||
|
@ -227,6 +230,12 @@ export class _ParseAST {
|
|||
private rbracketsExpected = 0;
|
||||
private rbracesExpected = 0;
|
||||
|
||||
// Cache of expression start and input indeces to the absolute source span they map to, used to
|
||||
// prevent creating superfluous source spans in `sourceSpan`.
|
||||
// A serial of the expression start and input index is used for mapping because both are stateful
|
||||
// and may change for subsequent expressions visited by the parser.
|
||||
private sourceSpanCache = new Map<string, AbsoluteSourceSpan>();
|
||||
|
||||
index: number = 0;
|
||||
|
||||
constructor(
|
||||
|
@ -248,6 +257,14 @@ export class _ParseAST {
|
|||
|
||||
span(start: number) { return new ParseSpan(start, this.inputIndex); }
|
||||
|
||||
sourceSpan(start: number): AbsoluteSourceSpan {
|
||||
const serial = `${start}@${this.inputIndex}`;
|
||||
if (!this.sourceSpanCache.has(serial)) {
|
||||
this.sourceSpanCache.set(serial, this.span(start).toAbsolute(this.absoluteOffset));
|
||||
}
|
||||
return this.sourceSpanCache.get(serial) !;
|
||||
}
|
||||
|
||||
advance() { this.index++; }
|
||||
|
||||
optionalCharacter(code: number): boolean {
|
||||
|
@ -318,9 +335,9 @@ export class _ParseAST {
|
|||
this.error(`Unexpected token '${this.next}'`);
|
||||
}
|
||||
}
|
||||
if (exprs.length == 0) return new EmptyExpr(this.span(start));
|
||||
if (exprs.length == 0) return new EmptyExpr(this.span(start), this.sourceSpan(start));
|
||||
if (exprs.length == 1) return exprs[0];
|
||||
return new Chain(this.span(start), exprs);
|
||||
return new Chain(this.span(start), this.sourceSpan(start), exprs);
|
||||
}
|
||||
|
||||
parsePipe(): AST {
|
||||
|
@ -336,7 +353,8 @@ export class _ParseAST {
|
|||
while (this.optionalCharacter(chars.$COLON)) {
|
||||
args.push(this.parseExpression());
|
||||
}
|
||||
result = new BindingPipe(this.span(result.span.start), result, name, args);
|
||||
const {start} = result.span;
|
||||
result = new BindingPipe(this.span(start), this.sourceSpan(start), result, name, args);
|
||||
} while (this.optionalOperator('|'));
|
||||
}
|
||||
|
||||
|
@ -356,11 +374,11 @@ export class _ParseAST {
|
|||
const end = this.inputIndex;
|
||||
const expression = this.input.substring(start, end);
|
||||
this.error(`Conditional expression ${expression} requires all 3 expressions`);
|
||||
no = new EmptyExpr(this.span(start));
|
||||
no = new EmptyExpr(this.span(start), this.sourceSpan(start));
|
||||
} else {
|
||||
no = this.parsePipe();
|
||||
}
|
||||
return new Conditional(this.span(start), result, yes, no);
|
||||
return new Conditional(this.span(start), this.sourceSpan(start), result, yes, no);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
|
@ -371,7 +389,8 @@ export class _ParseAST {
|
|||
let result = this.parseLogicalAnd();
|
||||
while (this.optionalOperator('||')) {
|
||||
const right = this.parseLogicalAnd();
|
||||
result = new Binary(this.span(result.span.start), '||', result, right);
|
||||
const {start} = result.span;
|
||||
result = new Binary(this.span(start), this.sourceSpan(start), '||', result, right);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -381,7 +400,8 @@ export class _ParseAST {
|
|||
let result = this.parseEquality();
|
||||
while (this.optionalOperator('&&')) {
|
||||
const right = this.parseEquality();
|
||||
result = new Binary(this.span(result.span.start), '&&', result, right);
|
||||
const {start} = result.span;
|
||||
result = new Binary(this.span(start), this.sourceSpan(start), '&&', result, right);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -398,7 +418,8 @@ export class _ParseAST {
|
|||
case '!==':
|
||||
this.advance();
|
||||
const right = this.parseRelational();
|
||||
result = new Binary(this.span(result.span.start), operator, result, right);
|
||||
const {start} = result.span;
|
||||
result = new Binary(this.span(start), this.sourceSpan(start), operator, result, right);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
@ -418,7 +439,8 @@ export class _ParseAST {
|
|||
case '>=':
|
||||
this.advance();
|
||||
const right = this.parseAdditive();
|
||||
result = new Binary(this.span(result.span.start), operator, result, right);
|
||||
const {start} = result.span;
|
||||
result = new Binary(this.span(start), this.sourceSpan(start), operator, result, right);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
@ -436,7 +458,8 @@ export class _ParseAST {
|
|||
case '-':
|
||||
this.advance();
|
||||
let right = this.parseMultiplicative();
|
||||
result = new Binary(this.span(result.span.start), operator, result, right);
|
||||
const {start} = result.span;
|
||||
result = new Binary(this.span(start), this.sourceSpan(start), operator, result, right);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
@ -455,7 +478,8 @@ export class _ParseAST {
|
|||
case '/':
|
||||
this.advance();
|
||||
let right = this.parsePrefix();
|
||||
result = new Binary(this.span(result.span.start), operator, result, right);
|
||||
const {start} = result.span;
|
||||
result = new Binary(this.span(start), this.sourceSpan(start), operator, result, right);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
@ -467,23 +491,26 @@ export class _ParseAST {
|
|||
if (this.next.type == TokenType.Operator) {
|
||||
const start = this.inputIndex;
|
||||
const operator = this.next.strValue;
|
||||
const literalSpan = new ParseSpan(start, start);
|
||||
const literalSourceSpan = literalSpan.toAbsolute(this.absoluteOffset);
|
||||
let result: AST;
|
||||
switch (operator) {
|
||||
case '+':
|
||||
this.advance();
|
||||
result = this.parsePrefix();
|
||||
return new Binary(
|
||||
this.span(start), '-', result, new LiteralPrimitive(new ParseSpan(start, start), 0));
|
||||
this.span(start), this.sourceSpan(start), '-', result,
|
||||
new LiteralPrimitive(literalSpan, literalSourceSpan, 0));
|
||||
case '-':
|
||||
this.advance();
|
||||
result = this.parsePrefix();
|
||||
return new Binary(
|
||||
this.span(start), operator, new LiteralPrimitive(new ParseSpan(start, start), 0),
|
||||
result);
|
||||
this.span(start), this.sourceSpan(start), operator,
|
||||
new LiteralPrimitive(literalSpan, literalSourceSpan, 0), result);
|
||||
case '!':
|
||||
this.advance();
|
||||
result = this.parsePrefix();
|
||||
return new PrefixNot(this.span(start), result);
|
||||
return new PrefixNot(this.span(start), this.sourceSpan(start), result);
|
||||
}
|
||||
}
|
||||
return this.parseCallChain();
|
||||
|
@ -491,6 +518,7 @@ export class _ParseAST {
|
|||
|
||||
parseCallChain(): AST {
|
||||
let result = this.parsePrimary();
|
||||
const resultStart = result.span.start;
|
||||
while (true) {
|
||||
if (this.optionalCharacter(chars.$PERIOD)) {
|
||||
result = this.parseAccessMemberOrMethodCall(result, false);
|
||||
|
@ -505,9 +533,10 @@ export class _ParseAST {
|
|||
this.expectCharacter(chars.$RBRACKET);
|
||||
if (this.optionalOperator('=')) {
|
||||
const value = this.parseConditional();
|
||||
result = new KeyedWrite(this.span(result.span.start), result, key, value);
|
||||
result = new KeyedWrite(
|
||||
this.span(resultStart), this.sourceSpan(resultStart), result, key, value);
|
||||
} else {
|
||||
result = new KeyedRead(this.span(result.span.start), result, key);
|
||||
result = new KeyedRead(this.span(resultStart), this.sourceSpan(resultStart), result, key);
|
||||
}
|
||||
|
||||
} else if (this.optionalCharacter(chars.$LPAREN)) {
|
||||
|
@ -515,10 +544,11 @@ export class _ParseAST {
|
|||
const args = this.parseCallArguments();
|
||||
this.rparensExpected--;
|
||||
this.expectCharacter(chars.$RPAREN);
|
||||
result = new FunctionCall(this.span(result.span.start), result, args);
|
||||
result =
|
||||
new FunctionCall(this.span(resultStart), this.sourceSpan(resultStart), result, args);
|
||||
|
||||
} else if (this.optionalOperator('!')) {
|
||||
result = new NonNullAssert(this.span(result.span.start), result);
|
||||
result = new NonNullAssert(this.span(resultStart), this.sourceSpan(resultStart), result);
|
||||
|
||||
} else {
|
||||
return result;
|
||||
|
@ -537,53 +567,54 @@ export class _ParseAST {
|
|||
|
||||
} else if (this.next.isKeywordNull()) {
|
||||
this.advance();
|
||||
return new LiteralPrimitive(this.span(start), null);
|
||||
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), null);
|
||||
|
||||
} else if (this.next.isKeywordUndefined()) {
|
||||
this.advance();
|
||||
return new LiteralPrimitive(this.span(start), void 0);
|
||||
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), void 0);
|
||||
|
||||
} else if (this.next.isKeywordTrue()) {
|
||||
this.advance();
|
||||
return new LiteralPrimitive(this.span(start), true);
|
||||
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), true);
|
||||
|
||||
} else if (this.next.isKeywordFalse()) {
|
||||
this.advance();
|
||||
return new LiteralPrimitive(this.span(start), false);
|
||||
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), false);
|
||||
|
||||
} else if (this.next.isKeywordThis()) {
|
||||
this.advance();
|
||||
return new ImplicitReceiver(this.span(start));
|
||||
return new ImplicitReceiver(this.span(start), this.sourceSpan(start));
|
||||
|
||||
} else if (this.optionalCharacter(chars.$LBRACKET)) {
|
||||
this.rbracketsExpected++;
|
||||
const elements = this.parseExpressionList(chars.$RBRACKET);
|
||||
this.rbracketsExpected--;
|
||||
this.expectCharacter(chars.$RBRACKET);
|
||||
return new LiteralArray(this.span(start), elements);
|
||||
return new LiteralArray(this.span(start), this.sourceSpan(start), elements);
|
||||
|
||||
} else if (this.next.isCharacter(chars.$LBRACE)) {
|
||||
return this.parseLiteralMap();
|
||||
|
||||
} else if (this.next.isIdentifier()) {
|
||||
return this.parseAccessMemberOrMethodCall(new ImplicitReceiver(this.span(start)), false);
|
||||
return this.parseAccessMemberOrMethodCall(
|
||||
new ImplicitReceiver(this.span(start), this.sourceSpan(start)), false);
|
||||
|
||||
} else if (this.next.isNumber()) {
|
||||
const value = this.next.toNumber();
|
||||
this.advance();
|
||||
return new LiteralPrimitive(this.span(start), value);
|
||||
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), value);
|
||||
|
||||
} else if (this.next.isString()) {
|
||||
const literalValue = this.next.toString();
|
||||
this.advance();
|
||||
return new LiteralPrimitive(this.span(start), literalValue);
|
||||
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), literalValue);
|
||||
|
||||
} else if (this.index >= this.tokens.length) {
|
||||
this.error(`Unexpected end of expression: ${this.input}`);
|
||||
return new EmptyExpr(this.span(start));
|
||||
return new EmptyExpr(this.span(start), this.sourceSpan(start));
|
||||
} else {
|
||||
this.error(`Unexpected token ${this.next}`);
|
||||
return new EmptyExpr(this.span(start));
|
||||
return new EmptyExpr(this.span(start), this.sourceSpan(start));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -614,7 +645,7 @@ export class _ParseAST {
|
|||
this.rbracesExpected--;
|
||||
this.expectCharacter(chars.$RBRACE);
|
||||
}
|
||||
return new LiteralMap(this.span(start), keys, values);
|
||||
return new LiteralMap(this.span(start), this.sourceSpan(start), keys, values);
|
||||
}
|
||||
|
||||
parseAccessMemberOrMethodCall(receiver: AST, isSafe: boolean = false): AST {
|
||||
|
@ -627,28 +658,30 @@ export class _ParseAST {
|
|||
this.expectCharacter(chars.$RPAREN);
|
||||
this.rparensExpected--;
|
||||
const span = this.span(start);
|
||||
return isSafe ? new SafeMethodCall(span, receiver, id, args) :
|
||||
new MethodCall(span, receiver, id, args);
|
||||
const sourceSpan = this.sourceSpan(start);
|
||||
return isSafe ? new SafeMethodCall(span, sourceSpan, receiver, id, args) :
|
||||
new MethodCall(span, sourceSpan, receiver, id, args);
|
||||
|
||||
} else {
|
||||
if (isSafe) {
|
||||
if (this.optionalOperator('=')) {
|
||||
this.error('The \'?.\' operator cannot be used in the assignment');
|
||||
return new EmptyExpr(this.span(start));
|
||||
return new EmptyExpr(this.span(start), this.sourceSpan(start));
|
||||
} else {
|
||||
return new SafePropertyRead(this.span(start), receiver, id);
|
||||
return new SafePropertyRead(this.span(start), this.sourceSpan(start), receiver, id);
|
||||
}
|
||||
} else {
|
||||
if (this.optionalOperator('=')) {
|
||||
if (!this.parseAction) {
|
||||
this.error('Bindings cannot contain assignments');
|
||||
return new EmptyExpr(this.span(start));
|
||||
return new EmptyExpr(this.span(start), this.sourceSpan(start));
|
||||
}
|
||||
|
||||
const value = this.parseConditional();
|
||||
return new PropertyWrite(this.span(start), receiver, id, value);
|
||||
return new PropertyWrite(this.span(start), this.sourceSpan(start), receiver, id, value);
|
||||
} else {
|
||||
return new PropertyRead(this.span(start), receiver, id);
|
||||
const span = this.span(start);
|
||||
return new PropertyRead(this.span(start), this.sourceSpan(start), receiver, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -722,12 +755,14 @@ export class _ParseAST {
|
|||
new ASTWithSource(ast, source, this.location, this.absoluteOffset, this.errors);
|
||||
}
|
||||
|
||||
bindings.push(new TemplateBinding(this.span(start), key, isVar, name, expression));
|
||||
bindings.push(new TemplateBinding(
|
||||
this.span(start), this.sourceSpan(start), key, isVar, name, expression));
|
||||
if (this.peekKeywordAs() && !isVar) {
|
||||
const letStart = this.inputIndex;
|
||||
this.advance(); // consume `as`
|
||||
const letName = this.expectTemplateBindingKey(); // read local var name
|
||||
bindings.push(new TemplateBinding(this.span(letStart), letName, true, key, null !));
|
||||
bindings.push(new TemplateBinding(
|
||||
this.span(letStart), this.sourceSpan(letStart), letName, true, key, null !));
|
||||
}
|
||||
if (!this.optionalCharacter(chars.$SEMICOLON)) {
|
||||
this.optionalCharacter(chars.$COMMA);
|
||||
|
|
|
@ -1372,16 +1372,19 @@ export class ValueConverter extends AstMemoryEfficientTransformer {
|
|||
const slotPseudoLocal = `PIPE:${slot}`;
|
||||
// Allocate one slot for the result plus one slot per pipe argument
|
||||
const pureFunctionSlot = this.allocatePureFunctionSlots(2 + pipe.args.length);
|
||||
const target = new PropertyRead(pipe.span, new ImplicitReceiver(pipe.span), slotPseudoLocal);
|
||||
const target = new PropertyRead(
|
||||
pipe.span, pipe.sourceSpan, new ImplicitReceiver(pipe.span, pipe.sourceSpan),
|
||||
slotPseudoLocal);
|
||||
const {identifier, isVarLength} = pipeBindingCallInfo(pipe.args);
|
||||
this.definePipe(pipe.name, slotPseudoLocal, slot, o.importExpr(identifier));
|
||||
const args: AST[] = [pipe.exp, ...pipe.args];
|
||||
const convertedArgs: AST[] =
|
||||
isVarLength ? this.visitAll([new LiteralArray(pipe.span, args)]) : this.visitAll(args);
|
||||
const convertedArgs: AST[] = isVarLength ?
|
||||
this.visitAll([new LiteralArray(pipe.span, pipe.sourceSpan, args)]) :
|
||||
this.visitAll(args);
|
||||
|
||||
const pipeBindExpr = new FunctionCall(pipe.span, target, [
|
||||
new LiteralPrimitive(pipe.span, slot),
|
||||
new LiteralPrimitive(pipe.span, pureFunctionSlot),
|
||||
const pipeBindExpr = new FunctionCall(pipe.span, pipe.sourceSpan, target, [
|
||||
new LiteralPrimitive(pipe.span, pipe.sourceSpan, slot),
|
||||
new LiteralPrimitive(pipe.span, pipe.sourceSpan, pureFunctionSlot),
|
||||
...convertedArgs,
|
||||
]);
|
||||
this._pipeBindExprs.push(pipeBindExpr);
|
||||
|
@ -1397,19 +1400,20 @@ export class ValueConverter extends AstMemoryEfficientTransformer {
|
|||
}
|
||||
|
||||
visitLiteralArray(array: LiteralArray, context: any): AST {
|
||||
return new BuiltinFunctionCall(array.span, this.visitAll(array.expressions), values => {
|
||||
// If the literal has calculated (non-literal) elements transform it into
|
||||
// calls to literal factories that compose the literal and will cache intermediate
|
||||
// values. Otherwise, just return an literal array that contains the values.
|
||||
const literal = o.literalArr(values);
|
||||
return values.every(a => a.isConstant()) ?
|
||||
this.constantPool.getConstLiteral(literal, true) :
|
||||
getLiteralFactory(this.constantPool, literal, this.allocatePureFunctionSlots);
|
||||
});
|
||||
return new BuiltinFunctionCall(
|
||||
array.span, array.sourceSpan, this.visitAll(array.expressions), values => {
|
||||
// If the literal has calculated (non-literal) elements transform it into
|
||||
// calls to literal factories that compose the literal and will cache intermediate
|
||||
// values. Otherwise, just return an literal array that contains the values.
|
||||
const literal = o.literalArr(values);
|
||||
return values.every(a => a.isConstant()) ?
|
||||
this.constantPool.getConstLiteral(literal, true) :
|
||||
getLiteralFactory(this.constantPool, literal, this.allocatePureFunctionSlots);
|
||||
});
|
||||
}
|
||||
|
||||
visitLiteralMap(map: LiteralMap, context: any): AST {
|
||||
return new BuiltinFunctionCall(map.span, this.visitAll(map.values), values => {
|
||||
return new BuiltinFunctionCall(map.span, map.sourceSpan, this.visitAll(map.values), values => {
|
||||
// If the literal has calculated (non-literal) elements transform it into
|
||||
// calls to literal factories that compose the literal and will cache intermediate
|
||||
// values. Otherwise, just return an literal array that contains the values.
|
||||
|
|
|
@ -6,69 +6,314 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ASTWithSource, AbsoluteSourceSpan, NullAstVisitor} from '@angular/compiler';
|
||||
import * as t from '../../src/render3/r3_ast';
|
||||
import {AbsoluteSourceSpan} from '@angular/compiler';
|
||||
import {humanizeExpressionSource} from './util/expression';
|
||||
import {parseR3 as parse} from './view/util';
|
||||
|
||||
class ExpressionLocationHumanizer extends NullAstVisitor implements t.Visitor {
|
||||
result: any[] = [];
|
||||
|
||||
visitASTWithSource(ast: ASTWithSource) { this.result.push([ast.source, ast.sourceSpan]); }
|
||||
|
||||
visitTemplate(ast: t.Template) { t.visitAll(this, ast.children); }
|
||||
visitElement(ast: t.Element) {
|
||||
t.visitAll(this, ast.children);
|
||||
t.visitAll(this, ast.inputs);
|
||||
t.visitAll(this, ast.outputs);
|
||||
}
|
||||
visitReference(ast: t.Reference) {}
|
||||
visitVariable(ast: t.Variable) {}
|
||||
visitEvent(ast: t.BoundEvent) { ast.handler.visit(this); }
|
||||
visitTextAttribute(ast: t.TextAttribute) {}
|
||||
visitBoundAttribute(ast: t.BoundAttribute) { ast.value.visit(this); }
|
||||
visitBoundEvent(ast: t.BoundEvent) { ast.handler.visit(this); }
|
||||
visitBoundText(ast: t.BoundText) { ast.value.visit(this); }
|
||||
visitContent(ast: t.Content) {}
|
||||
visitText(ast: t.Text) {}
|
||||
visitIcu(ast: t.Icu) {}
|
||||
}
|
||||
|
||||
function humanizeExpressionLocation(templateAsts: t.Node[]): any[] {
|
||||
const humanizer = new ExpressionLocationHumanizer();
|
||||
t.visitAll(humanizer, templateAsts);
|
||||
return humanizer.result;
|
||||
}
|
||||
|
||||
describe('expression AST absolute source spans', () => {
|
||||
// TODO(ayazhafiz): duplicate this test without `preserveWhitespaces` once whitespace rewriting is
|
||||
// moved to post-R3AST generation.
|
||||
it('should provide absolute offsets with arbitrary whitespace', () => {
|
||||
expect(humanizeExpressionLocation(
|
||||
expect(humanizeExpressionSource(
|
||||
parse('<div>\n \n{{foo}}</div>', {preserveWhitespaces: true}).nodes))
|
||||
.toContain(['\n \n{{foo}}', new AbsoluteSourceSpan(5, 16)]);
|
||||
.toContain(['\n \n{{ foo }}', new AbsoluteSourceSpan(5, 16)]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of an expression in a bound text', () => {
|
||||
expect(humanizeExpressionLocation(parse('<div>{{foo}}</div>').nodes)).toContain([
|
||||
'{{foo}}', new AbsoluteSourceSpan(5, 12)
|
||||
expect(humanizeExpressionSource(parse('<div>{{foo}}</div>').nodes)).toContain([
|
||||
'{{ foo }}', new AbsoluteSourceSpan(5, 12)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of an expression in a bound event', () => {
|
||||
expect(humanizeExpressionLocation(parse('<div (click)="foo();bar();"></div>').nodes))
|
||||
.toContain(['foo();bar();', new AbsoluteSourceSpan(14, 26)]);
|
||||
expect(humanizeExpressionSource(parse('<div (click)="foo();bar();"></div>').nodes)).toContain([
|
||||
'foo(); bar();', new AbsoluteSourceSpan(14, 26)
|
||||
]);
|
||||
|
||||
expect(humanizeExpressionLocation(parse('<div on-click="foo();bar();"></div>').nodes))
|
||||
.toContain(['foo();bar();', new AbsoluteSourceSpan(15, 27)]);
|
||||
expect(humanizeExpressionSource(parse('<div on-click="foo();bar();"></div>').nodes)).toContain([
|
||||
'foo(); bar();', new AbsoluteSourceSpan(15, 27)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of an expression in a bound attribute', () => {
|
||||
expect(
|
||||
humanizeExpressionLocation(parse('<input [disabled]="condition ? true : false" />').nodes))
|
||||
expect(humanizeExpressionSource(parse('<input [disabled]="condition ? true : false" />').nodes))
|
||||
.toContain(['condition ? true : false', new AbsoluteSourceSpan(19, 43)]);
|
||||
|
||||
expect(humanizeExpressionLocation(
|
||||
parse('<input bind-disabled="condition ? true : false" />').nodes))
|
||||
expect(
|
||||
humanizeExpressionSource(parse('<input bind-disabled="condition ? true : false" />').nodes))
|
||||
.toContain(['condition ? true : false', new AbsoluteSourceSpan(22, 46)]);
|
||||
});
|
||||
|
||||
describe('binary expression', () => {
|
||||
it('should provide absolute offsets of a binary expression', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{1 + 2}}<div>').nodes)).toContain([
|
||||
'1 + 2', new AbsoluteSourceSpan(7, 12)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a binary expression', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{1 + 2}}<div>').nodes))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
// TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions
|
||||
// with trailing whitespace in a binary expression. Look into fixing this.
|
||||
['1', new AbsoluteSourceSpan(7, 9)],
|
||||
['2', new AbsoluteSourceSpan(11, 12)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('conditional', () => {
|
||||
it('should provide absolute offsets of a conditional', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{bool ? 1 : 0}}<div>').nodes)).toContain([
|
||||
'bool ? 1 : 0', new AbsoluteSourceSpan(7, 19)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a conditional', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{bool ? 1 : 0}}<div>').nodes))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
// TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions
|
||||
// with trailing whitespace in a conditional expression. Look into fixing this.
|
||||
['bool', new AbsoluteSourceSpan(7, 12)],
|
||||
['1', new AbsoluteSourceSpan(14, 16)],
|
||||
['0', new AbsoluteSourceSpan(18, 19)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('chain', () => {
|
||||
it('should provide absolute offsets of a chain', () => {
|
||||
expect(humanizeExpressionSource(parse('<div (click)="a(); b();"><div>').nodes)).toContain([
|
||||
'a(); b();', new AbsoluteSourceSpan(14, 23)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a chain', () => {
|
||||
expect(humanizeExpressionSource(parse('<div (click)="a(); b();"><div>').nodes))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
['a()', new AbsoluteSourceSpan(14, 17)],
|
||||
['b()', new AbsoluteSourceSpan(19, 22)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('function call', () => {
|
||||
it('should provide absolute offsets of a function call', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{fn()()}}<div>').nodes)).toContain([
|
||||
'fn()()', new AbsoluteSourceSpan(7, 13)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a function call', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{fn()(param)}}<div>').nodes)).toContain([
|
||||
'param', new AbsoluteSourceSpan(12, 17)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of an implicit receiver', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{a.b}}<div>').nodes)).toContain([
|
||||
'', new AbsoluteSourceSpan(7, 7)
|
||||
]);
|
||||
});
|
||||
|
||||
describe('interpolation', () => {
|
||||
it('should provide absolute offsets of an interpolation', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{1 + foo.length}}<div>').nodes)).toContain([
|
||||
'{{ 1 + foo.length }}', new AbsoluteSourceSpan(5, 23)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in an interpolation', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{1 + 2}}<div>').nodes))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
// TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions
|
||||
// with trailing whitespace in a conditional expression. Look into fixing this.
|
||||
['1', new AbsoluteSourceSpan(7, 9)],
|
||||
['2', new AbsoluteSourceSpan(11, 12)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyed read', () => {
|
||||
it('should provide absolute offsets of a keyed read', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{obj[key]}}<div>').nodes)).toContain([
|
||||
'obj[key]', new AbsoluteSourceSpan(7, 15)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a keyed read', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{obj[key]}}<div>').nodes)).toContain([
|
||||
'key', new AbsoluteSourceSpan(11, 14)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyed write', () => {
|
||||
it('should provide absolute offsets of a keyed write', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{obj[key] = 0}}<div>').nodes)).toContain([
|
||||
'obj[key] = 0', new AbsoluteSourceSpan(7, 19)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a keyed write', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{obj[key] = 0}}<div>').nodes))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
['key', new AbsoluteSourceSpan(11, 14)],
|
||||
['0', new AbsoluteSourceSpan(18, 19)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of a literal primitive', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{100}}<div>').nodes)).toContain([
|
||||
'100', new AbsoluteSourceSpan(7, 10)
|
||||
]);
|
||||
});
|
||||
|
||||
describe('literal array', () => {
|
||||
it('should provide absolute offsets of a literal array', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{[0, 1, 2]}}<div>').nodes)).toContain([
|
||||
'[0, 1, 2]', new AbsoluteSourceSpan(7, 16)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a literal array', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{[0, 1, 2]}}<div>').nodes))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
['0', new AbsoluteSourceSpan(8, 9)],
|
||||
['1', new AbsoluteSourceSpan(11, 12)],
|
||||
['2', new AbsoluteSourceSpan(14, 15)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('literal map', () => {
|
||||
it('should provide absolute offsets of a literal map', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{ {a: 0} }}<div>').nodes)).toContain([
|
||||
// TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions
|
||||
// with trailing whitespace in a literal map. Look into fixing this.
|
||||
'{a: 0}', new AbsoluteSourceSpan(8, 15)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a literal map', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{ {a: 0} }}<div>').nodes))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
['0', new AbsoluteSourceSpan(12, 13)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('method call', () => {
|
||||
it('should provide absolute offsets of a method call', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{method()}}</div>').nodes)).toContain([
|
||||
'method()', new AbsoluteSourceSpan(7, 15)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a method call', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{method(param)}}<div>').nodes)).toContain([
|
||||
'param', new AbsoluteSourceSpan(14, 19)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-null assert', () => {
|
||||
it('should provide absolute offsets of a non-null assert', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop!}}</div>').nodes)).toContain([
|
||||
'prop!', new AbsoluteSourceSpan(7, 12)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a non-null assert', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop!}}<div>').nodes)).toContain([
|
||||
'prop', new AbsoluteSourceSpan(7, 11)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pipe', () => {
|
||||
it('should provide absolute offsets of a pipe', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop | pipe}}<div>').nodes)).toContain([
|
||||
'(prop | pipe)', new AbsoluteSourceSpan(7, 18)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets expressions in a pipe', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop | pipe}}<div>').nodes)).toContain([
|
||||
// TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions
|
||||
// with trailing whitespace in a pipe. Look into fixing this.
|
||||
'prop', new AbsoluteSourceSpan(7, 12)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of a property read', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop}}</div>').nodes)).toContain([
|
||||
'prop', new AbsoluteSourceSpan(7, 11)
|
||||
]);
|
||||
});
|
||||
|
||||
describe('property write', () => {
|
||||
it('should provide absolute offsets of a property write', () => {
|
||||
expect(humanizeExpressionSource(parse('<div (click)="prop = 0"></div>').nodes)).toContain([
|
||||
'prop = 0', new AbsoluteSourceSpan(14, 22)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a property write', () => {
|
||||
expect(humanizeExpressionSource(parse('<div (click)="prop = 0"></div>').nodes)).toContain([
|
||||
'0', new AbsoluteSourceSpan(21, 22)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('"not" prefix', () => {
|
||||
it('should provide absolute offsets of a "not" prefix', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{!prop}}</div>').nodes)).toContain([
|
||||
'!prop', new AbsoluteSourceSpan(7, 12)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a "not" prefix', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{!prop}}<div>').nodes)).toContain([
|
||||
'prop', new AbsoluteSourceSpan(8, 12)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('safe method call', () => {
|
||||
it('should provide absolute offsets of a safe method call', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop?.safe()}}<div>').nodes)).toContain([
|
||||
'prop?.safe()', new AbsoluteSourceSpan(7, 19)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in safe method call', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop?.safe()}}<div>').nodes)).toContain([
|
||||
'prop', new AbsoluteSourceSpan(7, 11)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('safe property read', () => {
|
||||
it('should provide absolute offsets of a safe property read', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop?.safe}}<div>').nodes)).toContain([
|
||||
'prop?.safe', new AbsoluteSourceSpan(7, 17)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in safe property read', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop?.safe}}<div>').nodes)).toContain([
|
||||
'prop', new AbsoluteSourceSpan(7, 11)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of a quote', () => {
|
||||
expect(humanizeExpressionSource(parse('<div [prop]="a:b"></div>').nodes)).toContain([
|
||||
'a:b', new AbsoluteSourceSpan(13, 16)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AbsoluteSourceSpan} from '@angular/compiler';
|
||||
import * as e from '../../../src/expression_parser/ast';
|
||||
import * as t from '../../../src/render3/r3_ast';
|
||||
import {unparse} from '../../expression_parser/utils/unparser';
|
||||
|
||||
type HumanizedExpressionSource = [string, AbsoluteSourceSpan];
|
||||
class ExpressionSourceHumanizer extends e.RecursiveAstVisitor implements t.Visitor {
|
||||
result: HumanizedExpressionSource[] = [];
|
||||
|
||||
private recordAst(ast: e.AST) { this.result.push([unparse(ast), ast.sourceSpan]); }
|
||||
|
||||
visitASTWithSource(ast: e.ASTWithSource) {
|
||||
this.recordAst(ast);
|
||||
this.visitAll([ast.ast], null);
|
||||
}
|
||||
visitBinary(ast: e.Binary) {
|
||||
this.recordAst(ast);
|
||||
super.visitBinary(ast, null);
|
||||
}
|
||||
visitChain(ast: e.Chain) {
|
||||
this.recordAst(ast);
|
||||
super.visitChain(ast, null);
|
||||
}
|
||||
visitConditional(ast: e.Conditional) {
|
||||
this.recordAst(ast);
|
||||
super.visitConditional(ast, null);
|
||||
}
|
||||
visitFunctionCall(ast: e.FunctionCall) {
|
||||
this.recordAst(ast);
|
||||
super.visitFunctionCall(ast, null);
|
||||
}
|
||||
visitImplicitReceiver(ast: e.ImplicitReceiver) {
|
||||
this.recordAst(ast);
|
||||
super.visitImplicitReceiver(ast, null);
|
||||
}
|
||||
visitInterpolation(ast: e.Interpolation) {
|
||||
this.recordAst(ast);
|
||||
super.visitInterpolation(ast, null);
|
||||
}
|
||||
visitKeyedRead(ast: e.KeyedRead) {
|
||||
this.recordAst(ast);
|
||||
super.visitKeyedRead(ast, null);
|
||||
}
|
||||
visitKeyedWrite(ast: e.KeyedWrite) {
|
||||
this.recordAst(ast);
|
||||
super.visitKeyedWrite(ast, null);
|
||||
}
|
||||
visitLiteralPrimitive(ast: e.LiteralPrimitive) {
|
||||
this.recordAst(ast);
|
||||
super.visitLiteralPrimitive(ast, null);
|
||||
}
|
||||
visitLiteralArray(ast: e.LiteralArray) {
|
||||
this.recordAst(ast);
|
||||
super.visitLiteralArray(ast, null);
|
||||
}
|
||||
visitLiteralMap(ast: e.LiteralMap) {
|
||||
this.recordAst(ast);
|
||||
super.visitLiteralMap(ast, null);
|
||||
}
|
||||
visitMethodCall(ast: e.MethodCall) {
|
||||
this.recordAst(ast);
|
||||
super.visitMethodCall(ast, null);
|
||||
}
|
||||
visitNonNullAssert(ast: e.NonNullAssert) {
|
||||
this.recordAst(ast);
|
||||
super.visitNonNullAssert(ast, null);
|
||||
}
|
||||
visitPipe(ast: e.BindingPipe) {
|
||||
this.recordAst(ast);
|
||||
super.visitPipe(ast, null);
|
||||
}
|
||||
visitPrefixNot(ast: e.PrefixNot) {
|
||||
this.recordAst(ast);
|
||||
super.visitPrefixNot(ast, null);
|
||||
}
|
||||
visitPropertyRead(ast: e.PropertyRead) {
|
||||
this.recordAst(ast);
|
||||
super.visitPropertyRead(ast, null);
|
||||
}
|
||||
visitPropertyWrite(ast: e.PropertyWrite) {
|
||||
this.recordAst(ast);
|
||||
super.visitPropertyWrite(ast, null);
|
||||
}
|
||||
visitSafeMethodCall(ast: e.SafeMethodCall) {
|
||||
this.recordAst(ast);
|
||||
super.visitSafeMethodCall(ast, null);
|
||||
}
|
||||
visitSafePropertyRead(ast: e.SafePropertyRead) {
|
||||
this.recordAst(ast);
|
||||
super.visitSafePropertyRead(ast, null);
|
||||
}
|
||||
visitQuote(ast: e.Quote) {
|
||||
this.recordAst(ast);
|
||||
super.visitQuote(ast, null);
|
||||
}
|
||||
|
||||
visitTemplate(ast: t.Template) { t.visitAll(this, ast.children); }
|
||||
visitElement(ast: t.Element) {
|
||||
t.visitAll(this, ast.children);
|
||||
t.visitAll(this, ast.inputs);
|
||||
t.visitAll(this, ast.outputs);
|
||||
}
|
||||
visitReference(ast: t.Reference) {}
|
||||
visitVariable(ast: t.Variable) {}
|
||||
visitEvent(ast: t.BoundEvent) { ast.handler.visit(this); }
|
||||
visitTextAttribute(ast: t.TextAttribute) {}
|
||||
visitBoundAttribute(ast: t.BoundAttribute) { ast.value.visit(this); }
|
||||
visitBoundEvent(ast: t.BoundEvent) { ast.handler.visit(this); }
|
||||
visitBoundText(ast: t.BoundText) { ast.value.visit(this); }
|
||||
visitContent(ast: t.Content) {}
|
||||
visitText(ast: t.Text) {}
|
||||
visitIcu(ast: t.Icu) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Humanizes expression AST source spans in a template by returning an array of tuples
|
||||
* [unparsed AST, AST source span]
|
||||
* for each expression in the template.
|
||||
* @param templateAsts template AST to humanize
|
||||
*/
|
||||
export function humanizeExpressionSource(templateAsts: t.Node[]): HumanizedExpressionSource[] {
|
||||
const humanizer = new ExpressionSourceHumanizer();
|
||||
t.visitAll(humanizer, templateAsts);
|
||||
return humanizer.result;
|
||||
}
|
|
@ -415,9 +415,12 @@ class ExpressionVisitor extends NullTemplateVisitor {
|
|||
valueRelativePosition > binding.span.start + (binding.key.length - key.length)) ||
|
||||
!binding.key) {
|
||||
const span = new ParseSpan(0, this.attr.value.length);
|
||||
const offset = ast.sourceSpan.start.offset;
|
||||
this.attributeValueCompletions(
|
||||
binding.expression ? binding.expression.ast :
|
||||
new PropertyRead(span, new ImplicitReceiver(span), ''),
|
||||
new PropertyRead(
|
||||
span, span.toAbsolute(offset),
|
||||
new ImplicitReceiver(span, span.toAbsolute(offset)), ''),
|
||||
valueRelativePosition);
|
||||
} else {
|
||||
keyCompletions();
|
||||
|
|
Loading…
Reference in New Issue