feat(language-service): introduce hybrid visitor to locate AST node (#38540)

This commit introduces two visitors, one for Template AST and the other
for Expression AST to allow us to easily find the node that most closely
corresponds to a given cursor position.

This is crucial because many language service APIs take in a `position`
parameter, and the information returned depends on how well we can find
a good candidate node.

In View Engine implementation of language service, the search for the node
and the processing of information to return the result are strongly coupled.
This makes the code hard to understand and hard to debug because the stack
trace is often littered with layers of visitor calls.

With this new feature, we could test the "searching" part separately and
colocate all the logic (aka hacks) that's required to retrieve an accurate
span for a given node.

Right now, only the most "narrow" node is returned by the main exported
function `findNodeAtPosition`. If needed, we could expose the entire AST
path, or expose other methods to provide more context for a node.

Note that due to limitations in the template AST interface, there are
a few known cases where microsyntax spans are not recorded properly.
This will be dealt with in a follow-up PR.

PR Close #38540
This commit is contained in:
Keen Yee Liau 2020-08-12 16:13:02 -07:00 committed by Misko Hevery
parent 874792dc43
commit b48cc6ead5
4 changed files with 750 additions and 0 deletions

View File

@ -6,6 +6,7 @@ ts_library(
name = "ivy",
srcs = glob(["*.ts"]),
deps = [
"//packages/compiler",
"//packages/compiler-cli",
"//packages/compiler-cli/src/ngtsc/core",
"//packages/compiler-cli/src/ngtsc/core:api",

View File

@ -0,0 +1,180 @@
/**
* @license
* Copyright Google LLC 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 {ParseSourceSpan} from '@angular/compiler';
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
/**
* Return the template AST node or expression AST node that most accurately
* represents the node at the specified cursor `position`.
* @param ast AST tree
* @param position cursor position
*/
export function findNodeAtPosition(ast: t.Node[], position: number): t.Node|e.AST|undefined {
const visitor = new R3Visitor(position);
visitor.visitAll(ast);
return visitor.path[visitor.path.length - 1];
}
class R3Visitor implements t.Visitor {
// We need to keep a path instead of the last node because we might need more
// context for the last node, for example what is the parent node?
readonly path: Array<t.Node|e.AST> = [];
// Position must be absolute in the source file.
constructor(private readonly position: number) {}
visit(node: t.Node) {
const {start, end} = getSpanIncludingEndTag(node);
// Note both start and end are inclusive because we want to match conditions
// like ¦start and end¦ where ¦ is the cursor.
if (start <= this.position && this.position <= end) {
const length = end - start;
const last: t.Node|e.AST|undefined = this.path[this.path.length - 1];
if (last) {
const {start, end} = isTemplateNode(last) ? getSpanIncludingEndTag(last) : last.sourceSpan;
const lastLength = end - start;
if (length > lastLength) {
// The current node has a span that is larger than the last node found
// so we do not descend into it. This typically means we have found
// a candidate in one of the root nodes so we do not need to visit
// other root nodes.
return;
}
}
if (node instanceof t.BoundEvent &&
this.path.find((n => n instanceof t.BoundAttribute && node.name === n.name + 'Change'))) {
// For two-way binding aka banana-in-a-box, there are two matches:
// BoundAttribute and BoundEvent. Both have the same spans. We choose to
// return BoundAttribute because it matches the identifier name verbatim.
// TODO: For operations like go to definition, ideally we want to return
// both.
return;
}
this.path.push(node);
node.visit(this);
}
}
visitElement(element: t.Element) {
this.visitAll(element.attributes);
this.visitAll(element.inputs);
this.visitAll(element.outputs);
this.visitAll(element.references);
this.visitAll(element.children);
}
visitTemplate(template: t.Template) {
this.visitAll(template.attributes);
this.visitAll(template.inputs);
this.visitAll(template.outputs);
this.visitAll(template.templateAttrs);
this.visitAll(template.references);
this.visitAll(template.variables);
this.visitAll(template.children);
}
visitContent(content: t.Content) {
t.visitAll(this, content.attributes);
}
visitVariable(variable: t.Variable) {
// Variable has no template nodes or expression nodes.
}
visitReference(reference: t.Reference) {
// Reference has no template nodes or expression nodes.
}
visitTextAttribute(attribute: t.TextAttribute) {
// Text attribute has no template nodes or expression nodes.
}
visitBoundAttribute(attribute: t.BoundAttribute) {
const visitor = new ExpressionVisitor(this.position);
visitor.visit(attribute.value, this.path);
}
visitBoundEvent(attribute: t.BoundEvent) {
const visitor = new ExpressionVisitor(this.position);
visitor.visit(attribute.handler, this.path);
}
visitText(text: t.Text) {
// Text has no template nodes or expression nodes.
}
visitBoundText(text: t.BoundText) {
const visitor = new ExpressionVisitor(this.position);
visitor.visit(text.value, this.path);
}
visitIcu(icu: t.Icu) {
for (const boundText of Object.values(icu.vars)) {
this.visit(boundText);
}
for (const boundTextOrText of Object.values(icu.placeholders)) {
this.visit(boundTextOrText);
}
}
visitAll(nodes: t.Node[]) {
for (const node of nodes) {
this.visit(node);
}
}
}
class ExpressionVisitor extends e.RecursiveAstVisitor {
// Position must be absolute in the source file.
constructor(private readonly position: number) {
super();
}
visit(node: e.AST, path: Array<t.Node|e.AST>) {
if (node instanceof e.ASTWithSource) {
// In order to reduce noise, do not include `ASTWithSource` in the path.
// For the purpose of source spans, there is no difference between
// `ASTWithSource` and and underlying node that it wraps.
node = node.ast;
}
const {start, end} = node.sourceSpan;
// The third condition is to account for the implicit receiver, which should
// not be visited.
if (start <= this.position && this.position <= end && !(node instanceof e.ImplicitReceiver)) {
path.push(node);
node.visit(this, path);
}
}
}
export function isTemplateNode(node: t.Node|e.AST): node is t.Node {
// Template node implements the Node interface so we cannot use instanceof.
return node.sourceSpan instanceof ParseSourceSpan;
}
export function isExpressionNode(node: t.Node|e.AST): node is e.AST {
return node instanceof e.AST;
}
function getSpanIncludingEndTag(ast: t.Node) {
const result = {
start: ast.sourceSpan.start.offset,
end: ast.sourceSpan.end.offset,
};
// For Element and Template node, sourceSpan.end is the end of the opening
// tag. For the purpose of language service, we need to actually recognize
// the end of the closing tag. Otherwise, for situation like
// <my-component></my-comp¦onent> where the cursor is in the closing tag
// we will not be able to return any information.
if ((ast instanceof t.Element || ast instanceof t.Template) && ast.endSourceSpan) {
result.end = ast.endSourceSpan.end.offset;
}
return result;
}

View File

@ -5,6 +5,7 @@ ts_library(
testonly = True,
srcs = glob(["*.ts"]),
deps = [
"//packages/compiler",
"//packages/language-service/ivy",
"@npm//typescript",
],

View File

@ -0,0 +1,568 @@
/**
* @license
* Copyright Google LLC 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 {ParseError, parseTemplate} from '@angular/compiler';
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
import {findNodeAtPosition, isExpressionNode, isTemplateNode} from '../hybrid_visitor';
interface ParseResult {
nodes: t.Node[];
errors?: ParseError[];
position: number;
}
function parse(template: string): ParseResult {
const position = template.indexOf('¦');
if (position < 0) {
throw new Error(`Template "${template}" does not contain the cursor`);
}
template = template.replace('¦', '');
const templateUrl = '/foo';
return {
...parseTemplate(template, templateUrl),
position,
};
}
describe('findNodeAtPosition for template AST', () => {
it('should locate element in opening tag', () => {
const {errors, nodes, position} = parse(`<di¦v></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
});
it('should locate element in closing tag', () => {
const {errors, nodes, position} = parse(`<div></di¦v>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
});
it('should locate element when cursor is at the beginning', () => {
const {errors, nodes, position} = parse(`<¦div></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
});
it('should locate element when cursor is at the end', () => {
const {errors, nodes, position} = parse(`<div¦></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
});
it('should locate attribute key', () => {
const {errors, nodes, position} = parse(`<div cla¦ss="foo"></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
});
it('should locate attribute value', () => {
const {errors, nodes, position} = parse(`<div class="fo¦o"></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
// TODO: Note that we do not have the ability to detect the RHS (yet)
expect(node).toBeInstanceOf(t.TextAttribute);
});
it('should locate bound attribute key', () => {
const {errors, nodes, position} = parse(`<test-cmp [fo¦o]="bar"></test-cmp>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
});
it('should locate bound attribute value', () => {
const {errors, nodes, position} = parse(`<test-cmp [foo]="b¦ar"></test-cmp>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
});
it('should locate bound event key', () => {
const {errors, nodes, position} = parse(`<test-cmp (fo¦o)="bar()"></test-cmp>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundEvent);
});
it('should locate bound event value', () => {
const {errors, nodes, position} = parse(`<test-cmp (foo)="b¦ar()"></test-cmp>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.MethodCall);
});
it('should locate element children', () => {
const {errors, nodes, position} = parse(`<div><sp¦an></span></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
expect((node as t.Element).name).toBe('span');
});
it('should locate element reference', () => {
const {errors, nodes, position} = parse(`<div #my¦div></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
});
it('should locate template text attribute', () => {
const {errors, nodes, position} = parse(`<ng-template ng¦If></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
});
it('should locate template bound attribute key', () => {
const {errors, nodes, position} = parse(`<ng-template [ng¦If]="foo"></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
});
it('should locate template bound attribute value', () => {
const {errors, nodes, position} = parse(`<ng-template [ngIf]="f¦oo"></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
});
it('should locate template bound attribute key in two-way binding', () => {
const {errors, nodes, position} = parse(`<ng-template [(f¦oo)]="bar"></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
expect((node as t.BoundAttribute).name).toBe('foo');
});
it('should locate template bound attribute value in two-way binding', () => {
const {errors, nodes, position} = parse(`<ng-template [(foo)]="b¦ar"></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('bar');
});
it('should locate template bound event key', () => {
const {errors, nodes, position} = parse(`<ng-template (cl¦ick)="foo()"></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundEvent);
});
it('should locate template bound event value', () => {
const {errors, nodes, position} = parse(`<ng-template (click)="f¦oo()"></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(node).toBeInstanceOf(e.MethodCall);
});
it('should locate template attribute key', () => {
const {errors, nodes, position} = parse(`<ng-template i¦d="foo"></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
});
it('should locate template attribute value', () => {
const {errors, nodes, position} = parse(`<ng-template id="f¦oo"></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
// TODO: Note that we do not have the ability to detect the RHS (yet)
expect(node).toBeInstanceOf(t.TextAttribute);
});
it('should locate template reference key via the # notation', () => {
const {errors, nodes, position} = parse(`<ng-template #f¦oo></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
expect((node as t.Reference).name).toBe('foo');
});
it('should locate template reference key via the ref- notation', () => {
const {errors, nodes, position} = parse(`<ng-template ref-fo¦o></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
expect((node as t.Reference).name).toBe('foo');
});
it('should locate template reference value via the # notation', () => {
const {errors, nodes, position} = parse(`<ng-template #foo="export¦As"></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
expect((node as t.Reference).value).toBe('exportAs');
// TODO: Note that we do not have the ability to distinguish LHS and RHS
});
it('should locate template reference value via the ref- notation', () => {
const {errors, nodes, position} = parse(`<ng-template ref-foo="export¦As"></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
expect((node as t.Reference).value).toBe('exportAs');
// TODO: Note that we do not have the ability to distinguish LHS and RHS
});
it('should locate template variable key', () => {
const {errors, nodes, position} = parse(`<ng-template let-f¦oo="bar"></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
});
it('should locate template variable value', () => {
const {errors, nodes, position} = parse(`<ng-template let-foo="b¦ar"></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
});
it('should locate template children', () => {
const {errors, nodes, position} = parse(`<ng-template><d¦iv></div></ng-template>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
});
it('should locate ng-content', () => {
const {errors, nodes, position} = parse(`<ng-co¦ntent></ng-content>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Content);
});
it('should locate ng-content attribute key', () => {
const {errors, nodes, position} = parse('<ng-content cla¦ss="red"></ng-content>');
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
});
it('should locate ng-content attribute value', () => {
const {errors, nodes, position} = parse('<ng-content class="r¦ed"></ng-content>');
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
// TODO: Note that we do not have the ability to detect the RHS (yet)
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
});
it('should not locate implicit receiver', () => {
const {errors, nodes, position} = parse(`<div [foo]="¦bar"></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
});
it('should locate bound attribute key in two-way binding', () => {
const {errors, nodes, position} = parse(`<cmp [(f¦oo)]="bar"></cmp>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
expect((node as t.BoundAttribute).name).toBe('foo');
});
it('should locate bound attribute value in two-way binding', () => {
const {errors, nodes, position} = parse(`<cmp [(foo)]="b¦ar"></cmp>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('bar');
});
});
describe('findNodeAtPosition for expression AST', () => {
it('should not locate implicit receiver', () => {
const {errors, nodes, position} = parse(`{{ ¦title }}`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('title');
});
it('should locate property read', () => {
const {errors, nodes, position} = parse(`{{ ti¦tle }}`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('title');
});
it('should locate safe property read', () => {
const {errors, nodes, position} = parse(`{{ foo?¦.bar }}`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.SafePropertyRead);
expect((node as e.SafePropertyRead).name).toBe('bar');
});
it('should locate keyed read', () => {
const {errors, nodes, position} = parse(`{{ foo['bar']¦ }}`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.KeyedRead);
});
it('should locate property write', () => {
const {errors, nodes, position} = parse(`<div (foo)="b¦ar=$event"></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyWrite);
});
it('should locate keyed write', () => {
const {errors, nodes, position} = parse(`<div (foo)="bar['baz']¦=$event"></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.KeyedWrite);
});
it('should locate binary', () => {
const {errors, nodes, position} = parse(`{{ 1 +¦ 2 }}`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.Binary);
});
it('should locate binding pipe with an identifier', () => {
const {errors, nodes, position} = parse(`{{ title | p¦ }}`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.BindingPipe);
});
it('should locate binding pipe without identifier',
() => {
// TODO: We are not able to locate pipe if identifier is missing because the
// parser throws an error. This case is important for autocomplete.
// const {errors, nodes, position} = parse(`{{ title | ¦ }}`);
// expect(errors).toBeUndefined();
// const node = findNodeAtPosition(nodes, position);
// expect(isExpressionNode(node!)).toBe(true);
// expect(node).toBeInstanceOf(e.BindingPipe);
});
it('should locate method call', () => {
const {errors, nodes, position} = parse(`{{ title.toString(¦) }}`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.MethodCall);
});
it('should locate safe method call', () => {
const {errors, nodes, position} = parse(`{{ title?.toString(¦) }}`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.SafeMethodCall);
});
it('should locate literal primitive in interpolation', () => {
const {errors, nodes, position} = parse(`{{ title.indexOf('t¦') }}`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.LiteralPrimitive);
expect((node as e.LiteralPrimitive).value).toBe('t');
});
it('should locate literal primitive in binding', () => {
const {errors, nodes, position} = parse(`<div [id]="'t¦'"></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.LiteralPrimitive);
expect((node as e.LiteralPrimitive).value).toBe('t');
});
it('should locate empty expression', () => {
const {errors, nodes, position} = parse(`<div [id]="¦"></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.EmptyExpr);
});
it('should locate literal array', () => {
const {errors, nodes, position} = parse(`{{ [1, 2,¦ 3] }}`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.LiteralArray);
});
it('should locate literal map', () => {
const {errors, nodes, position} = parse(`{{ { hello:¦ "world" } }}`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.LiteralMap);
});
it('should locate conditional', () => {
const {errors, nodes, position} = parse(`{{ cond ?¦ true : false }}`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.Conditional);
});
});
describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate template key', () => {
const {errors, nodes, position} = parse(`<div *ng¦If="foo"></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
});
it('should locate template value', () => {
const {errors, nodes, position} = parse(`<div *ngIf="f¦oo"></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
});
it('should locate text attribute', () => {
const {errors, nodes, position} = parse(`<div *ng¦For="let item of items"></div>`);
// ngFor is a text attribute because the desugared form is
// <ng-template ngFor let-item [ngForOf]="items">
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
// TODO: this is currently wrong because it should point to ngFor text
// attribute instead of ngForOf bound attribute
});
it('should locate not let keyword', () => {
const {errors, nodes, position} = parse(`<div *ngFor="l¦et item of items"></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
// TODO: this is currently wrong because node is currently pointing to
// "item". In this case, it should return undefined.
});
it('should locate let variable', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let i¦tem of items"></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
expect((node as t.Variable).name).toBe('item');
});
it('should locate bound attribute key', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let item o¦f items"></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
expect((node as t.BoundAttribute).name).toBe('ngForOf');
});
it('should locate bound attribute value', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let item of it¦ems"></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('items');
});
it('should locate template children', () => {
const {errors, nodes, position} = parse(`<di¦v *ngIf></div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
expect((node as t.Element).name).toBe('div');
});
it('should locate property read of variable declared within template', () => {
const {errors, nodes, position} = parse(`
<div *ngFor="let item of items; let i=index">
{{ i¦ }}
</div>`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
});
it('should locate LHS of variable declaration', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let item of items; let i¦=index">`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
// TODO: Currently there is no way to distinguish LHS from RHS
expect((node as t.Variable).name).toBe('i');
});
it('should locate RHS of variable declaration', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let item of items; let i=in¦dex">`);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
// TODO: Currently there is no way to distinguish LHS from RHS
expect((node as t.Variable).value).toBe('index');
});
});