diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts index df6b60ec10..de8f485b8c 100644 --- a/packages/compiler/src/expression_parser/parser.ts +++ b/packages/compiler/src/expression_parser/parser.ts @@ -243,14 +243,12 @@ export class Parser { const fullEnd = exprEnd + interpEnd.length; const text = input.substring(exprStart, exprEnd); - if (text.trim().length > 0) { - expressions.push({text, start: fullStart, end: fullEnd}); - } else { + if (text.trim().length === 0) { this._reportError( 'Blank expressions are not allowed in interpolated strings', input, `at column ${i} in`, location); - expressions.push({text: '$implicit', start: fullStart, end: fullEnd}); } + expressions.push({text, start: fullStart, end: fullEnd}); offsets.push(exprStart); i = fullEnd; @@ -571,7 +569,14 @@ export class _ParseAST { this.error(`Unexpected token '${this.next}'`); } } - if (exprs.length == 0) return new EmptyExpr(this.span(start), this.sourceSpan(start)); + if (exprs.length == 0) { + // We have no expressions so create an empty expression that spans the entire input length + const artificialStart = this.offset; + const artificialEnd = this.offset + this.inputLength; + return new EmptyExpr( + this.span(artificialStart, artificialEnd), + this.sourceSpan(artificialStart, artificialEnd)); + } if (exprs.length == 1) return exprs[0]; return new Chain(this.span(start), this.sourceSpan(start), exprs); } diff --git a/packages/compiler/test/expression_parser/parser_spec.ts b/packages/compiler/test/expression_parser/parser_spec.ts index 4130961cbc..5e5004f5b1 100644 --- a/packages/compiler/test/expression_parser/parser_spec.ts +++ b/packages/compiler/test/expression_parser/parser_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteSourceSpan, ASTWithSource, BindingPipe, Interpolation, ParserError, TemplateBinding, VariableBinding} from '@angular/compiler/src/expression_parser/ast'; +import {AbsoluteSourceSpan, ASTWithSource, BindingPipe, EmptyExpr, Interpolation, ParserError, TemplateBinding, VariableBinding} from '@angular/compiler/src/expression_parser/ast'; import {Lexer} from '@angular/compiler/src/expression_parser/lexer'; import {IvyParser, Parser, SplitInterpolation} from '@angular/compiler/src/expression_parser/parser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -910,6 +910,12 @@ describe('parser', () => { 'Parser Error: Blank expressions are not allowed in interpolated strings'); }); + it('should produce an empty expression ast for empty interpolations', () => { + const parsed = parseInterpolation('{{}}')!.ast as Interpolation; + expect(parsed.expressions.length).toBe(1); + expect(parsed.expressions[0]).toBeAnInstanceOf(EmptyExpr); + }); + it('should parse conditional expression', () => { checkInterpolation('{{ a < b ? a : b }}'); }); diff --git a/packages/language-service/ivy/completions.ts b/packages/language-service/ivy/completions.ts index 0dace86de3..1005e7acbe 100644 --- a/packages/language-service/ivy/completions.ts +++ b/packages/language-service/ivy/completions.ts @@ -220,12 +220,7 @@ export class CompletionBuilder { const {componentContext, templateContext} = completions; - let replacementSpan: ts.TextSpan|undefined = undefined; - // Non-empty nodes get replaced with the completion. - if (!(this.node instanceof EmptyExpr || this.node instanceof LiteralPrimitive || - this.node instanceof BoundEvent)) { - replacementSpan = makeReplacementSpanFromAst(this.node); - } + const replacementSpan = makeReplacementSpanFromAst(this.node); // Merge TS completion results with results from the template scope. let entries: ts.CompletionEntry[] = []; @@ -631,7 +626,14 @@ function makeReplacementSpanFromParseSourceSpan(span: ParseSourceSpan): ts.TextS } function makeReplacementSpanFromAst(node: PropertyRead|PropertyWrite|MethodCall|SafePropertyRead| - SafeMethodCall|BindingPipe): ts.TextSpan { + SafeMethodCall|BindingPipe|EmptyExpr|LiteralPrimitive| + BoundEvent): ts.TextSpan|undefined { + if ((node instanceof EmptyExpr || node instanceof LiteralPrimitive || + node instanceof BoundEvent)) { + // empty nodes do not replace any existing text + return undefined; + } + return { start: node.nameSpan.start, length: node.nameSpan.end - node.nameSpan.start,