Alex Rickabaugh 643c96184c refactor(language-service): introduce DisplayParts abstraction for Ivy (#39505)
This commit refactors the QuickInfo abstraction shared between the VE and
Ivy services and used to implement hover tooltips (quick info), which was
extracted from the VE code in commit faa81dc. The new DisplayParts
abstraction is more general and can be used to extract information needed by
various LS functions (e.g. autocompletion).

This commit effectively reverts faa81dc, returning the original code to the
VE implementation as the Ivy code is now diverged.

PR Close #39505
2020-11-02 10:29:50 -08:00

149 lines
4.8 KiB
TypeScript

/**
* @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 * as ts from 'typescript/lib/tsserverlibrary';
interface DirectiveClassLike {
decoratorId: ts.Identifier; // decorator identifier, like @Component
classId: ts.Identifier;
}
/**
* Return metadata about `node` if it looks like an Angular directive class.
* In this case, potential matches are `@NgModule`, `@Component`, `@Directive`,
* `@Pipe`, etc.
* These class declarations all share some common attributes, namely their
* decorator takes exactly one parameter and the parameter must be an object
* literal.
*
* For example,
* v---------- `decoratorId`
* @NgModule({ <
* declarations: [], < classDecln-al
* }) <
* class AppModule {} <
* ^----- `classId`
*
* @param node Potential node that represents an Angular directive.
*/
export function getDirectiveClassLike(node: ts.Node): DirectiveClassLike|undefined {
if (!ts.isClassDeclaration(node) || !node.name || !node.decorators) {
return;
}
for (const d of node.decorators) {
const expr = d.expression;
if (!ts.isCallExpression(expr) || expr.arguments.length !== 1 ||
!ts.isIdentifier(expr.expression)) {
continue;
}
const arg = expr.arguments[0];
if (ts.isObjectLiteralExpression(arg)) {
return {
decoratorId: expr.expression,
classId: node.name,
};
}
}
}
/**
* Finds the value of a property assignment that is nested in a TypeScript node and is of a certain
* type T.
*
* @param startNode node to start searching for nested property assignment from
* @param propName property assignment name
* @param predicate function to verify that a node is of type T.
* @return node property assignment value of type T, or undefined if none is found
*/
export function findPropertyValueOfType<T extends ts.Node>(
startNode: ts.Node, propName: string, predicate: (node: ts.Node) => node is T): T|undefined {
if (ts.isPropertyAssignment(startNode) && startNode.name.getText() === propName) {
const {initializer} = startNode;
if (predicate(initializer)) return initializer;
}
return startNode.forEachChild(c => findPropertyValueOfType(c, propName, predicate));
}
/**
* Return the node that most tightly encompass the specified `position`.
* @param node
* @param position
*/
export function findTightestNode(node: ts.Node, position: number): ts.Node|undefined {
if (node.getStart() <= position && position < node.getEnd()) {
return node.forEachChild(c => findTightestNode(c, position)) || node;
}
}
/**
* Returns a property assignment from the assignment value if the property name
* matches the specified `key`, or `undefined` if there is no match.
*/
export function getPropertyAssignmentFromValue(value: ts.Node, key: string): ts.PropertyAssignment|
undefined {
const propAssignment = value.parent;
if (!propAssignment || !ts.isPropertyAssignment(propAssignment) ||
propAssignment.name.getText() !== key) {
return;
}
return propAssignment;
}
/**
* Given the node which is the string of the inline template for a component, returns the
* `ts.ClassDeclaration` for the component.
*/
export function getClassDeclOfInlineTemplateNode(templateStringNode: ts.Node): ts.ClassDeclaration|
undefined {
if (!ts.isStringLiteralLike(templateStringNode)) {
return;
}
const tmplAsgn = getPropertyAssignmentFromValue(templateStringNode, 'template');
if (!tmplAsgn) {
return;
}
return getClassDeclFromDecoratorProp(tmplAsgn);
}
/**
* Given a decorator property assignment, return the ClassDeclaration node that corresponds to the
* directive class the property applies to.
* If the property assignment is not on a class decorator, no declaration is returned.
*
* For example,
*
* @Component({
* template: '<div></div>'
* ^^^^^^^^^^^^^^^^^^^^^^^---- property assignment
* })
* class AppComponent {}
* ^---- class declaration node
*
* @param propAsgnNode property assignment
*/
export function getClassDeclFromDecoratorProp(propAsgnNode: ts.PropertyAssignment):
ts.ClassDeclaration|undefined {
if (!propAsgnNode.parent || !ts.isObjectLiteralExpression(propAsgnNode.parent)) {
return;
}
const objLitExprNode = propAsgnNode.parent;
if (!objLitExprNode.parent || !ts.isCallExpression(objLitExprNode.parent)) {
return;
}
const callExprNode = objLitExprNode.parent;
if (!callExprNode.parent || !ts.isDecorator(callExprNode.parent)) {
return;
}
const decorator = callExprNode.parent;
if (!decorator.parent || !ts.isClassDeclaration(decorator.parent)) {
return;
}
const classDeclNode = decorator.parent;
return classDeclNode;
}