fix(ivy): never use imported type references as values (#29111)

ngtsc occasionally converts a type reference (such as the type of a
parameter in a constructor) to a value reference (argument to a
directiveInject call). TypeScript has a bad habit of sometimes removing
the import statement associated with this type reference, because it's a
type only import when it initially looks at the file.

A solution to this is to always add an import to refer to a type position
value that's imported, and not rely on the existing import.

PR Close #29111
This commit is contained in:
Alex Rickabaugh 2019-03-04 11:43:55 -08:00 committed by Andrew Kushnir
parent 20a9dbef8e
commit 881807dc36
15 changed files with 308 additions and 87 deletions

View File

@ -895,7 +895,13 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
paramInfo[index] :
{decorators: null, typeExpression: null};
const nameNode = node.name;
return {name: getNameText(nameNode), nameNode, typeExpression, typeNode: null, decorators};
return {
name: getNameText(nameNode),
nameNode,
typeValueReference:
typeExpression !== null ? {local: true as true, expression: typeExpression} : null,
typeNode: null, decorators
};
});
}

View File

@ -12,6 +12,8 @@ import {ClassMemberKind, Import} from '../../../ngtsc/reflection';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {convertToDirectTsLibImport, getDeclaration, makeTestProgram} from '../helpers/utils';
import {expectTypeValueReferencesForParameters} from './util';
const FILES = [
{
name: '/some_directive.js',
@ -262,8 +264,10 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
expect(parameters !.map(parameter => parameter.name)).toEqual([
'_viewContainer', '_template', 'injected'
]);
expect(parameters !.map(parameter => parameter.typeExpression !.getText())).toEqual([
'ViewContainerRef', 'TemplateRef', 'String'
expectTypeValueReferencesForParameters(parameters !, [
'ViewContainerRef',
'TemplateRef',
'String',
]);
});
@ -296,7 +300,10 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode) !;
const identifierOfViewContainerRef = ctrDecorators[0].typeExpression !as ts.Identifier;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{
local: true,
expression: ts.Identifier
}).expression;
const expectedDeclarationNode = getDeclaration(
program, '/some_directive.js', 'ViewContainerRef', ts.isClassDeclaration);

View File

