Ayaz Hafiz eb34aa551a feat(compiler): add name spans for property reads and method calls (#36826)
ASTs for property read and method calls contain information about
the entire span of the expression, including its receiver. Use cases
like a language service and compile error messages may be more
interested in the span of the direct identifier for which the
expression is constructed (i.e. an accessed property). To support this,
this commit adds a `nameSpan` property on

- `PropertyRead`s
- `SafePropertyRead`s
- `PropertyWrite`s
- `MethodCall`s
- `SafeMethodCall`s

The `nameSpan` property already existed for `BindingPipe`s.

This commit also updates usages of these expressions' `sourceSpan`s in
Ngtsc and the langauge service to use `nameSpan`s where appropriate.

PR Close #36826
2020-05-08 14:42:42 -07:00

194 lines
6.4 KiB
TypeScript

/**
* @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 {AST, AstPath as AstPathBase, ASTWithName, ASTWithSource, RecursiveAstVisitor} from '@angular/compiler';
import {AstType} from './expression_type';
import {BuiltinType, Span, Symbol, SymbolTable, TemplateSource} from './types';
import {inSpan} from './utils';
type AstPath = AstPathBase<AST>;
function findAstAt(ast: AST, position: number, excludeEmpty: boolean = false): AstPath {
const path: AST[] = [];
const visitor = new class extends RecursiveAstVisitor {
visit(ast: AST) {
if ((!excludeEmpty || ast.sourceSpan.start < ast.sourceSpan.end) &&
inSpan(position, ast.sourceSpan)) {
path.push(ast);
ast.visit(this);
}
}
};
// We never care about the ASTWithSource node and its visit() method calls its ast's visit so
// the visit() method above would never see it.
if (ast instanceof ASTWithSource) {
ast = ast.ast;
}
visitor.visit(ast);
return new AstPathBase<AST>(path, position);
}
export function getExpressionCompletions(
scope: SymbolTable, ast: AST, position: number, templateInfo: TemplateSource): Symbol[]|
undefined {
const path = findAstAt(ast, position);
if (path.empty) return undefined;
const tail = path.tail!;
let result: SymbolTable|undefined = scope;
function getType(ast: AST): Symbol {
return new AstType(scope, templateInfo.query, {}, templateInfo.source).getType(ast);
}
// If the completion request is in a not in a pipe or property access then the global scope
// (that is the scope of the implicit receiver) is the right scope as the user is typing the
// beginning of an expression.
tail.visit({
visitBinary(_ast) {},
visitChain(_ast) {},
visitConditional(_ast) {},
visitFunctionCall(_ast) {},
visitImplicitReceiver(_ast) {},
visitInterpolation(_ast) {
result = undefined;
},
visitKeyedRead(_ast) {},
visitKeyedWrite(_ast) {},
visitLiteralArray(_ast) {},
visitLiteralMap(_ast) {},
visitLiteralPrimitive(_ast) {},
visitMethodCall(_ast) {},
visitPipe(ast) {
if (position >= ast.exp.span.end &&
(!ast.args || !ast.args.length || position < (<AST>ast.args[0]).span.start)) {
// We are in a position a pipe name is expected.
result = templateInfo.query.getPipes();
}
},
visitPrefixNot(_ast) {},
visitNonNullAssert(_ast) {},
visitPropertyRead(ast) {
const receiverType = getType(ast.receiver);
result = receiverType ? receiverType.members() : scope;
},
visitPropertyWrite(ast) {
const receiverType = getType(ast.receiver);
result = receiverType ? receiverType.members() : scope;
},
visitQuote(_ast) {
// For a quote, return the members of any (if there are any).
result = templateInfo.query.getBuiltinType(BuiltinType.Any).members();
},
visitSafeMethodCall(ast) {
const receiverType = getType(ast.receiver);
result = receiverType ? receiverType.members() : scope;
},
visitSafePropertyRead(ast) {
const receiverType = getType(ast.receiver);
result = receiverType ? receiverType.members() : scope;
},
});
return result && result.values();
}
/**
* Retrieves the expression symbol at a particular position in a template.
*
* @param scope symbols in scope of the template
* @param ast template AST
* @param position absolute location in template to retrieve symbol at
* @param query type symbol query for the template scope
*/
export function getExpressionSymbol(
scope: SymbolTable, ast: AST, position: number,
templateInfo: TemplateSource): {symbol: Symbol, span: Span}|undefined {
const path = findAstAt(ast, position, /* excludeEmpty */ true);
if (path.empty) return undefined;
const tail = path.tail!;
function getType(ast: AST): Symbol {
return new AstType(scope, templateInfo.query, {}, templateInfo.source).getType(ast);
}
function spanFromName(ast: ASTWithName): Span {
// `nameSpan` is an absolute span, but the span expected by the result of this method is
// relative to the start of the expression.
// TODO(ayazhafiz): migrate to only using absolute spans
const offset = ast.sourceSpan.start - ast.span.start;
return {
start: ast.nameSpan.start - offset,
end: ast.nameSpan.end - offset,
};
}
let symbol: Symbol|undefined = undefined;
let span: Span|undefined = undefined;
// If the completion request is in a not in a pipe or property access then the global scope
// (that is the scope of the implicit receiver) is the right scope as the user is typing the
// beginning of an expression.
tail.visit({
visitBinary(_ast) {},
visitChain(_ast) {},
visitConditional(_ast) {},
visitFunctionCall(_ast) {},
visitImplicitReceiver(_ast) {},
visitInterpolation(_ast) {},
visitKeyedRead(_ast) {},
visitKeyedWrite(_ast) {},
visitLiteralArray(_ast) {},
visitLiteralMap(_ast) {},
visitLiteralPrimitive(_ast) {},
visitMethodCall(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = spanFromName(ast);
},
visitPipe(ast) {
if (inSpan(position, ast.nameSpan, /* exclusive */ true)) {
// We are in a position a pipe name is expected.
const pipes = templateInfo.query.getPipes();
symbol = pipes.get(ast.name);
span = spanFromName(ast);
}
},
visitPrefixNot(_ast) {},
visitNonNullAssert(_ast) {},
visitPropertyRead(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = spanFromName(ast);
},
visitPropertyWrite(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = spanFromName(ast);
},
visitQuote(_ast) {},
visitSafeMethodCall(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = spanFromName(ast);
},
visitSafePropertyRead(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = spanFromName(ast);
},
});
if (symbol && span) {
return {symbol, span};
}
}