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:
parent
20a9dbef8e
commit
881807dc36
|
@ -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
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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", ""];
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue