diff --git a/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts b/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts index fc868464eae..6e40fdd6c50 100644 --- a/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts +++ b/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts @@ -11,8 +11,10 @@ import {assert} from '../../../../linker'; import { AstFactory, BinaryOperator, + BuiltInType, LeadingComment, ObjectLiteralProperty, + Parameter, SourceMapRange, TemplateLiteral, VariableDeclarationType, @@ -104,40 +106,40 @@ export class BabelAstFactory implements AstFactory< createFunctionDeclaration( functionName: string, - parameters: string[], + parameters: Parameter[], body: t.Statement, ): t.Statement { assert(body, t.isBlockStatement, 'a block'); return t.functionDeclaration( t.identifier(functionName), - parameters.map((param) => t.identifier(param)), + parameters.map((param) => identifierWithType(param.name, param.type)), body, ); } createArrowFunctionExpression( - parameters: string[], + parameters: Parameter[], body: t.Statement | t.Expression, ): t.Expression { if (t.isStatement(body)) { assert(body, t.isBlockStatement, 'a block'); } return t.arrowFunctionExpression( - parameters.map((param) => t.identifier(param)), + parameters.map((param) => identifierWithType(param.name, param.type)), body, ); } createFunctionExpression( functionName: string | null, - parameters: string[], + parameters: Parameter[], body: t.Statement, ): t.Expression { assert(body, t.isBlockStatement, 'a block'); const name = functionName !== null ? t.identifier(functionName) : null; return t.functionExpression( name, - parameters.map((param) => t.identifier(param)), + parameters.map((param) => identifierWithType(param.name, param.type)), body, ); } @@ -228,10 +230,11 @@ export class BabelAstFactory implements AstFactory< createVariableDeclaration( variableName: string, initializer: t.Expression | null, - type: VariableDeclarationType, + variableType: VariableDeclarationType, + type: t.TSType | null, ): t.Statement { - return t.variableDeclaration(type, [ - t.variableDeclarator(t.identifier(variableName), initializer), + return t.variableDeclaration(variableType, [ + t.variableDeclarator(identifierWithType(variableName, type), initializer), ]); } @@ -265,6 +268,79 @@ export class BabelAstFactory implements AstFactory< return node; } + + createBuiltInType(type: BuiltInType): t.TSType { + switch (type) { + case 'any': + return t.tsAnyKeyword(); + case 'boolean': + return t.tsBooleanKeyword(); + case 'number': + return t.tsNumberKeyword(); + case 'string': + return t.tsStringKeyword(); + case 'function': + return t.tsTypeReference(t.identifier('Function')); + case 'never': + return t.tsNeverKeyword(); + case 'unknown': + return t.tsUnknownKeyword(); + } + } + + createExpressionType(expression: t.Expression, typeParams: t.TSType[] | null): t.TSType { + const typeName = getEntityTypeFromExpression(expression); + return t.tsTypeReference( + typeName, + typeParams ? t.tsTypeParameterInstantiation(typeParams) : null, + ); + } + + createArrayType(elementType: t.TSType): t.TSType { + return t.tsArrayType(elementType); + } + + createMapType(valueType: t.TSType): t.TSType { + const keySignature = identifierWithType('key', this.createBuiltInType('string')); + return t.tsTypeLiteral([t.tsIndexSignature([keySignature], t.tsTypeAnnotation(valueType))]); + } + + transplantType(type: t.TSType): t.TSType { + if (t.isNode(type) && t.isTSType(type)) { + return type; + } + throw new Error('Attempting to transplant a type node from a non-Babel AST: ' + type); + } +} + +function getEntityTypeFromExpression(expression: t.Expression): t.Identifier | t.TSQualifiedName { + if (t.isIdentifier(expression)) { + return expression; + } + + if (t.isMemberExpression(expression)) { + const left = getEntityTypeFromExpression(expression.object); + + if (!t.isIdentifier(expression.property)) { + throw new Error( + `Unsupported property access for type reference: ${expression.property.type}`, + ); + } + + return t.tsQualifiedName(left, expression.property); + } + + throw new Error(`Unsupported expression for type reference: ${expression.type}`); +} + +function identifierWithType(name: string, type: t.TSType | null): t.Identifier { + const node = t.identifier(name); + + if (type !== null) { + node.typeAnnotation = t.tsTypeAnnotation(type); + } + + return node; } function isLExpression(expr: t.Expression): expr is Extract { diff --git a/packages/compiler-cli/linker/babel/test/ast/babel_ast_factory_spec.ts b/packages/compiler-cli/linker/babel/test/ast/babel_ast_factory_spec.ts index de749ac9a2c..d0364247272 100644 --- a/packages/compiler-cli/linker/babel/test/ast/babel_ast_factory_spec.ts +++ b/packages/compiler-cli/linker/babel/test/ast/babel_ast_factory_spec.ts @@ -149,9 +149,16 @@ describe('BabelAstFactory', () => { describe('createFunctionDeclaration()', () => { it('should create a function declaration node with the given name, parameters and body statements', () => { const stmts = statement.ast`{x = 10; y = 20;}`; - const fn = factory.createFunctionDeclaration('foo', ['arg1', 'arg2'], stmts); + const fn = factory.createFunctionDeclaration( + 'foo', + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + stmts, + ); expect(generate(fn).code).toEqual( - ['function foo(arg1, arg2) {', ' x = 10;', ' y = 20;', '}'].join('\n'), + ['function foo(arg1, arg2: number) {', ' x = 10;', ' y = 20;', '}'].join('\n'), ); }); }); @@ -159,18 +166,32 @@ describe('BabelAstFactory', () => { describe('createFunctionExpression()', () => { it('should create a function expression node with the given name, parameters and body statements', () => { const stmts = statement.ast`{x = 10; y = 20;}`; - const fn = factory.createFunctionExpression('foo', ['arg1', 'arg2'], stmts); + const fn = factory.createFunctionExpression( + 'foo', + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + stmts, + ); expect(t.isStatement(fn)).toBe(false); expect(generate(fn).code).toEqual( - ['function foo(arg1, arg2) {', ' x = 10;', ' y = 20;', '}'].join('\n'), + ['function foo(arg1, arg2: number) {', ' x = 10;', ' y = 20;', '}'].join('\n'), ); }); it('should create an anonymous function expression node if the name is null', () => { const stmts = statement.ast`{x = 10; y = 20;}`; - const fn = factory.createFunctionExpression(null, ['arg1', 'arg2'], stmts); + const fn = factory.createFunctionExpression( + null, + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + stmts, + ); expect(generate(fn).code).toEqual( - ['function (arg1, arg2) {', ' x = 10;', ' y = 20;', '}'].join('\n'), + ['function (arg1, arg2: number) {', ' x = 10;', ' y = 20;', '}'].join('\n'), ); }); }); @@ -178,8 +199,14 @@ describe('BabelAstFactory', () => { describe('createArrowFunctionExpression()', () => { it('should create an arrow function with an implicit return if a single statement is provided', () => { const expr = expression.ast`arg2 + arg1`; - const fn = factory.createArrowFunctionExpression(['arg1', 'arg2'], expr); - expect(generate(fn).code).toEqual('(arg1, arg2) => arg2 + arg1'); + const fn = factory.createArrowFunctionExpression( + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + expr, + ); + expect(generate(fn).code).toEqual('(arg1, arg2: number) => arg2 + arg1'); }); it('should create an arrow function with an implicit return object literal', () => { @@ -190,9 +217,15 @@ describe('BabelAstFactory', () => { it('should create an arrow function with a body when an array of statements is provided', () => { const stmts = statement.ast`{x = 10; y = 20; return x + y;}`; - const fn = factory.createArrowFunctionExpression(['arg1', 'arg2'], stmts); + const fn = factory.createArrowFunctionExpression( + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + stmts, + ); expect(generate(fn).code).toEqual( - ['(arg1, arg2) => {', ' x = 10;', ' y = 20;', ' return x + y;', '}'].join('\n'), + ['(arg1, arg2: number) => {', ' x = 10;', ' y = 20;', ' return x + y;', '}'].join('\n'), ); }); }); @@ -378,26 +411,37 @@ describe('BabelAstFactory', () => { describe('createVariableDeclaration()', () => { it('should create a variable declaration statement node for the given variable name and initializer', () => { const initializer = expression.ast`42`; - const varDecl = factory.createVariableDeclaration('foo', initializer, 'let'); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'let', null); expect(generate(varDecl).code).toEqual('let foo = 42;'); }); it('should create a constant declaration statement node for the given variable name and initializer', () => { const initializer = expression.ast`42`; - const varDecl = factory.createVariableDeclaration('foo', initializer, 'const'); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'const', null); expect(generate(varDecl).code).toEqual('const foo = 42;'); }); it('should create a downleveled variable declaration statement node for the given variable name and initializer', () => { const initializer = expression.ast`42`; - const varDecl = factory.createVariableDeclaration('foo', initializer, 'var'); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'var', null); expect(generate(varDecl).code).toEqual('var foo = 42;'); }); it('should create an uninitialized variable declaration statement node for the given variable name and a null initializer', () => { - const varDecl = factory.createVariableDeclaration('foo', null, 'let'); + const varDecl = factory.createVariableDeclaration('foo', null, 'let', null); expect(generate(varDecl).code).toEqual('let foo;'); }); + + it('should create a variable declaration with a type', () => { + const initializer = expression.ast`42`; + const varDecl = factory.createVariableDeclaration( + 'foo', + initializer, + 'let', + factory.createBuiltInType('number'), + ); + expect(generate(varDecl).code).toEqual('let foo: number = 42;'); + }); }); describe('createRegularExpressionLiteral()', () => { @@ -429,6 +473,55 @@ describe('BabelAstFactory', () => { }); }); + describe('createBuiltInType()', () => { + it('should create type annotations for built in types', () => { + expect(generate(factory.createBuiltInType('any')).code).toEqual('any'); + expect(generate(factory.createBuiltInType('boolean')).code).toEqual('boolean'); + expect(generate(factory.createBuiltInType('number')).code).toEqual('number'); + expect(generate(factory.createBuiltInType('string')).code).toEqual('string'); + expect(generate(factory.createBuiltInType('never')).code).toEqual('never'); + expect(generate(factory.createBuiltInType('unknown')).code).toEqual('unknown'); + expect(generate(factory.createBuiltInType('function')).code).toEqual('Function'); + }); + }); + + describe('createExpressionType()', () => { + it('should create a generic type annotation for an identifier', () => { + const id = factory.createIdentifier('MyType'); + const type = factory.createExpressionType(id, null); + expect(generate(type).code).toEqual('MyType'); + }); + + it('should create a generic type annotation for property access', () => { + const expr = factory.createPropertyAccess(factory.createIdentifier('ns'), 'MyType'); + const type = factory.createExpressionType(expr, null); + expect(generate(type).code).toEqual('ns.MyType'); + }); + + it('should create a generic type annotation with type parameters', () => { + const id = factory.createIdentifier('MyType'); + const typeParam = factory.createBuiltInType('string'); + const type = factory.createExpressionType(id, [typeParam]); + expect(generate(type).code).toEqual('MyType'); + }); + }); + + describe('createArrayType()', () => { + it('should create an array type annotation', () => { + const elementType = factory.createBuiltInType('string'); + const type = factory.createArrayType(elementType); + expect(generate(type).code).toEqual('string[]'); + }); + }); + + describe('createMapType()', () => { + it('should create an object type with an indexer', () => { + const valueType = factory.createBuiltInType('number'); + const type = factory.createMapType(valueType); + expect(generate(type).code).toEqual('{\n [key: string]: number;\n}'); + }); + }); + describe('setSourceMapRange()', () => { it('should attach the `sourceMapRange` to the given `node`', () => { const expr = expression.ast`42`; diff --git a/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts b/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts index ddd0e49f353..bf670f48995 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts @@ -10,8 +10,10 @@ import ts from 'typescript'; import { AstFactory, BinaryOperator, + BuiltInType, LeadingComment, ObjectLiteralProperty, + Parameter, SourceMapRange, TemplateLiteral, UnaryOperator, @@ -160,7 +162,7 @@ export class TypeScriptAstFactory implements AstFactory[], body: ts.Statement, ): ts.Statement { if (!ts.isBlock(body)) { @@ -171,7 +173,7 @@ export class TypeScriptAstFactory implements AstFactory ts.factory.createParameterDeclaration(undefined, undefined, param)), + parameters.map((param) => this.createParameter(param)), undefined, body, ); @@ -179,7 +181,7 @@ export class TypeScriptAstFactory implements AstFactory[], body: ts.Statement, ): ts.Expression { if (!ts.isBlock(body)) { @@ -190,14 +192,14 @@ export class TypeScriptAstFactory implements AstFactory ts.factory.createParameterDeclaration(undefined, undefined, param)), + parameters.map((param) => this.createParameter(param)), undefined, body, ); } createArrowFunctionExpression( - parameters: string[], + parameters: Parameter[], body: ts.Statement | ts.Expression, ): ts.Expression { if (ts.isStatement(body) && !ts.isBlock(body)) { @@ -207,13 +209,23 @@ export class TypeScriptAstFactory implements AstFactory ts.factory.createParameterDeclaration(undefined, undefined, param)), + parameters.map((param) => this.createParameter(param)), undefined, undefined, body, ); } + private createParameter(param: Parameter): ts.ParameterDeclaration { + return ts.factory.createParameterDeclaration( + undefined, + undefined, + param.name, + undefined, + param.type ?? undefined, + ); + } + createIdentifier = ts.factory.createIdentifier; createIfStatement( @@ -330,7 +342,8 @@ export class TypeScriptAstFactory implements AstFactory { items: [body], generate, } = setupStatements('{x = 10; y = 20;}'); - const fn = factory.createFunctionDeclaration('foo', ['arg1', 'arg2'], body); - expect(generate(fn)).toEqual('function foo(arg1, arg2) { x = 10; y = 20; }'); + const fn = factory.createFunctionDeclaration( + 'foo', + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + body, + ); + expect(generate(fn)).toEqual('function foo(arg1, arg2: number) { x = 10; y = 20; }'); }); }); @@ -189,9 +196,16 @@ describe('TypeScriptAstFactory', () => { items: [body], generate, } = setupStatements('{x = 10; y = 20;}'); - const fn = factory.createFunctionExpression('foo', ['arg1', 'arg2'], body); + const fn = factory.createFunctionExpression( + 'foo', + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + body, + ); expect(ts.isExpressionStatement(fn)).toBe(false); - expect(generate(fn)).toEqual('function foo(arg1, arg2) { x = 10; y = 20; }'); + expect(generate(fn)).toEqual('function foo(arg1, arg2: number) { x = 10; y = 20; }'); }); it('should create an anonymous function expression node if the name is null', () => { @@ -199,8 +213,15 @@ describe('TypeScriptAstFactory', () => { items: [body], generate, } = setupStatements('{x = 10; y = 20;}'); - const fn = factory.createFunctionExpression(null, ['arg1', 'arg2'], body); - expect(generate(fn)).toEqual('function (arg1, arg2) { x = 10; y = 20; }'); + const fn = factory.createFunctionExpression( + null, + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + body, + ); + expect(generate(fn)).toEqual('function (arg1, arg2: number) { x = 10; y = 20; }'); }); }); @@ -242,8 +263,14 @@ describe('TypeScriptAstFactory', () => { items: [body], generate, } = setupExpressions('arg2 + arg1'); - const fn = factory.createArrowFunctionExpression(['arg1', 'arg2'], body); - expect(generate(fn)).toEqual('(arg1, arg2) => arg2 + arg1'); + const fn = factory.createArrowFunctionExpression( + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + body, + ); + expect(generate(fn)).toEqual('(arg1, arg2: number) => arg2 + arg1'); }); it('should create an arrow function with an implicit return object literal', () => { @@ -260,8 +287,14 @@ describe('TypeScriptAstFactory', () => { items: [body], generate, } = setupStatements('{x = 10; y = 20; return x + y;}'); - const fn = factory.createArrowFunctionExpression(['arg1', 'arg2'], body); - expect(generate(fn)).toEqual('(arg1, arg2) => { x = 10; y = 20; return x + y; }'); + const fn = factory.createArrowFunctionExpression( + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + body, + ); + expect(generate(fn)).toEqual('(arg1, arg2: number) => { x = 10; y = 20; return x + y; }'); }); }); @@ -447,7 +480,7 @@ describe('TypeScriptAstFactory', () => { items: [initializer], generate, } = setupExpressions(`42`); - const varDecl = factory.createVariableDeclaration('foo', initializer, 'let'); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'let', null); expect(generate(varDecl)).toEqual('let foo = 42;'); }); @@ -456,7 +489,7 @@ describe('TypeScriptAstFactory', () => { items: [initializer], generate, } = setupExpressions(`42`); - const varDecl = factory.createVariableDeclaration('foo', initializer, 'const'); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'const', null); expect(generate(varDecl)).toEqual('const foo = 42;'); }); @@ -465,15 +498,29 @@ describe('TypeScriptAstFactory', () => { items: [initializer], generate, } = setupExpressions(`42`); - const varDecl = factory.createVariableDeclaration('foo', initializer, 'var'); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'var', null); expect(generate(varDecl)).toEqual('var foo = 42;'); }); it('should create an uninitialized variable declaration statement node for the given variable name and a null initializer', () => { const {generate} = setupStatements(); - const varDecl = factory.createVariableDeclaration('foo', null, 'let'); + const varDecl = factory.createVariableDeclaration('foo', null, 'let', null); expect(generate(varDecl)).toEqual('let foo;'); }); + + it('should create a variable declaration with a type', () => { + const { + items: [initializer], + generate, + } = setupExpressions(`42`); + const varDecl = factory.createVariableDeclaration( + 'foo', + initializer, + 'let', + factory.createBuiltInType('number'), + ); + expect(generate(varDecl)).toEqual('let foo: number = 42;'); + }); }); describe('createRegularExpressionLiteral()', () => { @@ -509,6 +556,62 @@ describe('TypeScriptAstFactory', () => { }); }); + describe('createBuiltInType()', () => { + it('should create keyword type nodes for simple types', () => { + const {generate} = setupStatements(); + expect(generate(factory.createBuiltInType('any'))).toEqual('any'); + expect(generate(factory.createBuiltInType('boolean'))).toEqual('boolean'); + expect(generate(factory.createBuiltInType('number'))).toEqual('number'); + expect(generate(factory.createBuiltInType('string'))).toEqual('string'); + expect(generate(factory.createBuiltInType('never'))).toEqual('never'); + expect(generate(factory.createBuiltInType('unknown'))).toEqual('unknown'); + }); + + it('should create a type reference for "function"', () => { + const {generate} = setupStatements(); + expect(generate(factory.createBuiltInType('function'))).toEqual('Function'); + }); + }); + + describe('createExpressionType()', () => { + it('should create a type reference for an identifier', () => { + const {items, generate} = setupExpressions('MyType'); + const type = factory.createExpressionType(items[0], null); + expect(generate(type)).toEqual('MyType'); + }); + + it('should create a type reference for a property access', () => { + const {items, generate} = setupExpressions('ns.MyType'); + const type = factory.createExpressionType(items[0], null); + expect(generate(type)).toEqual('ns.MyType'); + }); + + it('should create a type reference with type parameters', () => { + const {items, generate} = setupExpressions('MyType'); + const typeParam = factory.createBuiltInType('string'); + const type = factory.createExpressionType(items[0], [typeParam]); + expect(generate(type)).toEqual('MyType'); + }); + }); + + describe('createArrayType()', () => { + it('should create an array type node', () => { + const {generate} = setupStatements(); + const elementType = factory.createBuiltInType('string'); + const type = factory.createArrayType(elementType); + expect(generate(type)).toEqual('string[]'); + }); + }); + + describe('createMapType()', () => { + it('should create a type literal with an index signature', () => { + const {generate} = setupStatements(); + const valueType = factory.createBuiltInType('number'); + const type = factory.createMapType(valueType); + expect(generate(type)).toEqual('{\n [key: string]: number;\n}'); + }); + }); + describe('setSourceMapRange()', () => { it('should attach the `sourceMapRange` to the given `node`', () => { const {