Skip to content

Commit

Permalink
feat(ivy): generator of setClassMetadata statements for Angular types (
Browse files Browse the repository at this point in the history
…#26860)

This commit introduces generateSetClassMetadataCall(), an API in ngtsc
for generating calls to setClassMetadata() for a given declaration. The
reflection API is used to enumerate Angular decorators on the declaration,
which are converted to a format that ReflectionCapabilities can understand.
The reflection metadata is then patched onto the declared type via a call
to setClassMetadata().

This is simply a utility, a future commit invokes this utility for
each DecoratorHandler.

Testing strategy: tests are included which exercise generateSetClassMetadata
in isolation.

PR Close #26860
  • Loading branch information
alxhub authored and matsko committed Oct 31, 2018
1 parent ca1e538 commit 4925761
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 0 deletions.
133 changes: 133 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/src/metadata.ts
@@ -0,0 +1,133 @@
/**
* @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 {ExternalExpr, Identifiers, InvokeFunctionExpr, Statement, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';

import {CtorParameter, Decorator, ReflectionHost} from '../../host';

/**
* Given a class declaration, generate a call to `setClassMetadata` with the Angular metadata
* present on the class or its member fields.
*
* If no such metadata is present, this function returns `null`. Otherwise, the call is returned
* as a `Statement` for inclusion along with the class.
*/
export function generateSetClassMetadataCall(
clazz: ts.Declaration, reflection: ReflectionHost, isCore: boolean): Statement|null {
// Classes come in two flavors, class declarations (ES2015) and variable declarations (ES5).
// Both must have a declared name to have metadata set on them.
if ((!ts.isClassDeclaration(clazz) && !ts.isVariableDeclaration(clazz)) ||
clazz.name === undefined || !ts.isIdentifier(clazz.name)) {
return null;
}
const id = ts.updateIdentifier(clazz.name);

// Reflect over the class decorators. If none are present, or those that are aren't from
// Angular, then return null. Otherwise, turn them into metadata.
const classDecorators = reflection.getDecoratorsOfDeclaration(clazz);
if (classDecorators === null) {
return null;
}
const ngClassDecorators =
classDecorators.filter(dec => isAngularDecorator(dec, isCore)).map(decoratorToMetadata);
if (ngClassDecorators.length === 0) {
return null;
}
const metaDecorators = ts.createArrayLiteral(ngClassDecorators);

// Convert the constructor parameters to metadata, passing null if none are present.
let metaCtorParameters: ts.Expression = ts.createNull();
const classCtorParameters = reflection.getConstructorParameters(clazz);
if (classCtorParameters !== null) {
metaCtorParameters = ts.createArrayLiteral(
classCtorParameters.map(param => ctorParameterToMetadata(param, isCore)));
}

// Do the same for property decorators.
let metaPropDecorators: ts.Expression = ts.createNull();
const decoratedMembers =
reflection.getMembersOfClass(clazz)
.filter(member => !member.isStatic && member.decorators !== null)
.map(member => classMemberToMetadata(member.name, member.decorators !, isCore));
if (decoratedMembers.length > 0) {
metaPropDecorators = ts.createObjectLiteral(decoratedMembers);
}

// Generate a pure call to setClassMetadata with the class identifier and its metadata.
const setClassMetadata = new ExternalExpr(Identifiers.setClassMetadata);
const fnCall = new InvokeFunctionExpr(
/* fn */ setClassMetadata,
/* args */
[
new WrappedNodeExpr(id),
new WrappedNodeExpr(metaDecorators),
new WrappedNodeExpr(metaCtorParameters),
new WrappedNodeExpr(metaPropDecorators),
],
/* type */ undefined,
/* sourceSpan */ undefined,
/* pure */ true);
return fnCall.toStmt();
}

/**
* Convert a reflected constructor parameter to metadata.
*/
function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): ts.Expression {
// Parameters sometimes have a type that can be referenced. If so, then use it, otherwise
// its type is undefined.
const type = param.type !== null ? param.type : ts.createIdentifier('undefined');
const properties: ts.ObjectLiteralElementLike[] = [
ts.createPropertyAssignment('type', type),
];

// If the parameter has decorators, include the ones from Angular.
if (param.decorators !== null) {
const ngDecorators =
param.decorators.filter(dec => isAngularDecorator(dec, isCore)).map(decoratorToMetadata);
properties.push(ts.createPropertyAssignment('decorators', ts.createArrayLiteral(ngDecorators)));
}
return ts.createObjectLiteral(properties, true);
}

