From 6b4909c588213e689530bb14dc884000d06921ff Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 26 Jan 2021 12:45:18 -0800 Subject: [PATCH] fix(compiler): Don't set expression text to synthetic `$implicit` when empty (#40583) When parsing interpolations, if we encounter an empty interpolation (`{{}}`), the current code uses a "pretend" value of `$implicit` for the name as if the interplotion were really `{{$implicit}}`. This is problematic because the spans are then incorrect downstream since they are based off of the `$implicit` text. This commit changes the interpretation of empty interpolations so that the text is simply an empty string. Fixes https://github.com/angular/vscode-ng-language-service/issues/1077 Fixes https://github.com/angular/vscode-ng-language-service/issues/1078 PR Close #40583 --- .../compiler/src/expression_parser/parser.ts | 15 ++++++++++----- .../test/expression_parser/parser_spec.ts | 8 +++++++- packages/language-service/ivy/completions.ts | 16 +++++++++------- 3 files changed, 26 insertions(+), 13 deletions(-) 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,