Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion goldens/public-api/common/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
// (undocumented)
static ngAcceptInputType_height: unknown;
// (undocumented)
static ngAcceptInputType_ngSrc: string | i1_2.SafeValue;
static ngAcceptInputType_ngSrc: string | i0.ɵSafeValue;
// (undocumented)
static ngAcceptInputType_priority: unknown;
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,10 @@ function parseInputTransformFunction(
// Treat functions with no arguments as `unknown` since returning
// the same value from the transform function is valid.
if (!firstParam) {
return {node, type: ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)};
return {
node,
type: new Reference(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword))
};
}

// This should be caught by `noImplicitAny` already, but null check it just in case.
Expand All @@ -795,7 +798,8 @@ function parseInputTransformFunction(

assertEmittableInputType(firstParam.type, clazz.getSourceFile(), reflector, refEmitter);

return {node, type: firstParam.type};
const viaModule = value instanceof Reference ? value.bestGuessOwningModule : null;
return {node, type: new Reference(firstParam.type, viaModule)};
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-cli/src/ngtsc/metadata/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export type InputMapping = InputOrOutput&{
/** Metadata for an input's transform function. */
export interface InputTransform {
node: ts.Node;
type: ts.TypeNode;
type: Reference<ts.TypeNode>;
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/translator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {ImportGenerator, NamedImport} from './src/api/import_generator';
export {Context} from './src/context';
export {Import, ImportManager} from './src/import_manager';
export {ExpressionTranslatorVisitor, RecordWrappedNodeFn, TranslatorOptions} from './src/translator';
export {canEmitType, TypeEmitter, TypeReferenceTranslator} from './src/type_emitter';
export {translateType} from './src/type_translator';
export {attachComments, createTemplateMiddle, createTemplateTail, TypeScriptAstFactory} from './src/typescript_ast_factory';
export {translateExpression, translateStatement} from './src/typescript_translator';
92 changes: 32 additions & 60 deletions packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
import * as o from '@angular/compiler';
import ts from 'typescript';

import {assertSuccessfulReferenceEmit, ImportFlags, Reference, ReferenceEmitter} from '../../imports';
import {assertSuccessfulReferenceEmit, ImportFlags, OwningModule, Reference, ReferenceEmitter} from '../../imports';
import {ReflectionHost} from '../../reflection';

import {Context} from './context';
import {ImportManager} from './import_manager';
import {TypeEmitter} from './type_emitter';


export function translateType(
Expand Down Expand Up @@ -79,12 +80,17 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor {
return ts.factory.createTypeLiteralNode([indexSignature]);
}

visitTransplantedType(ast: o.TransplantedType<ts.Node>, context: any) {
if (!ts.isTypeNode(ast.type)) {
visitTransplantedType(ast: o.TransplantedType<unknown>, context: Context) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: would it make sense to use something like ts.Node | Reference here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided not to, because the T of TransplantedType is not constrained so using ts.Node | Reference here might not be accurate.

const node = ast.type instanceof Reference ? ast.type.node : ast.type;
if (!ts.isTypeNode(node)) {
throw new Error(`A TransplantedType must wrap a TypeNode`);
}

return this.translateTransplantedTypeNode(ast.type, context);
const viaModule = ast.type instanceof Reference ? ast.type.bestGuessOwningModule : null;

const emitter =
new TypeEmitter(typeRef => this.translateTypeReference(typeRef, context, viaModule));
return emitter.emitType(node);
}

visitReadVarExpr(ast: o.ReadVarExpr, context: Context): ts.TypeQueryNode {
Expand Down Expand Up @@ -253,70 +259,36 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor {
return typeNode;
}

/**
* Translates a type reference node so that all of its references
* are imported into the context file.
*/
private translateTransplantedTypeReferenceNode(
node: ts.TypeReferenceNode&{typeName: ts.Identifier}, context: any): ts.TypeReferenceNode {
const declaration = this.reflector.getDeclarationOfIdentifier(node.typeName);

private translateTypeReference(
type: ts.TypeReferenceNode, context: Context,
viaModule: OwningModule|null): ts.TypeReferenceNode|null {
const target = ts.isIdentifier(type.typeName) ? type.typeName : type.typeName.right;
const declaration = this.reflector.getDeclarationOfIdentifier(target);
if (declaration === null) {
throw new Error(
`Unable to statically determine the declaration file of type node ${node.typeName.text}`);
`Unable to statically determine the declaration file of type node ${target.text}`);
}

const emittedType = this.refEmitter.emit(
new Reference(declaration.node), this.contextFile,
ImportFlags.NoAliasing | ImportFlags.AllowTypeImports |
ImportFlags.AllowRelativeDtsImports);

assertSuccessfulReferenceEmit(emittedType, node, 'type');

const result = emittedType.expression.visitExpression(this, context);

if (!ts.isTypeReferenceNode(result)) {
throw new Error(`Expected TypeReferenceNode when referencing the type for ${
node.typeName.text}, but received ${ts.SyntaxKind[result.kind]}`);
let owningModule = viaModule;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can reduce this to:

const owningModule = declaration.viaModule === null ? viaModule : owningModule = {
  specifier: declaration.viaModule,
  resolutionContext: type.getSourceFile().fileName,
};

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would do the same indeed, but I find the current logic more readable.

if (declaration.viaModule !== null) {
owningModule = {
specifier: declaration.viaModule,
resolutionContext: type.getSourceFile().fileName,
};
}

// If the original node doesn't have any generic parameters we return the results.
if (node.typeArguments === undefined || node.typeArguments.length === 0) {
return result;
}
const reference = new Reference(declaration.node, owningModule);
const emittedType = this.refEmitter.emit(
reference, this.contextFile, ImportFlags.NoAliasing | ImportFlags.AllowTypeImports);

// If there are any generics, we have to reflect them as well.
const translatedArgs =
node.typeArguments.map(arg => this.translateTransplantedTypeNode(arg, context));

return ts.factory.updateTypeReferenceNode(
result, result.typeName, ts.factory.createNodeArray(translatedArgs));
}

/**
* Translates a type node so that all of the type references it
* contains are imported and can be referenced in the context file.
*/
private translateTransplantedTypeNode(rootNode: ts.TypeNode, context: any): ts.TypeNode {
const factory: ts.TransformerFactory<ts.Node> = transformContext => root => {
const walk = (node: ts.Node): ts.Node => {
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
const translated =
this.translateTransplantedTypeReferenceNode(node as ts.TypeReferenceNode & {
typeName: ts.Identifier;
}, context);

if (translated !== node) {
return translated;
}
}

return ts.visitEachChild(node, walk, transformContext);
};
assertSuccessfulReferenceEmit(emittedType, target, 'type');

return ts.visitNode(root, walk);
};
const typeNode = this.translateExpression(emittedType.expression, context);

return ts.transform(rootNode, [factory]).transformed[0] as ts.TypeNode;
if (!ts.isTypeReferenceNode(typeNode)) {
throw new Error(
`Expected TypeReferenceNode for emitted reference, got ${ts.SyntaxKind[typeNode.kind]}.`);
}
return typeNode;
}
}
4 changes: 2 additions & 2 deletions packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ export class Environment implements ReferenceEmitEnvironment {
/**
* Generates a `ts.TypeNode` representing a type that is being referenced from a different place
* in the program. Any type references inside the transplanted type will be rewritten so that
* they can be imported in the context fiel.
* they can be imported in the context file.
*/
referenceTransplantedType(type: TransplantedType<ts.TypeNode>): ts.TypeNode {
referenceTransplantedType(type: TransplantedType<Reference<ts.TypeNode>>): ts.TypeNode {
return translateType(
type, this.contextFile, this.reflector, this.refEmitter, this.importManager);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,7 @@ class TcbDirectiveInputsOp extends TcbOp {
if (this.dir.coercedInputFields.has(fieldName)) {
let type: ts.TypeNode;

if (transformType) {
if (transformType !== null) {
type = this.tcb.env.referenceTransplantedType(new TransplantedType(transformType));
} else {
// The input has a coercion declaration which should be used instead of assigning the
Expand Down Expand Up @@ -2067,7 +2067,10 @@ class Scope {

interface TcbBoundAttribute {
attribute: TmplAstBoundAttribute|TmplAstTextAttribute;
inputs: {fieldName: ClassPropertyName, required: boolean, transformType: ts.TypeNode|null}[];
inputs:
{fieldName: ClassPropertyName,
required: boolean,
transformType: Reference<ts.TypeNode>|null}[];
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ function constructTypeCtorParameter(
/* type */
transform == null ?
tsCreateTypeQueryForCoercedInput(rawType.typeName, classPropertyName) :
transform.type));
transform.type.node));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still broken, but likely hasn't caused issues because type constructors in the TCB are not translated to template code so they are never reported to users.

The type node should go through the emitter process here as well, as we can't simply dump the original type node in a completely different source file as is currently done. Fixing it requires some more extensive changes though so I kept it broken for now.

}
}
if (plainKeys.length > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import ts from 'typescript';

import {OwningModule, Reference} from '../../imports';
import {DeclarationNode, ReflectionHost} from '../../reflection';

import {canEmitType, TypeEmitter} from './type_emitter';

import {canEmitType, TypeEmitter} from '../../translator';

/**
* See `TypeEmitter` for more information on the emitting process.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import ts from 'typescript';

import {initMockFileSystem} from '../../file_system/testing';
import {Reference} from '../../imports';
import {TypeCheckingConfig} from '../api';
import {ALL_ENABLED_CONFIG, tcb, TestDeclaration, TestDirective} from '../testing';

Expand Down Expand Up @@ -611,10 +612,10 @@ describe('type check blocks', () => {
transform: {
node: ts.factory.createFunctionDeclaration(
undefined, undefined, undefined, undefined, [], undefined, undefined),
type: ts.factory.createUnionTypeNode([
type: new Reference(ts.factory.createUnionTypeNode([
ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword),
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
])
]))
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,10 @@ TestClass.ngTypeCtor({value: 'test'});
bindingPropertyName: 'baz',
required: false,
transform: {
type: ts.factory.createUnionTypeNode([
type: new Reference(ts.factory.createUnionTypeNode([
ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword),
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
]),
])),
node: ts.factory.createFunctionDeclaration(
undefined, undefined, undefined, undefined, [], undefined, undefined)
}
Expand Down
31 changes: 29 additions & 2 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8713,7 +8713,7 @@ function allTests(os: string) {
expect(jsContents).toContain(`import { externalToNumber } from 'external';`);
expect(jsContents).toContain('inputs: { value: ["value", "value", externalToNumber] }');
expect(jsContents).toContain('features: [i0.ɵɵInputTransformsFeature]');
expect(dtsContents).toContain('import * as i1 from "./node_modules/external/index";');
expect(dtsContents).toContain('import * as i1 from "external";');
expect(dtsContents).toContain('static ngAcceptInputType_value: i1.ExternalToNumberType;');
});

Expand Down Expand Up @@ -8744,10 +8744,37 @@ function allTests(os: string) {
expect(jsContents)
.toContain('inputs: { value: ["value", "value", (value) => value ? 1 : 0] }');
expect(jsContents).toContain('features: [i0.ɵɵInputTransformsFeature]');
expect(dtsContents).toContain('import * as i1 from "./node_modules/external/index";');
expect(dtsContents).toContain('import * as i1 from "external";');
expect(dtsContents).toContain('static ngAcceptInputType_value: i1.ExternalToNumberType;');
});

it('should compile an input referencing an imported function with literal types', () => {
env.write('/transforms.ts', `
export function toBoolean(value: boolean | '' | 'true' | 'false'): boolean {
return !!value;
}
`);
env.write('/test.ts', `
import {Directive, Input} from '@angular/core';
import {toBoolean} from './transforms';

@Directive({standalone: true})
export class Dir {
@Input({transform: toBoolean}) value!: number;
}
`);

env.driveMain();

const jsContents = env.getContents('test.js');
const dtsContents = env.getContents('test.d.ts');

expect(jsContents).toContain('inputs: { value: ["value", "value", toBoolean] }');
expect(jsContents).toContain('features: [i0.ɵɵInputTransformsFeature]');
expect(dtsContents)
.toContain(`static ngAcceptInputType_value: boolean | "" | "true" | "false";`);
});

it('should compile a directive input with a transform function with a `this` typing', () => {
env.write('/test.ts', `
import {Directive, Input} from '@angular/core';
Expand Down