diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts index ed40d0a47d..3f6c1a002e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts @@ -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]); } diff --git a/packages/compiler/src/compiler_util/expression_converter.ts b/packages/compiler/src/compiler_util/expression_converter.ts index 971aec3223..5eb7ab27a9 100644 --- a/packages/compiler/src/compiler_util/expression_converter.ts +++ b/packages/compiler/src/compiler_util/expression_converter.ts @@ -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); } } diff --git a/packages/compiler/src/expression_parser/ast.ts b/packages/compiler/src/expression_parser/ast.ts index 0ae7cc4e8f..5323373a19 100644 --- a/packages/compiler/src/expression_parser/ast.ts +++ b/packages/compiler/src/expression_parser/ast.ts @@ -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) {} 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; } diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts index 01b3092d49..4a6ca50aee 100644 --- a/packages/compiler/src/expression_parser/parser.ts +++ b/packages/compiler/src/expression_parser/parser.ts @@ -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(); + 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); diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 5786805778..64e7b2a01b 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -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. diff --git a/packages/compiler/test/render3/r3_ast_absolute_span_spec.ts b/packages/compiler/test/render3/r3_ast_absolute_span_spec.ts index 96fa208c3c..151e2906e0 100644 --- a/packages/compiler/test/render3/r3_ast_absolute_span_spec.ts +++ b/packages/compiler/test/render3/r3_ast_absolute_span_spec.ts @@ -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('
\n \n{{foo}}
', {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('
{{foo}}
').nodes)).toContain([ - '{{foo}}', new AbsoluteSourceSpan(5, 12) + expect(humanizeExpressionSource(parse('
{{foo}}
').nodes)).toContain([ + '{{ foo }}', new AbsoluteSourceSpan(5, 12) ]); }); it('should provide absolute offsets of an expression in a bound event', () => { - expect(humanizeExpressionLocation(parse('
').nodes)) - .toContain(['foo();bar();', new AbsoluteSourceSpan(14, 26)]); + expect(humanizeExpressionSource(parse('
').nodes)).toContain([ + 'foo(); bar();', new AbsoluteSourceSpan(14, 26) + ]); - expect(humanizeExpressionLocation(parse('
').nodes)) - .toContain(['foo();bar();', new AbsoluteSourceSpan(15, 27)]); + expect(humanizeExpressionSource(parse('
').nodes)).toContain([ + 'foo(); bar();', new AbsoluteSourceSpan(15, 27) + ]); }); it('should provide absolute offsets of an expression in a bound attribute', () => { - expect( - humanizeExpressionLocation(parse('').nodes)) + expect(humanizeExpressionSource(parse('').nodes)) .toContain(['condition ? true : false', new AbsoluteSourceSpan(19, 43)]); - expect(humanizeExpressionLocation( - parse('').nodes)) + expect( + humanizeExpressionSource(parse('').nodes)) .toContain(['condition ? true : false', new AbsoluteSourceSpan(22, 46)]); }); + + describe('binary expression', () => { + it('should provide absolute offsets of a binary expression', () => { + expect(humanizeExpressionSource(parse('
{{1 + 2}}
').nodes)).toContain([ + '1 + 2', new AbsoluteSourceSpan(7, 12) + ]); + }); + + it('should provide absolute offsets of expressions in a binary expression', () => { + expect(humanizeExpressionSource(parse('
{{1 + 2}}
').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('
{{bool ? 1 : 0}}
').nodes)).toContain([ + 'bool ? 1 : 0', new AbsoluteSourceSpan(7, 19) + ]); + }); + + it('should provide absolute offsets of expressions in a conditional', () => { + expect(humanizeExpressionSource(parse('
{{bool ? 1 : 0}}
').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('
').nodes)).toContain([ + 'a(); b();', new AbsoluteSourceSpan(14, 23) + ]); + }); + + it('should provide absolute offsets of expressions in a chain', () => { + expect(humanizeExpressionSource(parse('
').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('
{{fn()()}}
').nodes)).toContain([ + 'fn()()', new AbsoluteSourceSpan(7, 13) + ]); + }); + + it('should provide absolute offsets of expressions in a function call', () => { + expect(humanizeExpressionSource(parse('
{{fn()(param)}}
').nodes)).toContain([ + 'param', new AbsoluteSourceSpan(12, 17) + ]); + }); + }); + + it('should provide absolute offsets of an implicit receiver', () => { + expect(humanizeExpressionSource(parse('
{{a.b}}
').nodes)).toContain([ + '', new AbsoluteSourceSpan(7, 7) + ]); + }); + + describe('interpolation', () => { + it('should provide absolute offsets of an interpolation', () => { + expect(humanizeExpressionSource(parse('
{{1 + foo.length}}
').nodes)).toContain([ + '{{ 1 + foo.length }}', new AbsoluteSourceSpan(5, 23) + ]); + }); + + it('should provide absolute offsets of expressions in an interpolation', () => { + expect(humanizeExpressionSource(parse('
{{1 + 2}}
').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('
{{obj[key]}}
').nodes)).toContain([ + 'obj[key]', new AbsoluteSourceSpan(7, 15) + ]); + }); + + it('should provide absolute offsets of expressions in a keyed read', () => { + expect(humanizeExpressionSource(parse('
{{obj[key]}}
').nodes)).toContain([ + 'key', new AbsoluteSourceSpan(11, 14) + ]); + }); + }); + + describe('keyed write', () => { + it('should provide absolute offsets of a keyed write', () => { + expect(humanizeExpressionSource(parse('
{{obj[key] = 0}}
').nodes)).toContain([ + 'obj[key] = 0', new AbsoluteSourceSpan(7, 19) + ]); + }); + + it('should provide absolute offsets of expressions in a keyed write', () => { + expect(humanizeExpressionSource(parse('
{{obj[key] = 0}}
').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('
{{100}}
').nodes)).toContain([ + '100', new AbsoluteSourceSpan(7, 10) + ]); + }); + + describe('literal array', () => { + it('should provide absolute offsets of a literal array', () => { + expect(humanizeExpressionSource(parse('
{{[0, 1, 2]}}
').nodes)).toContain([ + '[0, 1, 2]', new AbsoluteSourceSpan(7, 16) + ]); + }); + + it('should provide absolute offsets of expressions in a literal array', () => { + expect(humanizeExpressionSource(parse('
{{[0, 1, 2]}}
').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('
{{ {a: 0} }}
').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('
{{ {a: 0} }}
').nodes)) + .toEqual(jasmine.arrayContaining([ + ['0', new AbsoluteSourceSpan(12, 13)], + ])); + }); + }); + + describe('method call', () => { + it('should provide absolute offsets of a method call', () => { + expect(humanizeExpressionSource(parse('
{{method()}}
').nodes)).toContain([ + 'method()', new AbsoluteSourceSpan(7, 15) + ]); + }); + + it('should provide absolute offsets of expressions in a method call', () => { + expect(humanizeExpressionSource(parse('
{{method(param)}}
').nodes)).toContain([ + 'param', new AbsoluteSourceSpan(14, 19) + ]); + }); + }); + + describe('non-null assert', () => { + it('should provide absolute offsets of a non-null assert', () => { + expect(humanizeExpressionSource(parse('
{{prop!}}
').nodes)).toContain([ + 'prop!', new AbsoluteSourceSpan(7, 12) + ]); + }); + + it('should provide absolute offsets of expressions in a non-null assert', () => { + expect(humanizeExpressionSource(parse('
{{prop!}}
').nodes)).toContain([ + 'prop', new AbsoluteSourceSpan(7, 11) + ]); + }); + }); + + describe('pipe', () => { + it('should provide absolute offsets of a pipe', () => { + expect(humanizeExpressionSource(parse('
{{prop | pipe}}
').nodes)).toContain([ + '(prop | pipe)', new AbsoluteSourceSpan(7, 18) + ]); + }); + + it('should provide absolute offsets expressions in a pipe', () => { + expect(humanizeExpressionSource(parse('
{{prop | pipe}}
').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('
{{prop}}
').nodes)).toContain([ + 'prop', new AbsoluteSourceSpan(7, 11) + ]); + }); + + describe('property write', () => { + it('should provide absolute offsets of a property write', () => { + expect(humanizeExpressionSource(parse('
').nodes)).toContain([ + 'prop = 0', new AbsoluteSourceSpan(14, 22) + ]); + }); + + it('should provide absolute offsets of expressions in a property write', () => { + expect(humanizeExpressionSource(parse('
').nodes)).toContain([ + '0', new AbsoluteSourceSpan(21, 22) + ]); + }); + }); + + describe('"not" prefix', () => { + it('should provide absolute offsets of a "not" prefix', () => { + expect(humanizeExpressionSource(parse('
{{!prop}}
').nodes)).toContain([ + '!prop', new AbsoluteSourceSpan(7, 12) + ]); + }); + + it('should provide absolute offsets of expressions in a "not" prefix', () => { + expect(humanizeExpressionSource(parse('
{{!prop}}
').nodes)).toContain([ + 'prop', new AbsoluteSourceSpan(8, 12) + ]); + }); + }); + + describe('safe method call', () => { + it('should provide absolute offsets of a safe method call', () => { + expect(humanizeExpressionSource(parse('
{{prop?.safe()}}
').nodes)).toContain([ + 'prop?.safe()', new AbsoluteSourceSpan(7, 19) + ]); + }); + + it('should provide absolute offsets of expressions in safe method call', () => { + expect(humanizeExpressionSource(parse('
{{prop?.safe()}}
').nodes)).toContain([ + 'prop', new AbsoluteSourceSpan(7, 11) + ]); + }); + }); + + describe('safe property read', () => { + it('should provide absolute offsets of a safe property read', () => { + expect(humanizeExpressionSource(parse('
{{prop?.safe}}
').nodes)).toContain([ + 'prop?.safe', new AbsoluteSourceSpan(7, 17) + ]); + }); + + it('should provide absolute offsets of expressions in safe property read', () => { + expect(humanizeExpressionSource(parse('
{{prop?.safe}}
').nodes)).toContain([ + 'prop', new AbsoluteSourceSpan(7, 11) + ]); + }); + }); + + it('should provide absolute offsets of a quote', () => { + expect(humanizeExpressionSource(parse('
').nodes)).toContain([ + 'a:b', new AbsoluteSourceSpan(13, 16) + ]); + }); }); diff --git a/packages/compiler/test/render3/util/expression.ts b/packages/compiler/test/render3/util/expression.ts new file mode 100644 index 0000000000..5ed2c4b7d7 --- /dev/null +++ b/packages/compiler/test/render3/util/expression.ts @@ -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; +} diff --git a/packages/language-service/src/completions.ts b/packages/language-service/src/completions.ts index 5ac9143ef4..e58a5d066f 100644 --- a/packages/language-service/src/completions.ts +++ b/packages/language-service/src/completions.ts @@ -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();