@ -12,6 +12,8 @@ import {ClassMemberKind, Import} from '../../../ngtsc/reflection';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils';
import {expectTypeValueReferencesForParameters} from './util';
const SOME_DIRECTIVE_FILE = {
name: '/some_directive.js',
contents: `
@ -930,9 +932,7 @@ describe('Fesm2015ReflectionHost', () => {
expect(parameters.map(parameter => parameter.name)).toEqual([
'_viewContainer', '_template', 'injected'
]);
expect(parameters.map(parameter => parameter.typeExpression !.getText())).toEqual([
'ViewContainerRef', 'TemplateRef', 'undefined'
]);
expectTypeValueReferencesForParameters(parameters, ['ViewContainerRef', 'TemplateRef', null]);
});
it('should throw if the symbol is not a class', () => {
@ -1293,7 +1293,10 @@ describe('Fesm2015ReflectionHost', () => {
const classNode =
getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode) !;
const identifierOfViewContainerRef = ctrDecorators[0].typeExpression !as ts.Identifier;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{
local: true,
expression: ts.Identifier
}).expression;
const expectedDeclarationNode = getDeclaration(
program, SOME_DIRECTIVE_FILE.name, 'ViewContainerRef', ts.isVariableDeclaration);

View File

@ -12,6 +12,8 @@ import {ClassMemberKind, Import} from '../../../ngtsc/reflection';
import {Esm5ReflectionHost} from '../../src/host/esm5_host';
import {convertToDirectTsLibImport, getDeclaration, makeTestProgram} from '../helpers/utils';
import {expectTypeValueReferencesForParameters} from './util';
const FILES = [
{
name: '/some_directive.js',
@ -277,8 +279,10 @@ describe('Esm5ReflectionHost [import helper style]', () => {
expect(parameters !.map(parameter => parameter.name)).toEqual([
'_viewContainer', '_template', 'injected'
]);
expect(parameters !.map(parameter => parameter.typeExpression !.getText())).toEqual([
'ViewContainerRef', 'TemplateRef', 'String'
expectTypeValueReferencesForParameters(parameters !, [
'ViewContainerRef',
'TemplateRef',
'String',
]);
});
@ -332,7 +336,10 @@ describe('Esm5ReflectionHost [import helper style]', () => {
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode) !;
const identifierOfViewContainerRef = ctrDecorators[0].typeExpression !as ts.Identifier;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{
local: true,
expression: ts.Identifier
}).expression;
const expectedDeclarationNode = getDeclaration(
program, '/some_directive.js', 'ViewContainerRef', ts.isVariableDeclaration);

View File

@ -13,6 +13,8 @@ import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {Esm5ReflectionHost} from '../../src/host/esm5_host';
import {getDeclaration, makeTestProgram} from '../helpers/utils';
import {expectTypeValueReferencesForParameters} from './util';
const SOME_DIRECTIVE_FILE = {
name: '/some_directive.js',
contents: `
@ -918,8 +920,10 @@ describe('Esm5ReflectionHost', () => {
expect(parameters !.map(parameter => parameter.name)).toEqual([
'_viewContainer', '_template', 'injected'
]);
expect(parameters !.map(parameter => parameter.typeExpression !.getText())).toEqual([
'ViewContainerRef', 'TemplateRef', 'undefined'
expectTypeValueReferencesForParameters(parameters !, [
'ViewContainerRef',
'TemplateRef',
null,
]);
});
@ -1251,7 +1255,10 @@ describe('Esm5ReflectionHost', () => {
const classNode = getDeclaration(
program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isVariableDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode) !;
const identifierOfViewContainerRef = ctrDecorators[0].typeExpression !as ts.Identifier;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{
local: true,
expression: ts.Identifier
}).expression;
const expectedDeclarationNode = getDeclaration(
program, SOME_DIRECTIVE_FILE.name, 'ViewContainerRef', ts.isVariableDeclaration);

View File

@ -0,0 +1,31 @@
/**
* @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 * as ts from 'typescript';
import {CtorParameter} from '../../../ngtsc/reflection';
/**
* Check that a given list of `CtorParameter`s has `typeValueReference`s of specific `ts.Identifier`
* names.
*/
export function expectTypeValueReferencesForParameters(
parameters: CtorParameter[], expectedParams: (string | null)[]) {
parameters !.forEach((param, idx) => {
const expected = expectedParams[idx];
if (expected !== null) {
if (param.typeValueReference === null || !param.typeValueReference.local ||
!ts.isIdentifier(param.typeValueReference.expression)) {
fail(`Incorrect typeValueReference generated, expected ${expected}`);
} else {
expect(param.typeValueReference.expression.text).toEqual(expected);
}
}
});
}

View File

@ -6,11 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ExternalExpr, Identifiers, InvokeFunctionExpr, Statement, WrappedNodeExpr} from '@angular/compiler';
import {Expression, ExternalExpr, FunctionExpr, Identifiers, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, ReturnStatement, Statement, WrappedNodeExpr, literalMap} from '@angular/compiler';
import * as ts from 'typescript';
import {CtorParameter, Decorator, ReflectionHost} from '../../reflection';
import {valueReferenceToExpression} from './util';
/**
* Given a class declaration, generate a call to `setClassMetadata` with the Angular metadata
* present on the class or its member fields.
@ -39,18 +41,13 @@ export function generateSetClassMetadataCall(
const metaDecorators = ts.createArrayLiteral(ngClassDecorators);
// Convert the constructor parameters to metadata, passing null if none are present.
let metaCtorParameters: ts.Expression = ts.createNull();
let metaCtorParameters: Expression = new LiteralExpr(null);
const classCtorParameters = reflection.getConstructorParameters(clazz);
if (classCtorParameters !== null) {
const ctorParameters = ts.createArrayLiteral(
classCtorParameters.map(param => ctorParameterToMetadata(param, isCore)));
metaCtorParameters = ts.createFunctionExpression(
/* modifiers */ undefined,
/* asteriskToken */ undefined,
/* name */ undefined,
/* typeParameters */ undefined,
/* parameters */ undefined,
/* type */ undefined, ts.createBlock([ts.createReturn(ctorParameters)]));
const ctorParameters = classCtorParameters.map(param => ctorParameterToMetadata(param, isCore));
metaCtorParameters = new FunctionExpr([], [
new ReturnStatement(new LiteralArrayExpr(ctorParameters)),
]);
}
// Do the same for property decorators.
@ -71,7 +68,7 @@ export function generateSetClassMetadataCall(
[
new WrappedNodeExpr(id),
new WrappedNodeExpr(metaDecorators),
new WrappedNodeExpr(metaCtorParameters),
metaCtorParameters,
new WrappedNodeExpr(metaPropDecorators),
],
/* type */ undefined,
@ -83,22 +80,25 @@ export function generateSetClassMetadataCall(
/**
* Convert a reflected constructor parameter to metadata.
*/
function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): ts.Expression {
function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): Expression {
// Parameters sometimes have a type that can be referenced. If so, then use it, otherwise
// its type is undefined.
const type =
param.typeExpression !== null ? param.typeExpression : ts.createIdentifier('undefined');
const properties: ts.ObjectLiteralElementLike[] = [
ts.createPropertyAssignment('type', type),
const type = param.typeValueReference !== null ?
valueReferenceToExpression(param.typeValueReference) :
new LiteralExpr(undefined);
const mapEntries: {key: string, value: Expression, quoted: false}[] = [
{key: 'type', value: type, quoted: false},
];
// 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)));
const value = new WrappedNodeExpr(ts.createArrayLiteral(ngDecorators));
mapEntries.push({key: 'decorators', value, quoted: false});
}
return ts.createObjectLiteral(properties, true);
return literalMap(mapEntries);
}
/**

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
import {Expression, ExternalExpr, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {ImportMode, Reference, ReferenceEmitter} from '../../imports';
import {ForeignFunctionResolver} from '../../partial_evaluator';
import {ClassMemberKind, CtorParameter, Decorator, ReflectionHost} from '../../reflection';
import {ClassMemberKind, CtorParameter, Decorator, ReflectionHost, TypeValueReference} from '../../reflection';
export enum ConstructorDepErrorKind {
NO_SUITABLE_TOKEN,
@ -45,7 +45,7 @@ export function getConstructorDependencies(
}
}
ctorParams.forEach((param, idx) => {
let tokenExpr = param.typeExpression;
let token = valueReferenceToExpression(param.typeValueReference);
let optional = false, self = false, skipSelf = false, host = false;
let resolved = R3ResolvedDependencyType.Token;
(param.decorators || []).filter(dec => isCore || isAngularCore(dec)).forEach(dec => {
@ -56,7 +56,7 @@ export function getConstructorDependencies(
ErrorCode.DECORATOR_ARITY_WRONG, dec.node,
`Unexpected number of arguments to @Inject().`);
}
tokenExpr = dec.args[0];
token = new WrappedNodeExpr(dec.args[0]);
} else if (name === 'Optional') {
optional = true;
} else if (name === 'SkipSelf') {
@ -71,20 +71,19 @@ export function getConstructorDependencies(
ErrorCode.DECORATOR_ARITY_WRONG, dec.node,
`Unexpected number of arguments to @Attribute().`);
}
tokenExpr = dec.args[0];
token = new WrappedNodeExpr(dec.args[0]);
resolved = R3ResolvedDependencyType.Attribute;
} else {
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_UNEXPECTED, dec.node, `Unexpected decorator ${name} on parameter.`);
}
});
if (tokenExpr === null) {
if (token === null) {
errors.push({
index: idx,
kind: ConstructorDepErrorKind.NO_SUITABLE_TOKEN, param,
});
} else {
const token = new WrappedNodeExpr(tokenExpr);
deps.push({token, optional, self, skipSelf, host, resolved});
}
});
@ -95,6 +94,27 @@ export function getConstructorDependencies(
}
}
/**
* Convert a `TypeValueReference` to an `Expression` which refers to the type as a value.
*
* Local references are converted to a `WrappedNodeExpr` of the TypeScript expression, and non-local
* references are converted to an `ExternalExpr`. Note that this is only valid in the context of the
* file in which the `TypeValueReference` originated.
*/
export function valueReferenceToExpression(valueRef: TypeValueReference): Expression;
export function valueReferenceToExpression(valueRef: null): null;
export function valueReferenceToExpression(valueRef: TypeValueReference | null): Expression|null;
export function valueReferenceToExpression(valueRef: TypeValueReference | null): Expression|null {
if (valueRef === null) {
return null;
} else if (valueRef.local) {
return new WrappedNodeExpr(valueRef.expression);
} else {
// TODO(alxhub): this cast is necessary because the g3 typescript version doesn't narrow here.
return new ExternalExpr(valueRef as{moduleName: string, name: string});
}
}
export function getValidConstructorDependencies(
clazz: ts.ClassDeclaration, reflector: ReflectionHost, isCore: boolean): R3DependencyMetadata[]|
null {

View File

@ -46,7 +46,7 @@ describe('ngtsc setClassMetadata converter', () => {
}
`);
expect(res).toContain(
`function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: Injector }]; }, null);`);
`function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: i0.Injector }]; }, null);`);
});
it('should convert decorated field metadata', () => {

View File

@ -151,6 +151,31 @@ export interface ClassMember {
decorators: Decorator[]|null;
}
/**
* A reference to a value that originated from a type position.
*
* For example, a constructor parameter could be declared as `foo: Foo`. A `TypeValueReference`
* extracted from this would refer to the value of the class `Foo` (assuming it was actually a
* type).
*
* There are two kinds of such references. A reference with `local: false` refers to a type that was
* imported, and gives the symbol `name` and the `moduleName` of the import. Note that this
* `moduleName` may be a relative path, and thus is likely only valid within the context of the file
* which contained the original type reference.
*
* A reference with `local: true` refers to any other kind of type via a `ts.Expression` that's
* valid within the local file where the type was referenced.
*/
export type TypeValueReference = {
local: true; expression: ts.Expression;
} |
{
local: false;
name: string;
moduleName: string;
valueDeclaration: ts.Declaration;
};
/**
* A parameter to a constructor.
*/
@ -172,18 +197,22 @@ export interface CtorParameter {
nameNode: ts.BindingName;
/**
* TypeScript `ts.Expression` representing the type value of the parameter, if the type is a
* simple
* expression type that can be converted to a value.
* Reference to the value of the parameter's type annotation, if it's possible to refer to the
* parameter's type as a value.
*
* If the type is not present or cannot be represented as an expression, `type` is `null`.
* This can either be a reference to a local value, in which case it has `local` set to `true` and
* contains a `ts.Expression`, or it's a reference to an imported value, in which case `local` is
* set to `false` and the symbol and module name of the imported value are provided instead.
*
* If the type is not present or cannot be represented as an expression, `typeValueReference` is
* `null`.
*/
typeExpression: ts.Expression|null;
typeValueReference: TypeValueReference|null;
/**
* TypeScript `ts.TypeNode` representing the type node found in the type position.
*
* This field can be used for diagnostics reporting if `typeExpression` is `null`.
* This field can be used for diagnostics reporting if `typeValueReference` is `null`.
*
* Can be null, if the param has no type declared.
*/

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Import, ReflectionHost} from './host';
import {ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Import, ReflectionHost, TypeValueReference} from './host';
/**
* reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`.
@ -48,7 +48,7 @@ export class TypeScriptReflectionHost implements ReflectionHost {
// It may or may not be possible to write an expression that refers to the value side of the
// type named for the parameter.
let typeValueExpr: ts.Expression|null = null;
let typeValueExpr: TypeValueReference|null = null;
let originalTypeNode = node.type || null;
let typeNode = originalTypeNode;
@ -69,27 +69,57 @@ export class TypeScriptReflectionHost implements ReflectionHost {
// It's not possible to get a value expression if the parameter doesn't even have a type.
if (typeNode && ts.isTypeReferenceNode(typeNode)) {
// It's only valid to convert a type reference to a value reference if the type actually has
// a value declaration associated with it.
let symbol: ts.Symbol|undefined = this.checker.getSymbolAtLocation(typeNode.typeName);
if (symbol !== undefined) {
let resolvedSymbol = symbol;
if (symbol.flags & ts.SymbolFlags.Alias) {
resolvedSymbol = this.checker.getAliasedSymbol(symbol);
}
if (resolvedSymbol.valueDeclaration !== undefined) {
const symbols = resolveTypeSymbols(typeNode, this.checker);
if (symbols !== null) {
const {local, decl} = symbols;
// It's only valid to convert a type reference to a value reference if the type actually
// has a value declaration associated with it.
if (decl.valueDeclaration !== undefined) {
// The type points to a valid value declaration. Rewrite the TypeReference into an
// Expression which references the value pointed to by the TypeReference, if possible.
const firstDecl = symbol.declarations && symbol.declarations[0];
// Look at the local `ts.Symbol`'s declarations and see if it comes from an import
// statement. If so, extract the module specifier and the name of the imported type.
const firstDecl = local.declarations && local.declarations[0];
if (firstDecl && ts.isImportSpecifier(firstDecl)) {
// Making sure TS produces the necessary imports in case a symbol was declared in a
// different script and imported. To do that we check symbol's first declaration and
// if it's an import - use its identifier. The `Identifier` from the `ImportSpecifier`
// knows it could be a value reference, and will emit as one if needed.
typeValueExpr = ts.updateIdentifier(firstDecl.name);
// The symbol was imported by name, in a ts.ImportSpecifier.
const name = (firstDecl.propertyName || firstDecl.name).text;
const moduleSpecifier = firstDecl.parent.parent.parent.moduleSpecifier;
if (!ts.isStringLiteral(moduleSpecifier)) {
throw new Error('not a module specifier');
}
const moduleName = moduleSpecifier.text;
typeValueExpr = {
local: false,
name,
moduleName,
valueDeclaration: decl.valueDeclaration,
};
} else if (
firstDecl && ts.isNamespaceImport(firstDecl) && symbols.importName !== null) {
// The symbol was imported via a namespace import. In this case, the name to use when
// importing it was extracted by resolveTypeSymbols.
const name = symbols.importName;
const moduleSpecifier = firstDecl.parent.parent.moduleSpecifier;
if (!ts.isStringLiteral(moduleSpecifier)) {
throw new Error('not a module specifier');
}
const moduleName = moduleSpecifier.text;
typeValueExpr = {
local: false,
name,
moduleName,
valueDeclaration: decl.valueDeclaration,
};
} else {
typeValueExpr = typeNodeToValueExpr(typeNode);
const expression = typeNodeToValueExpr(typeNode);
if (expression !== null) {
typeValueExpr = {
local: true,
expression,
};
}
}
}
}
@ -98,7 +128,7 @@ export class TypeScriptReflectionHost implements ReflectionHost {
return {
name,
nameNode: node.name,
typeExpression: typeValueExpr,
typeValueReference: typeValueExpr,
typeNode: originalTypeNode, decorators,
};
});
@ -486,3 +516,48 @@ function propertyNameToString(node: ts.PropertyName): string|null {
return null;
}
}
/**
* Resolve a `TypeReference` node to the `ts.Symbol`s for both its declaration and its local source.
*
* In the event that the `TypeReference` refers to a locally declared symbol, these will be the
* same. If the `TypeReference` refers to an imported symbol, then `decl` will be the fully resolved
* `ts.Symbol` of the referenced symbol. `local` will be the `ts.Symbol` of the `ts.Identifer` which
* points to the import statement by which the symbol was imported.
*/
function resolveTypeSymbols(typeRef: ts.TypeReferenceNode, checker: ts.TypeChecker):
{local: ts.Symbol, decl: ts.Symbol, importName: string | null}|null {
const typeName = typeRef.typeName;
// typeRefSymbol is the ts.Symbol of the entire type reference.
const typeRefSymbol: ts.Symbol|undefined = checker.getSymbolAtLocation(typeName);
if (typeRefSymbol === undefined) {
return null;
}
// local is the ts.Symbol for the local ts.Identifier for the type.
// If the type is actually locally declared or is imported by name, for example:
// import {Foo} from './foo';
// then it'll be the same as top. If the type is imported via a namespace import, for example:
// import * as foo from './foo';
// and then referenced as:
// constructor(f: foo.Foo)
// then local will be the ts.Symbol of `foo`, whereas top will be the ts.Symbol of `foo.Foo`.
// This allows tracking of the import behind whatever type reference exists.
let local = typeRefSymbol;
let importName: string|null = null;
if (ts.isQualifiedName(typeName) && ts.isIdentifier(typeName.left) &&
ts.isIdentifier(typeName.right)) {
const localTmp = checker.getSymbolAtLocation(typeName.left);
if (localTmp !== undefined) {
local = localTmp;
importName = typeName.right.text;
}
}
// De-alias the top-level type reference symbol to get the symbol of the actual declaration.
let decl = typeRefSymbol;
if (typeRefSymbol.flags & ts.SymbolFlags.Alias) {
decl = checker.getAliasedSymbol(typeRefSymbol);
}
return {local, decl, importName};
}

View File

@ -116,12 +116,38 @@ describe('reflector', () => {
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(2);
expectParameter(args[0], 'bar', 'Bar');
expectParameter(args[1], 'otherBar', 'star.Bar');
expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'});
expectParameter(args[1], 'otherBar', {moduleName: './bar', name: 'Bar'});
});
it('should reflect an argument from an aliased import', () => {
const {program} = makeProgram([
{
name: 'bar.ts',
contents: `
export class Bar {}
`
},
{
name: 'entry.ts',
contents: `
import {Bar as LocalBar} from './bar';
it('should reflect an nullable argument', () => {
class Foo {
constructor(bar: LocalBar) {}
}
`
}
]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(1);
expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'});
});
it('should reflect a nullable argument', () => {
const {program} = makeProgram([
{
name: 'bar.ts',
@ -145,7 +171,7 @@ describe('reflector', () => {
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(1);
expectParameter(args[0], 'bar', 'Bar');
expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'});
});
});
@ -193,14 +219,24 @@ describe('reflector', () => {
});
function expectParameter(
param: CtorParameter, name: string, type?: string, decorator?: string,
decoratorFrom?: string): void {
param: CtorParameter, name: string, type?: string | {name: string, moduleName: string},
decorator?: string, decoratorFrom?: string): void {
expect(param.name !).toEqual(name);
if (type === undefined) {
expect(param.typeExpression).toBeNull();
expect(param.typeValueReference).toBeNull();
} else {
expect(param.typeExpression).not.toBeNull();
expect(argExpressionToString(param.typeExpression !)).toEqual(type);
if (param.typeValueReference === null) {
return fail(`Expected parameter ${name} to have a typeValueReference`);
}
if (param.typeValueReference.local && typeof type === 'string') {
expect(argExpressionToString(param.typeValueReference.expression)).toEqual(type);
} else if (!param.typeValueReference.local && typeof type !== 'string') {
expect(param.typeValueReference.moduleName).toEqual(type.moduleName);
expect(param.typeValueReference.name).toEqual(type.name);
} else {
return fail(
`Mismatch between typeValueReference and expected type: ${param.name} / ${param.typeValueReference.local}`);
}
}
if (decorator !== undefined) {
expect(param.decorators).not.toBeNull();

View File

@ -740,8 +740,8 @@ describe('compiler compliance', () => {
selectors: [["my-component"]],
factory: function MyComponent_Factory(t) {
return new (t || MyComponent)(
$r3$.ɵdirectiveInject(ElementRef), $r3$.ɵdirectiveInject(ViewContainerRef),
$r3$.ɵdirectiveInject(ChangeDetectorRef));
$r3$.ɵdirectiveInject($i$.ElementRef), $r3$.ɵdirectiveInject($i$.ViewContainerRef),
$r3$.ɵdirectiveInject($i$.ChangeDetectorRef));
},
consts: 0,
vars: 0,
@ -784,7 +784,7 @@ describe('compiler compliance', () => {
IfDirective.ngDirectiveDef = $r3$.ɵdefineDirective({
type: IfDirective,
selectors: [["", "if", ""]],
factory: function IfDirective_Factory(t) { return new (t || IfDirective)($r3$.ɵdirectiveInject(TemplateRef)); }
factory: function IfDirective_Factory(t) { return new (t || IfDirective)($r3$.ɵdirectiveInject($i$.TemplateRef)); }
});`;
const MyComponentDefinition = `
const $c1$ = ["foo", ""];

View File

@ -1104,7 +1104,7 @@ describe('ngtsc behavioral tests', () => {
const jsContents = env.getContents('test.js');
expect(jsContents)
.toContain(
`factory: function FooCmp_Factory(t) { return new (t || FooCmp)(i0.ɵinjectAttribute("test"), i0.ɵdirectiveInject(ChangeDetectorRef), i0.ɵdirectiveInject(ElementRef), i0.ɵdirectiveInject(Injector), i0.ɵdirectiveInject(Renderer2), i0.ɵdirectiveInject(TemplateRef), i0.ɵdirectiveInject(ViewContainerRef)); }`);
`factory: function FooCmp_Factory(t) { return new (t || FooCmp)(i0.ɵinjectAttribute("test"), i0.ɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵdirectiveInject(i0.ElementRef), i0.ɵdirectiveInject(i0.Injector), i0.ɵdirectiveInject(i0.Renderer2), i0.ɵdirectiveInject(i0.TemplateRef), i0.ɵdirectiveInject(i0.ViewContainerRef)); }`);
});
it('should generate queries for components', () => {
@ -1932,9 +1932,9 @@ describe('ngtsc behavioral tests', () => {
env.driveMain();
const jsContents = trim(env.getContents('test.js'));
expect(jsContents).toContain(`import { MyTypeA, MyTypeB } from './types';`);
expect(jsContents).toMatch(setClassMetadataRegExp('type: MyTypeA'));
expect(jsContents).toMatch(setClassMetadataRegExp('type: MyTypeB'));
expect(jsContents).toContain(`import * as i1 from "./types";`);
expect(jsContents).toMatch(setClassMetadataRegExp('type: i1\\.MyTypeA'));
expect(jsContents).toMatch(setClassMetadataRegExp('type: i1\\.MyTypeB'));
});
it('should use imported types in setClassMetadata if they can be represented as values and imported as `* as foo`',
@ -1961,13 +1961,13 @@ describe('ngtsc behavioral tests', () => {
export class SomeComp {
constructor(@Inject('arg-token') arg: types.MyTypeB) {}
}
`);
`);
env.driveMain();
const jsContents = trim(env.getContents('test.js'));
expect(jsContents).toContain(`import * as types from './types';`);
expect(jsContents).toMatch(setClassMetadataRegExp('type: types.MyTypeA'));
expect(jsContents).toMatch(setClassMetadataRegExp('type: types.MyTypeB'));
expect(jsContents).toMatch(setClassMetadataRegExp('type: i\\d\\.MyTypeA'));
expect(jsContents).toMatch(setClassMetadataRegExp('type: i\\d\\.MyTypeB'));
});
it('should use `undefined` in setClassMetadata if types can\'t be represented as values', () => {

View File

@ -76,7 +76,7 @@ export * from './ml_parser/interpolation_config';
export * from './ml_parser/tags';
export {LexerRange} from './ml_parser/lexer';
export {NgModuleCompiler} from './ng_module_compiler';
export {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinType, BuiltinTypeName, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, TypeofExpr, collectExternalReferences} from './output/output_ast';
export {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinType, BuiltinTypeName, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, literalMap, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, TypeofExpr, collectExternalReferences} from './output/output_ast';
export {EmitterVisitorContext} from './output/abstract_emitter';
export {JitEvaluator} from './output/output_jit';
export * from './output/ts_emitter';