refactor(language-service): add generic decorator property verifications (#32252)

This PR makes finding class declarations properties in decorators are
applied to more generic to all properties that may be in a decorator,
and adds helper methods enabling getting the property assignment of a
property value and verifying that a property assignment is actually in a
decorator applied to a class.

This is done so that it will be easier to provide Angular definitions
for decorator properties moving forward. Most immediately, this will
provide decorator class verification for #32238.

PR Close #32252
This commit is contained in:
ayazhafiz 2019-08-21 20:46:30 -05:00 committed by atscott
parent 3dd614db61
commit c624b14e8e
3 changed files with 43 additions and 22 deletions

View File

@ -127,30 +127,34 @@ export class ExternalTemplate extends BaseTemplate {
} }
/** /**
* Given a template node, return the ClassDeclaration node that corresponds to * Returns a property assignment from the assignment value, or `undefined` if there is no
* the component class for the template. * assignment.
*/
export function getPropertyAssignmentFromValue(value: ts.Node): ts.PropertyAssignment|undefined {
if (!value.parent || !ts.isPropertyAssignment(value.parent)) {
return;
}
return value.parent;
}
/**
* 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, * For example,
* *
* @Component({ * @Component({
* template: '<div></div>' <-- template node * template: '<div></div>'
* ^^^^^^^^^^^^^^^^^^^^^^^---- property assignment
* }) * })
* class AppComponent {} * class AppComponent {}
* ^---- class declaration node * ^---- class declaration node
* *
* @param node template node * @param propAsgn property assignment
*/ */
export function getClassDeclFromTemplateNode(node: ts.Node): ts.ClassDeclaration|undefined { export function getClassDeclFromDecoratorProp(propAsgnNode: ts.PropertyAssignment):
if (!ts.isStringLiteralLike(node)) { ts.ClassDeclaration|undefined {
return;
}
if (!node.parent || !ts.isPropertyAssignment(node.parent)) {
return;
}
const propAsgnNode = node.parent;
if (propAsgnNode.name.getText() !== 'template') {
return;
}
if (!propAsgnNode.parent || !ts.isObjectLiteralExpression(propAsgnNode.parent)) { if (!propAsgnNode.parent || !ts.isObjectLiteralExpression(propAsgnNode.parent)) {
return; return;
} }
@ -169,3 +173,14 @@ export function getClassDeclFromTemplateNode(node: ts.Node): ts.ClassDeclaration
const classDeclNode = decorator.parent; const classDeclNode = decorator.parent;
return classDeclNode; return classDeclNode;
} }
/**
* Determines if a property assignment is on a class decorator.
* See `getClassDeclFromDecoratorProperty`, which gets the class the decorator is applied to, for
* more details.
*
* @param prop property assignment
*/
export function isClassDecoratorProperty(propAsgn: ts.PropertyAssignment): boolean {
return !!getClassDeclFromDecoratorProp(propAsgn);
}

View File

@ -13,7 +13,7 @@ import * as ts from 'typescript';
import {AstResult, isAstResult} from './common'; import {AstResult, isAstResult} from './common';
import {createLanguageService} from './language_service'; import {createLanguageService} from './language_service';
import {ReflectorHost} from './reflector_host'; import {ReflectorHost} from './reflector_host';
import {ExternalTemplate, InlineTemplate, getClassDeclFromTemplateNode} from './template'; import {ExternalTemplate, InlineTemplate, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './template';
import {Declaration, DeclarationError, Diagnostic, DiagnosticKind, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types'; import {Declaration, DeclarationError, Diagnostic, DiagnosticKind, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types';
import {findTightestNode, getDirectiveClassLike} from './utils'; import {findTightestNode, getDirectiveClassLike} from './utils';
@ -310,7 +310,11 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
if (!ts.isStringLiteralLike(node)) { if (!ts.isStringLiteralLike(node)) {
return; return;
} }
const classDecl = getClassDeclFromTemplateNode(node); const tmplAsgn = getPropertyAssignmentFromValue(node);
if (!tmplAsgn || tmplAsgn.name.getText() !== 'template') {
return;
}
const classDecl = getClassDeclFromDecoratorProp(tmplAsgn);
if (!classDecl || !classDecl.name) { // Does not handle anonymous class if (!classDecl || !classDecl.name) { // Does not handle anonymous class
return; return;
} }

View File

@ -7,7 +7,7 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {getClassDeclFromTemplateNode} from '../src/template'; import {getClassDeclFromDecoratorProp} from '../src/template';
import {toh} from './test_data'; import {toh} from './test_data';
import {MockTypescriptHost} from './test_utils'; import {MockTypescriptHost} from './test_utils';
@ -22,7 +22,10 @@ describe('getClassDeclFromTemplateNode', () => {
class MyComponent {}`, class MyComponent {}`,
ts.ScriptTarget.ES2015, true /* setParentNodes */); ts.ScriptTarget.ES2015, true /* setParentNodes */);
function visit(node: ts.Node): ts.ClassDeclaration|undefined { function visit(node: ts.Node): ts.ClassDeclaration|undefined {
return getClassDeclFromTemplateNode(node) || node.forEachChild(visit); if (ts.isPropertyAssignment(node)) {
return getClassDeclFromDecoratorProp(node);
}
return node.forEachChild(visit);
} }
const classDecl = sourceFile.forEachChild(visit); const classDecl = sourceFile.forEachChild(visit);
expect(classDecl).toBeTruthy(); expect(classDecl).toBeTruthy();
@ -37,9 +40,8 @@ describe('getClassDeclFromTemplateNode', () => {
const sourceFile = tsLS.getProgram() !.getSourceFile('/app/app.component.ts'); const sourceFile = tsLS.getProgram() !.getSourceFile('/app/app.component.ts');
expect(sourceFile).toBeTruthy(); expect(sourceFile).toBeTruthy();
const classDecl = sourceFile !.forEachChild(function visit(node): ts.Node | undefined { const classDecl = sourceFile !.forEachChild(function visit(node): ts.Node | undefined {
const candidate = getClassDeclFromTemplateNode(node); if (ts.isPropertyAssignment(node)) {
if (candidate) { return getClassDeclFromDecoratorProp(node);
return candidate;
} }
return node.forEachChild(visit); return node.forEachChild(visit);
}); });