
When we were outputting class members for `setClassMetadata` calls, we were using the string representation of the member name. This can lead to us generating invalid code when the name contains dashes and is quoted (e.g. `@Output() 'has-dashes' = new EventEmitter()`), because the quotes will be stripped for the string representation. These changes fix the issue by using the original name AST node that was used for the declaration and which knows whether it's supposed to be quoted or not. Fixes #38311. PR Close #38387
142 lines
4.9 KiB
TypeScript
142 lines
4.9 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';
|
|
|
|
import {absoluteFrom, getSourceFileOrError} from '../../file_system';
|
|
import {runInEachFileSystem, TestFile} from '../../file_system/testing';
|
|
import {NOOP_DEFAULT_IMPORT_RECORDER, NoopImportRewriter} from '../../imports';
|
|
import {TypeScriptReflectionHost} from '../../reflection';
|
|
import {getDeclaration, makeProgram} from '../../testing';
|
|
import {ImportManager, translateStatement} from '../../translator';
|
|
import {generateSetClassMetadataCall} from '../src/metadata';
|
|
|
|
runInEachFileSystem(() => {
|
|
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__*/ (function () { i0.ɵsetClassMetadata(Target, [{ type: Component, args: ['metadata'] }], null, null); })();`);
|
|
});
|
|
|
|
it('should convert namespaced decorated class metadata', () => {
|
|
const res = compileAndPrint(`
|
|
import * as core from '@angular/core';
|
|
|
|
@core.Component('metadata') class Target {}
|
|
`);
|
|
expect(res).toEqual(
|
|
`/*@__PURE__*/ (function () { i0.ɵsetClassMetadata(Target, [{ type: core.Component, args: ['metadata'] }], null, null); })();`);
|
|
});
|
|
|
|
it('should convert decorated class constructor 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(
|
|
`function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: i0.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 convert decorated field getter/setter metadata', () => {
|
|
const res = compileAndPrint(`
|
|
import {Component, Input} from '@angular/core';
|
|
|
|
@Component('metadata') class Target {
|
|
@Input() get foo() { return this._foo; }
|
|
set foo(value: string) { this._foo = value; }
|
|
private _foo: string;
|
|
|
|
get bar() { return this._bar; }
|
|
@Input('value') set bar(value: string) { this._bar = value; }
|
|
private _bar: 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('');
|
|
});
|
|
|
|
it('should preserve quotes around class member names', () => {
|
|
const res = compileAndPrint(`
|
|
import {Component, Input} from '@angular/core';
|
|
|
|
@Component('metadata') class Target {
|
|
@Input() 'has-dashes-in-name' = 123;
|
|
@Input() noDashesInName = 456;
|
|
}
|
|
`);
|
|
expect(res).toContain(
|
|
`{ 'has-dashes-in-name': [{ type: Input }], noDashesInName: [{ type: Input }] })`);
|
|
});
|
|
});
|
|
|
|
function compileAndPrint(contents: string): string {
|
|
const _ = absoluteFrom;
|
|
const CORE: TestFile = {
|
|
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 {}
|
|
`
|
|
};
|
|
|
|
const {program} = makeProgram(
|
|
[
|
|
CORE, {
|
|
name: _('/index.ts'),
|
|
contents,
|
|
}
|
|
],
|
|
{target: ts.ScriptTarget.ES2015});
|
|
const host = new TypeScriptReflectionHost(program.getTypeChecker());
|
|
const target = getDeclaration(program, _('/index.ts'), 'Target', ts.isClassDeclaration);
|
|
const call = generateSetClassMetadataCall(target, host, NOOP_DEFAULT_IMPORT_RECORDER, false);
|
|
if (call === null) {
|
|
return '';
|
|
}
|
|
const sf = getSourceFileOrError(program, _('/index.ts'));
|
|
const im = new ImportManager(new NoopImportRewriter(), 'i');
|
|
const tsStatement =
|
|
translateStatement(call, im, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES2015);
|
|
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf);
|
|
return res.replace(/\s+/g, ' ');
|
|
}
|
|
});
|