Skip to content
Merged
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
110 changes: 97 additions & 13 deletions packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import {assert} from '../../../../linker';
import {
AstFactory,
BinaryOperator,
BuiltInType,
LeadingComment,
ObjectLiteralProperty,
Parameter,
SourceMapRange,
TemplateLiteral,
VariableDeclarationType,
Expand All @@ -21,11 +23,19 @@ import {
/**
* A Babel flavored implementation of the AstFactory.
*/
export class BabelAstFactory implements AstFactory<t.Statement, t.Expression | t.SpreadElement> {
export class BabelAstFactory implements AstFactory<
t.Statement,
t.Expression | t.SpreadElement,
t.TSType
> {
private readonly typesEnabled: boolean;

constructor(
/** The absolute path to the source file being compiled. */
private sourceUrl: string,
) {}
private sourcePath: string,
) {
this.typesEnabled = sourcePath.endsWith('.ts') || sourcePath.endsWith('.mts');
}

attachComments(statement: t.Statement | t.Expression, leadingComments: LeadingComment[]): void {
// We must process the comments in reverse because `t.addComment()` will add new ones in front.
Expand Down Expand Up @@ -100,40 +110,40 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression | t

createFunctionDeclaration(
functionName: string,
parameters: string[],
parameters: Parameter<t.TSType>[],
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) => this.identifierWithType(param.name, param.type)),
body,
);
}

createArrowFunctionExpression(
parameters: string[],
parameters: Parameter<t.TSType>[],
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) => this.identifierWithType(param.name, param.type)),
body,
);
}

createFunctionExpression(
functionName: string | null,
parameters: string[],
parameters: Parameter<t.TSType>[],
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) => this.identifierWithType(param.name, param.type)),
body,
);
}
Expand Down Expand Up @@ -224,10 +234,11 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression | t
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(this.identifierWithType(variableName, type), initializer),
]);
}

Expand All @@ -246,7 +257,7 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression | t
// Add in the filename so that we can map to external template files.
// Note that Babel gets confused if you specify a filename when it is the original source
// file. This happens when the template is inline, in which case just use `undefined`.
filename: sourceMapRange.url !== this.sourceUrl ? sourceMapRange.url : undefined,
filename: sourceMapRange.url !== this.sourcePath ? sourceMapRange.url : undefined,
start: {
line: sourceMapRange.start.line + 1, // lines are 1-based in Babel.
column: sourceMapRange.start.column,
Expand All @@ -261,6 +272,79 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression | t

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 = this.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);
}

private identifierWithType(name: string, type: t.TSType | null): t.Identifier {
const node = t.identifier(name);

if (this.typesEnabled && type != null) {
node.typeAnnotation = t.tsTypeAnnotation(type);
}

return node;
}
}

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 isLExpression(expr: t.Expression): expr is Extract<t.LVal, t.Expression> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export function createEs2015LinkerPlugin({
let fileLinker: FileLinker<
ConstantScopePath,
t.Statement,
t.Expression | t.SpreadElement
t.Expression | t.SpreadElement,
t.TSType
> | null = null;

return {
Expand All @@ -53,7 +54,8 @@ export function createEs2015LinkerPlugin({

const linkerEnvironment = LinkerEnvironment.create<
t.Statement,
t.Expression | t.SpreadElement
t.Expression | t.SpreadElement,
t.TSType
>(fileSystem, logger, new BabelAstHost(), new BabelAstFactory(sourceUrl), options);
fileLinker = new FileLinker(linkerEnvironment, sourceUrl, file.code);
},
Expand Down
121 changes: 107 additions & 14 deletions packages/compiler-cli/linker/babel/test/ast/babel_ast_factory_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,37 +149,64 @@ 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'),
);
});
});

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'),
);
});
});

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', () => {
Expand All @@ -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'),
);
});
});
Expand Down Expand Up @@ -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()', () => {
Expand Down Expand Up @@ -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<string>');
});
});

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`;
Expand Down
Loading
Loading