fix(compiler): record correct end of expression (#34690)

This commit fixes a bug with the expression parser wherein the end index
of an expression node was recorded as the start index of the next token,
not the end index of the current token.

Closes #33477
Closes https://github.com/angular/vscode-ng-language-service/issues/433

PR Close #34690
This commit is contained in:
ayazhafiz
2019-12-18 19:30:56 -06:00
committed by Misko Hevery
parent 47bfec4e46
commit df890d7629
9 changed files with 87 additions and 92 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AST, ASTWithSource, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, HtmlAstPath, NAMED_ENTITIES, Node as HtmlAst, NullTemplateVisitor, ReferenceAst, TagContentType, TemplateBinding, Text, VariableBinding, getHtmlTagDefinition} from '@angular/compiler';
import {AST, AbsoluteSourceSpan, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, EmptyExpr, ExpressionBinding, HtmlAstPath, NAMED_ENTITIES, Node as HtmlAst, NullTemplateVisitor, ParseSpan, ReferenceAst, TagContentType, TemplateBinding, Text, VariableBinding, getHtmlTagDefinition} from '@angular/compiler';
import {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
import {AstResult} from './common';
@ -575,21 +575,21 @@ class ExpressionVisitor extends NullTemplateVisitor {
}
}
}
}
else if (inSpan(valueRelativePosition, binding.value?.ast.span)) {
this.processExpressionCompletions(binding.value !.ast);
return;
}
// If the expression is incomplete, for example *ngFor="let x of |"
// binding.expression is null. We could still try to provide suggestions
// by looking for symbols that are in scope.
const KW_OF = ' of ';
const ofLocation = attr.value.indexOf(KW_OF);
if (ofLocation > 0 && valueRelativePosition >= ofLocation + KW_OF.length) {
const expressionAst = this.info.expressionParser.parseBinding(
attr.value, attr.sourceSpan.toString(), attr.sourceSpan.start.offset);
this.processExpressionCompletions(expressionAst);
} else if (binding instanceof ExpressionBinding) {
if (inSpan(this.position, binding.value?.ast.sourceSpan)) {
this.processExpressionCompletions(binding.value !.ast);
return;
} else if (!binding.value && this.position > binding.key.span.end) {
// No expression is defined for the value of the key expression binding, but the cursor is
// in a location where the expression would be defined. This can happen in a case like
// let i of |
// ^-- cursor
// In this case, backfill the value to be an empty expression and retrieve completions.
this.processExpressionCompletions(new EmptyExpr(
new ParseSpan(valueRelativePosition, valueRelativePosition),
new AbsoluteSourceSpan(this.position, this.position)));
return;
}
}
}
}

View File

@ -310,15 +310,30 @@ describe('completions', () => {
expect(completions).toBeUndefined();
});
it('should include field reference', () => {
mockHost.override(TEST_TEMPLATE, `<div *ngFor="let x of ~{cursor}"></div>`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title', 'heroes', 'league']);
// the symbol 'x' declared in *ngFor is also in scope. This asserts that
// we are actually taking the AST into account and not just referring to
// the symbol table of the Component.
expectContain(completions, CompletionKind.VARIABLE, ['x']);
describe('template binding: key expression', () => {
it('should complete the RHS of a template key expression without an expression value', () => {
mockHost.override(
TEST_TEMPLATE, `<div *ngFor="let x of ~{cursor}"></div>`); // value is undefined
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title', 'heroes', 'league']);
// the symbol 'x' declared in *ngFor is also in scope. This asserts that
// we are actually taking the AST into account and not just referring to
// the symbol table of the Component.
expectContain(completions, CompletionKind.VARIABLE, ['x']);
});
it('should complete the RHS of a template key expression with an expression value', () => {
mockHost.override(
TEST_TEMPLATE, `<div *ngFor="let x of t~{cursor}"></div>`); // value is defined
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title', 'heroes', 'league']);
// the symbol 'x' declared in *ngFor is also in scope. This asserts that
// we are actually taking the AST into account and not just referring to
// the symbol table of the Component.
expectContain(completions, CompletionKind.VARIABLE, ['x']);
});
});
it('should include expression completions', () => {

View File

@ -328,7 +328,7 @@ describe('diagnostics', () => {
it('report an unknown field in $implicit context', () => {
mockHost.override(TEST_TEMPLATE, `
<div *withContext="let myVar">
{{ ~{start-emb}myVar.missingField ~{end-emb}}}
{{ ~{start-emb}myVar.missingField~{end-emb} }}
</div>
`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
@ -346,7 +346,7 @@ describe('diagnostics', () => {
it('report an unknown field in non implicit context', () => {
mockHost.override(TEST_TEMPLATE, `
<div *withContext="let myVar = nonImplicitPerson">
{{ ~{start-emb}myVar.missingField ~{end-emb}}}
{{ ~{start-emb}myVar.missingField~{end-emb} }}
</div>
`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);