/**
* Convert a reflected class member to metadata.
*/
function classMemberToMetadata(
name: string, decorators: Decorator[], isCore: boolean): ts.PropertyAssignment {
const ngDecorators =
decorators.filter(dec => isAngularDecorator(dec, isCore)).map(decoratorToMetadata);
const decoratorMeta = ts.createArrayLiteral(ngDecorators);
return ts.createPropertyAssignment(name, decoratorMeta);
}

/**
* Convert a reflected decorator to metadata.
*/
function decoratorToMetadata(decorator: Decorator): ts.ObjectLiteralExpression {
// Decorators have a type.
const properties: ts.ObjectLiteralElementLike[] = [
ts.createPropertyAssignment('type', ts.updateIdentifier(decorator.identifier)),
];
// Sometimes they have arguments.
if (decorator.args !== null && decorator.args.length > 0) {
const args = decorator.args.map(arg => ts.getMutableClone(arg));
properties.push(ts.createPropertyAssignment('args', ts.createArrayLiteral(args)));
}
return ts.createObjectLiteral(properties, true);
}

/**
* Whether a given decorator should be treated as an Angular decorator.
*
* Either it's used in @angular/core, or it's imported from there.
*/
function isAngularDecorator(decorator: Decorator, isCore: boolean): boolean {
return isCore || (decorator.import !== null && decorator.import.from === '@angular/core');
}
Expand Up @@ -15,6 +15,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/translator",
"@ngdeps//typescript",
],
)
Expand Down
94 changes: 94 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts
@@ -0,0 +1,94 @@
/**
* @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 {Statement} from '@angular/compiler';
import * as ts from 'typescript';

import {TypeScriptReflectionHost} from '../../metadata';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {ImportManager, translateStatement} from '../../translator';
import {generateSetClassMetadataCall} from '../src/metadata';

const CORE = {
name: 'node_modules/@angular/core/index.d.ts',
contents: `
export declare function Input(...args: any[]): any;
export declare function Inject(...args: any[]): any;
export declare function Component(...args: any[]): any;
export declare class Injector {}
`
};

describe('ngtsc setClassMetadata converter', () => {
it('should convert decorated class metadata', () => {
const res = compileAndPrint(`
import {Component} from '@angular/core';
@Component('metadata') class Target {}
`);
expect(res).toEqual(
`/*@__PURE__*/ i0.ɵsetClassMetadata(Target, [{ type: Component, args: ['metadata'] }], null, null);`);
});

it('should convert decorated class construtor parameter metadata', () => {
const res = compileAndPrint(`
import {Component, Inject, Injector} from '@angular/core';
const FOO = 'foo';
@Component('metadata') class Target {
constructor(@Inject(FOO) foo: any, bar: Injector) {}
}
`);
expect(res).toContain(
`[{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: Injector }], null);`);
});

it('should convert decorated field metadata', () => {
const res = compileAndPrint(`
import {Component, Input} from '@angular/core';
@Component('metadata') class Target {
@Input() foo: string;
@Input('value') bar: string;
notDecorated: string;
}
`);
expect(res).toContain(`{ foo: [{ type: Input }], bar: [{ type: Input, args: ['value'] }] })`);
});

it('should not convert non-angular decorators to metadata', () => {
const res = compileAndPrint(`
declare function NotAComponent(...args: any[]): any;
@NotAComponent('metadata') class Target {}
`);
expect(res).toBe('');
});
});

function compileAndPrint(contents: string): string {
const {program} = makeProgram([
CORE, {
name: 'index.ts',
contents,
}
]);
const host = new TypeScriptReflectionHost(program.getTypeChecker());
const target = getDeclaration(program, 'index.ts', 'Target', ts.isClassDeclaration);
const call = generateSetClassMetadataCall(target, host, false);
if (call === null) {
return '';
}
const sf = program.getSourceFile('index.ts') !;
const im = new ImportManager(false, 'i');
const tsStatement = translateStatement(call, im);
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf);
return res.replace(/\s+/g, ' ');
}

0 comments on commit 4925761

Please sign in to comment.