Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ivy): generator of setClassMetadata statements for Angular types (…
…#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
Showing
3 changed files
with
228 additions
and
0 deletions.
There are no files selected for viewing
133 changes: 133 additions & 0 deletions
133
packages/compiler-cli/src/ngtsc/annotations/src/metadata.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, ' '); | ||
} |