refactor(compiler-cli): linker - add Babel plugin, FileLinker and initial PartialLinkers (#39116)
This commit adds the basic building blocks for linking partial declarations. In particular it provides a generic `FileLinker` class that delegates to a set of (not yet implemented) `PartialLinker` classes. The Babel plugin makes use of this `FileLinker` providing concrete classes for `AstHost` and `AstFactory` that work with Babel AST. It can be created with the following code: ```ts const plugin = createEs2015LinkerPlugin({ /* options */ }); ``` PR Close #39116
This commit is contained in:

committed by
Andrew Kushnir

parent
b304bd0535
commit
7e742aea7c
@ -12,14 +12,7 @@ ts_library(
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/linker",
|
||||
"@npm//@babel/core",
|
||||
"@npm//@babel/generator",
|
||||
"@npm//@babel/parser",
|
||||
"@npm//@babel/template",
|
||||
"@npm//@babel/types",
|
||||
"@npm//@types/babel__core",
|
||||
"@npm//@types/babel__generator",
|
||||
"@npm//@types/babel__template",
|
||||
"//packages/compiler-cli/src/ngtsc/translator",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
318
packages/compiler-cli/linker/test/ast/ast_value_spec.ts
Normal file
318
packages/compiler-cli/linker/test/ast/ast_value_spec.ts
Normal file
@ -0,0 +1,318 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {WrappedNodeExpr} from '@angular/compiler';
|
||||
import {TypeScriptAstFactory} from '@angular/compiler-cli/src/ngtsc/translator';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AstObject, AstValue} from '../../src/ast/ast_value';
|
||||
import {TypeScriptAstHost} from '../../src/ast/typescript/typescript_ast_host';
|
||||
|
||||
const host = new TypeScriptAstHost();
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const nestedObj = factory.createObjectLiteral([
|
||||
{propertyName: 'x', quoted: false, value: factory.createLiteral(42)},
|
||||
{propertyName: 'y', quoted: false, value: factory.createLiteral('X')},
|
||||
]);
|
||||
const nestedArray =
|
||||
factory.createArrayLiteral([factory.createLiteral(1), factory.createLiteral(2)]);
|
||||
const obj = AstObject.parse(
|
||||
factory.createObjectLiteral([
|
||||
{propertyName: 'a', quoted: false, value: factory.createLiteral(42)},
|
||||
{propertyName: 'b', quoted: false, value: factory.createLiteral('X')},
|
||||
{propertyName: 'c', quoted: false, value: factory.createLiteral(true)},
|
||||
{propertyName: 'd', quoted: false, value: nestedObj},
|
||||
{propertyName: 'e', quoted: false, value: nestedArray},
|
||||
]),
|
||||
host);
|
||||
|
||||
describe('AstObject', () => {
|
||||
describe('has()', () => {
|
||||
it('should return true if the property exists on the object', () => {
|
||||
expect(obj.has('a')).toBe(true);
|
||||
expect(obj.has('b')).toBe(true);
|
||||
expect(obj.has('z')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNumber()', () => {
|
||||
it('should return the number value of the property', () => {
|
||||
expect(obj.getNumber('a')).toEqual(42);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a number', () => {
|
||||
expect(() => obj.getNumber('b'))
|
||||
.toThrowError('Unsupported syntax, expected a numeric literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getString()', () => {
|
||||
it('should return the string value of the property', () => {
|
||||
expect(obj.getString('b')).toEqual('X');
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a string', () => {
|
||||
expect(() => obj.getString('a'))
|
||||
.toThrowError('Unsupported syntax, expected a string literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBoolean()', () => {
|
||||
it('should return the boolean value of the property', () => {
|
||||
expect(obj.getBoolean('c')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a boolean', () => {
|
||||
expect(() => obj.getBoolean('b'))
|
||||
.toThrowError('Unsupported syntax, expected a boolean literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getObject()', () => {
|
||||
it('should return an AstObject instance parsed from the value of the property', () => {
|
||||
expect(obj.getObject('d')).toEqual(AstObject.parse(nestedObj, host));
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not an object expression', () => {
|
||||
expect(() => obj.getObject('b'))
|
||||
.toThrowError('Unsupported syntax, expected an object literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArray()', () => {
|
||||
it('should return an array of AstValue instances of parsed from the value of the property',
|
||||
() => {
|
||||
expect(obj.getArray('e')).toEqual([
|
||||
new AstValue(factory.createLiteral(1), host),
|
||||
new AstValue(factory.createLiteral(2), host)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not an array of expressions', () => {
|
||||
expect(() => obj.getArray('b'))
|
||||
.toThrowError('Unsupported syntax, expected an array literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOpaque()', () => {
|
||||
it('should return the expression value of the property wrapped in a `WrappedNodeExpr`', () => {
|
||||
expect(obj.getOpaque('d')).toEqual(jasmine.any(WrappedNodeExpr));
|
||||
expect(obj.getOpaque('d').node).toEqual(obj.getNode('d'));
|
||||
});
|
||||
|
||||
it('should throw an error if the property does not exist', () => {
|
||||
expect(() => obj.getOpaque('x')).toThrowError('Expected property \'x\' to be present.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNode()', () => {
|
||||
it('should return the original expression value of the property', () => {
|
||||
expect(obj.getNode('a')).toEqual(factory.createLiteral(42));
|
||||
});
|
||||
|
||||
it('should throw an error if the property does not exist', () => {
|
||||
expect(() => obj.getNode('x')).toThrowError('Expected property \'x\' to be present.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValue()', () => {
|
||||
it('should return the expression value of the property wrapped in an `AstValue`', () => {
|
||||
expect(obj.getValue('a')).toEqual(jasmine.any(AstValue));
|
||||
expect(obj.getValue('a').getNumber()).toEqual(42);
|
||||
});
|
||||
|
||||
it('should throw an error if the property does not exist', () => {
|
||||
expect(() => obj.getValue('x')).toThrowError('Expected property \'x\' to be present.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toLiteral()', () => {
|
||||
it('should convert the AstObject to a raw object with each property mapped', () => {
|
||||
expect(obj.toLiteral(value => value.getOpaque())).toEqual({
|
||||
a: obj.getOpaque('a'),
|
||||
b: obj.getOpaque('b'),
|
||||
c: obj.getOpaque('c'),
|
||||
d: obj.getOpaque('d'),
|
||||
e: obj.getOpaque('e'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AstValue', () => {
|
||||
describe('isNumber', () => {
|
||||
it('should return true if the value is a number', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isNumber()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not a number', () => {
|
||||
expect(new AstValue(factory.createLiteral('a'), host).isNumber()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNumber', () => {
|
||||
it('should return the number value of the AstValue', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).getNumber()).toEqual(42);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a number', () => {
|
||||
expect(() => new AstValue(factory.createLiteral('a'), host).getNumber())
|
||||
.toThrowError('Unsupported syntax, expected a numeric literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isString', () => {
|
||||
it('should return true if the value is a string', () => {
|
||||
expect(new AstValue(factory.createLiteral('a'), host).isString()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not a string', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isString()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getString', () => {
|
||||
it('should return the string value of the AstValue', () => {
|
||||
expect(new AstValue(factory.createLiteral('X'), host).getString()).toEqual('X');
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a string', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getString())
|
||||
.toThrowError('Unsupported syntax, expected a string literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBoolean', () => {
|
||||
it('should return true if the value is a boolean', () => {
|
||||
expect(new AstValue(factory.createLiteral(true), host).isBoolean()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not a boolean', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isBoolean()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBoolean', () => {
|
||||
it('should return the boolean value of the AstValue', () => {
|
||||
expect(new AstValue(factory.createLiteral(true), host).getBoolean()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a boolean', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getBoolean())
|
||||
.toThrowError('Unsupported syntax, expected a boolean literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isObject', () => {
|
||||
it('should return true if the value is an object literal', () => {
|
||||
expect(new AstValue(nestedObj, host).isObject()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not an object literal', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isObject()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getObject', () => {
|
||||
it('should return the AstObject value of the AstValue', () => {
|
||||
expect(new AstValue(nestedObj, host).getObject()).toEqual(AstObject.parse(nestedObj, host));
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not an object literal', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getObject())
|
||||
.toThrowError('Unsupported syntax, expected an object literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isArray', () => {
|
||||
it('should return true if the value is an array literal', () => {
|
||||
expect(new AstValue(nestedArray, host).isArray()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not an object literal', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isArray()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArray', () => {
|
||||
it('should return an array of AstValue objects from the AstValue', () => {
|
||||
expect(new AstValue(nestedArray, host).getArray()).toEqual([
|
||||
new AstValue(factory.createLiteral(1), host),
|
||||
new AstValue(factory.createLiteral(2), host),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not an array', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getArray())
|
||||
.toThrowError('Unsupported syntax, expected an array literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFunction', () => {
|
||||
it('should return true if the value is a function expression', () => {
|
||||
const funcExpr = factory.createFunctionExpression(
|
||||
'foo', [],
|
||||
factory.createBlock([factory.createReturnStatement(factory.createLiteral(42))]));
|
||||
expect(new AstValue(funcExpr, host).isFunction()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not a function expression', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isFunction()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFunctionReturnValue', () => {
|
||||
it('should return the "return value" of the function expression', () => {
|
||||
const funcExpr = factory.createFunctionExpression(
|
||||
'foo', [],
|
||||
factory.createBlock([factory.createReturnStatement(factory.createLiteral(42))]));
|
||||
expect(new AstValue(funcExpr, host).getFunctionReturnValue())
|
||||
.toEqual(new AstValue(factory.createLiteral(42), host));
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a function expression', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getFunctionReturnValue())
|
||||
.toThrowError('Unsupported syntax, expected a function.');
|
||||
});
|
||||
|
||||
it('should throw an error if the property is a function expression with no return value',
|
||||
() => {
|
||||
const funcExpr = factory.createFunctionExpression(
|
||||
'foo', [], factory.createBlock([factory.createExpressionStatement(
|
||||
factory.createLiteral('do nothing'))]));
|
||||
expect(() => new AstValue(funcExpr, host).getFunctionReturnValue())
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOpaque()', () => {
|
||||
it('should return the value wrapped in a `WrappedNodeExpr`', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).getOpaque())
|
||||
.toEqual(jasmine.any(WrappedNodeExpr));
|
||||
expect(new AstValue(factory.createLiteral(42), host).getOpaque().node)
|
||||
.toEqual(factory.createLiteral(42));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRange()', () => {
|
||||
it('should return the source range of the AST node', () => {
|
||||
const file = ts.createSourceFile(
|
||||
'test.ts', '// preamble\nx = \'moo\';', ts.ScriptTarget.ES2015,
|
||||
/* setParentNodes */ true);
|
||||
|
||||
// Grab the `'moo'` string literal from the generated AST
|
||||
const stmt = file.statements[0] as ts.ExpressionStatement;
|
||||
const mooString =
|
||||
(stmt.expression as ts.AssignmentExpression<ts.Token<ts.SyntaxKind.EqualsToken>>).right;
|
||||
|
||||
// Check that this string literal has the expected range.
|
||||
expect(new AstValue(mooString, host).getRange())
|
||||
.toEqual({startLine: 1, startCol: 4, startPos: 16, endPos: 21});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,382 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {leadingComment} from '@angular/compiler';
|
||||
import generate from '@babel/generator';
|
||||
import {expression, statement} from '@babel/template';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import {BabelAstFactory} from '../../../src/ast/babel/babel_ast_factory';
|
||||
|
||||
describe('BabelAstFactory', () => {
|
||||
let factory: BabelAstFactory;
|
||||
beforeEach(() => factory = new BabelAstFactory());
|
||||
|
||||
describe('attachComments()', () => {
|
||||
it('should add the comments to the given statement', () => {
|
||||
const stmt = statement.ast`x = 10;`;
|
||||
factory.attachComments(
|
||||
stmt, [leadingComment('comment 1', true), leadingComment('comment 2', false)]);
|
||||
|
||||
expect(generate(stmt).code).toEqual([
|
||||
'/* comment 1 */',
|
||||
'//comment 2',
|
||||
'x = 10;',
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('createArrayLiteral()', () => {
|
||||
it('should create an array node containing the provided expressions', () => {
|
||||
const expr1 = expression.ast`42`;
|
||||
const expr2 = expression.ast`"moo"`;
|
||||
|
||||
const array = factory.createArrayLiteral([expr1, expr2]);
|
||||
expect(generate(array).code).toEqual('[42, "moo"]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAssignment()', () => {
|
||||
it('should create an assignment node using the target and value expressions', () => {
|
||||
const target = expression.ast`x`;
|
||||
const value = expression.ast`42`;
|
||||
const assignment = factory.createAssignment(target, value);
|
||||
expect(generate(assignment).code).toEqual('x = 42');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBinaryExpression()', () => {
|
||||
it('should create a binary operation node using the left and right expressions', () => {
|
||||
const left = expression.ast`17`;
|
||||
const right = expression.ast`42`;
|
||||
const expr = factory.createBinaryExpression(left, '+', right);
|
||||
expect(generate(expr).code).toEqual('17 + 42');
|
||||
});
|
||||
|
||||
it('should create a binary operation node for logical operators', () => {
|
||||
const left = expression.ast`17`;
|
||||
const right = expression.ast`42`;
|
||||
const expr = factory.createBinaryExpression(left, '&&', right);
|
||||
expect(t.isLogicalExpression(expr)).toBe(true);
|
||||
expect(generate(expr).code).toEqual('17 && 42');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBlock()', () => {
|
||||
it('should create a block statement containing the given statements', () => {
|
||||
const stmt1 = statement.ast`x = 10`;
|
||||
const stmt2 = statement.ast`y = 20`;
|
||||
const block = factory.createBlock([stmt1, stmt2]);
|
||||
expect(generate(block).code).toEqual([
|
||||
'{',
|
||||
' x = 10;',
|
||||
' y = 20;',
|
||||
'}',
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCallExpression()', () => {
|
||||
it('should create a call on the `callee` with the given `args`', () => {
|
||||
const callee = expression.ast`foo`;
|
||||
const arg1 = expression.ast`42`;
|
||||
const arg2 = expression.ast`"moo"`;
|
||||
const call = factory.createCallExpression(callee, [arg1, arg2], false);
|
||||
expect(generate(call).code).toEqual('foo(42, "moo")');
|
||||
});
|
||||
|
||||
it('should create a call marked with a PURE comment if `pure` is true', () => {
|
||||
const callee = expression.ast`foo`;
|
||||
const arg1 = expression.ast`42`;
|
||||
const arg2 = expression.ast`"moo"`;
|
||||
const call = factory.createCallExpression(callee, [arg1, arg2], true);
|
||||
expect(generate(call).code).toEqual(['/* @__PURE__ */', 'foo(42, "moo")'].join('\n'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('createConditional()', () => {
|
||||
it('should create a condition expression', () => {
|
||||
const test = expression.ast`!test`;
|
||||
const thenExpr = expression.ast`42`;
|
||||
const elseExpr = expression.ast`"moo"`;
|
||||
const conditional = factory.createConditional(test, thenExpr, elseExpr);
|
||||
expect(generate(conditional).code).toEqual('!test ? 42 : "moo"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createElementAccess()', () => {
|
||||
it('should create an expression accessing the element of an array/object', () => {
|
||||
const expr = expression.ast`obj`;
|
||||
const element = expression.ast`"moo"`;
|
||||
const access = factory.createElementAccess(expr, element);
|
||||
expect(generate(access).code).toEqual('obj["moo"]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createExpressionStatement()', () => {
|
||||
it('should create a statement node from the given expression', () => {
|
||||
const expr = expression.ast`x = 10`;
|
||||
const stmt = factory.createExpressionStatement(expr);
|
||||
expect(t.isStatement(stmt)).toBe(true);
|
||||
expect(generate(stmt).code).toEqual('x = 10;');
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
expect(generate(fn).code).toEqual([
|
||||
'function foo(arg1, arg2) {',
|
||||
' 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);
|
||||
expect(t.isStatement(fn)).toBe(false);
|
||||
expect(generate(fn).code).toEqual([
|
||||
'function foo(arg1, arg2) {',
|
||||
' 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);
|
||||
expect(generate(fn).code).toEqual([
|
||||
'function (arg1, arg2) {',
|
||||
' x = 10;',
|
||||
' y = 20;',
|
||||
'}',
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('createIdentifier()', () => {
|
||||
it('should create an identifier with the given name', () => {
|
||||
const id = factory.createIdentifier('someId') as t.Identifier;
|
||||
expect(t.isIdentifier(id)).toBe(true);
|
||||
expect(id.name).toEqual('someId');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createIfStatement()', () => {
|
||||
it('should create an if-else statement', () => {
|
||||
const test = expression.ast`!test`;
|
||||
const thenStmt = statement.ast`x = 10;`;
|
||||
const elseStmt = statement.ast`x = 42;`;
|
||||
const ifStmt = factory.createIfStatement(test, thenStmt, elseStmt);
|
||||
expect(generate(ifStmt).code).toEqual('if (!test) x = 10;else x = 42;');
|
||||
});
|
||||
|
||||
it('should create an if statement if the else expression is null', () => {
|
||||
const test = expression.ast`!test`;
|
||||
const thenStmt = statement.ast`x = 10;`;
|
||||
const ifStmt = factory.createIfStatement(test, thenStmt, null);
|
||||
expect(generate(ifStmt).code).toEqual('if (!test) x = 10;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createLiteral()', () => {
|
||||
it('should create a string literal', () => {
|
||||
const literal = factory.createLiteral('moo');
|
||||
expect(t.isStringLiteral(literal)).toBe(true);
|
||||
expect(generate(literal).code).toEqual('"moo"');
|
||||
});
|
||||
|
||||
it('should create a number literal', () => {
|
||||
const literal = factory.createLiteral(42);
|
||||
expect(t.isNumericLiteral(literal)).toBe(true);
|
||||
expect(generate(literal).code).toEqual('42');
|
||||
});
|
||||
|
||||
it('should create a number literal for `NaN`', () => {
|
||||
const literal = factory.createLiteral(NaN);
|
||||
expect(t.isNumericLiteral(literal)).toBe(true);
|
||||
expect(generate(literal).code).toEqual('NaN');
|
||||
});
|
||||
|
||||
it('should create a boolean literal', () => {
|
||||
const literal = factory.createLiteral(true);
|
||||
expect(t.isBooleanLiteral(literal)).toBe(true);
|
||||
expect(generate(literal).code).toEqual('true');
|
||||
});
|
||||
|
||||
it('should create an `undefined` literal', () => {
|
||||
const literal = factory.createLiteral(undefined);
|
||||
expect(t.isIdentifier(literal)).toBe(true);
|
||||
expect(generate(literal).code).toEqual('undefined');
|
||||
});
|
||||
|
||||
it('should create a null literal', () => {
|
||||
const literal = factory.createLiteral(null);
|
||||
expect(t.isNullLiteral(literal)).toBe(true);
|
||||
expect(generate(literal).code).toEqual('null');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNewExpression()', () => {
|
||||
it('should create a `new` operation on the constructor `expression` with the given `args`',
|
||||
() => {
|
||||
const expr = expression.ast`Foo`;
|
||||
const arg1 = expression.ast`42`;
|
||||
const arg2 = expression.ast`"moo"`;
|
||||
const call = factory.createNewExpression(expr, [arg1, arg2]);
|
||||
expect(generate(call).code).toEqual('new Foo(42, "moo")');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createObjectLiteral()', () => {
|
||||
it('should create an object literal node, with the given properties', () => {
|
||||
const prop1 = expression.ast`42`;
|
||||
const prop2 = expression.ast`"moo"`;
|
||||
const obj = factory.createObjectLiteral([
|
||||
{propertyName: 'prop1', value: prop1, quoted: false},
|
||||
{propertyName: 'prop2', value: prop2, quoted: true},
|
||||
]);
|
||||
expect(generate(obj).code).toEqual([
|
||||
'{',
|
||||
' prop1: 42,',
|
||||
' "prop2": "moo"',
|
||||
'}',
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('createParenthesizedExpression()', () => {
|
||||
it('should add parentheses around the given expression', () => {
|
||||
const expr = expression.ast`a + b`;
|
||||
const paren = factory.createParenthesizedExpression(expr);
|
||||
expect(generate(paren).code).toEqual('(a + b)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPropertyAccess()', () => {
|
||||
it('should create a property access expression node', () => {
|
||||
const expr = expression.ast`obj`;
|
||||
const access = factory.createPropertyAccess(expr, 'moo');
|
||||
expect(generate(access).code).toEqual('obj.moo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createReturnStatement()', () => {
|
||||
it('should create a return statement returning the given expression', () => {
|
||||
const expr = expression.ast`42`;
|
||||
const returnStmt = factory.createReturnStatement(expr);
|
||||
expect(generate(returnStmt).code).toEqual('return 42;');
|
||||
});
|
||||
|
||||
it('should create a void return statement if the expression is null', () => {
|
||||
const returnStmt = factory.createReturnStatement(null);
|
||||
expect(generate(returnStmt).code).toEqual('return;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTaggedTemplate()', () => {
|
||||
it('should create a tagged template node from the tag, elements and expressions', () => {
|
||||
const elements = [
|
||||
{raw: 'raw1', cooked: 'cooked1', range: null},
|
||||
{raw: 'raw2', cooked: 'cooked2', range: null},
|
||||
{raw: 'raw3', cooked: 'cooked3', range: null},
|
||||
];
|
||||
const expressions = [
|
||||
expression.ast`42`,
|
||||
expression.ast`"moo"`,
|
||||
];
|
||||
const tag = expression.ast`tagFn`;
|
||||
const template = factory.createTaggedTemplate(tag, {elements, expressions});
|
||||
expect(generate(template).code).toEqual('tagFn`raw1${42}raw2${"moo"}raw3`');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createThrowStatement()', () => {
|
||||
it('should create a throw statement, throwing the given expression', () => {
|
||||
const expr = expression.ast`new Error("bad")`;
|
||||
const throwStmt = factory.createThrowStatement(expr);
|
||||
expect(generate(throwStmt).code).toEqual('throw new Error("bad");');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTypeOfExpression()', () => {
|
||||
it('should create a typeof expression node', () => {
|
||||
const expr = expression.ast`42`;
|
||||
const typeofExpr = factory.createTypeOfExpression(expr);
|
||||
expect(generate(typeofExpr).code).toEqual('typeof 42');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createUnaryExpression()', () => {
|
||||
it('should create a unary expression with the operator and operand', () => {
|
||||
const expr = expression.ast`value`;
|
||||
const unaryExpr = factory.createUnaryExpression('!', expr);
|
||||
expect(generate(unaryExpr).code).toEqual('!value');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
expect(generate(varDecl).code).toEqual('let foo;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSourceMapRange()', () => {
|
||||
it('should attach the `sourceMapRange` to the given `node`', () => {
|
||||
const expr = expression.ast`42`;
|
||||
expect(expr.loc).toBeUndefined();
|
||||
expect(expr.start).toBeUndefined();
|
||||
expect(expr.end).toBeUndefined();
|
||||
|
||||
factory.setSourceMapRange(expr, {
|
||||
start: {line: 0, column: 1, offset: 1},
|
||||
end: {line: 2, column: 3, offset: 15},
|
||||
content: '-****\n*****\n****',
|
||||
url: 'original.ts'
|
||||
});
|
||||
|
||||
// Lines are 1-based in Babel.
|
||||
expect(expr.loc).toEqual({
|
||||
start: {line: 1, column: 1},
|
||||
end: {line: 3, column: 3},
|
||||
});
|
||||
expect(expr.start).toEqual(1);
|
||||
expect(expr.end).toEqual(15);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,305 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 t from '@babel/types';
|
||||
import template from '@babel/template';
|
||||
import {parse} from '@babel/parser';
|
||||
import {BabelAstHost} from '../../../src/ast/babel/babel_ast_host';
|
||||
|
||||
describe('BabelAstHost', () => {
|
||||
let host: BabelAstHost;
|
||||
beforeEach(() => host = new BabelAstHost());
|
||||
|
||||
describe('getSymbolName()', () => {
|
||||
it('should return the name of an identifier', () => {
|
||||
expect(host.getSymbolName(expr('someIdentifier'))).toEqual('someIdentifier');
|
||||
});
|
||||
|
||||
it('should return the name of an identifier at the end of a property access chain', () => {
|
||||
expect(host.getSymbolName(expr('a.b.c.someIdentifier'))).toEqual('someIdentifier');
|
||||
});
|
||||
|
||||
it('should return null if the expression has no identifier', () => {
|
||||
expect(host.getSymbolName(expr('42'))).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isStringLiteral()', () => {
|
||||
it('should return true if the expression is a string literal', () => {
|
||||
expect(host.isStringLiteral(expr('"moo"'))).toBe(true);
|
||||
expect(host.isStringLiteral(expr('\'moo\''))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the expression is not a string literal', () => {
|
||||
expect(host.isStringLiteral(expr('true'))).toBe(false);
|
||||
expect(host.isStringLiteral(expr('someIdentifier'))).toBe(false);
|
||||
expect(host.isStringLiteral(expr('42'))).toBe(false);
|
||||
expect(host.isStringLiteral(expr('{}'))).toBe(false);
|
||||
expect(host.isStringLiteral(expr('[]'))).toBe(false);
|
||||
expect(host.isStringLiteral(expr('null'))).toBe(false);
|
||||
expect(host.isStringLiteral(expr('\'a\' + \'b\''))).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if the expression is a template string', () => {
|
||||
expect(host.isStringLiteral(expr('\`moo\`'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseStringLiteral()', () => {
|
||||
it('should extract the string value', () => {
|
||||
expect(host.parseStringLiteral(expr('"moo"'))).toEqual('moo');
|
||||
expect(host.parseStringLiteral(expr('\'moo\''))).toEqual('moo');
|
||||
});
|
||||
|
||||
it('should error if the value is not a string literal', () => {
|
||||
expect(() => host.parseStringLiteral(expr('42')))
|
||||
.toThrowError('Unsupported syntax, expected a string literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNumericLiteral()', () => {
|
||||
it('should return true if the expression is a number literal', () => {
|
||||
expect(host.isNumericLiteral(expr('42'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the expression is not a number literal', () => {
|
||||
expect(host.isStringLiteral(expr('true'))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('"moo"'))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('\'moo\''))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('someIdentifier'))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('{}'))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('[]'))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('null'))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('\'a\' + \'b\''))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('\`moo\`'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseNumericLiteral()', () => {
|
||||
it('should extract the number value', () => {
|
||||
expect(host.parseNumericLiteral(expr('42'))).toEqual(42);
|
||||
});
|
||||
|
||||
it('should error if the value is not a numeric literal', () => {
|
||||
expect(() => host.parseNumericLiteral(expr('"moo"')))
|
||||
.toThrowError('Unsupported syntax, expected a numeric literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBooleanLiteral()', () => {
|
||||
it('should return true if the expression is a boolean literal', () => {
|
||||
expect(host.isBooleanLiteral(expr('true'))).toBe(true);
|
||||
expect(host.isBooleanLiteral(expr('false'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the expression is not a boolean literal', () => {
|
||||
expect(host.isBooleanLiteral(expr('"moo"'))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('\'moo\''))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('someIdentifier'))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('42'))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('{}'))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('[]'))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('null'))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('\'a\' + \'b\''))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('\`moo\`'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseBooleanLiteral()', () => {
|
||||
it('should extract the boolean value', () => {
|
||||
expect(host.parseBooleanLiteral(expr('true'))).toEqual(true);
|
||||
expect(host.parseBooleanLiteral(expr('false'))).toEqual(false);
|
||||
});
|
||||
|
||||
it('should error if the value is not a boolean literal', () => {
|
||||
expect(() => host.parseBooleanLiteral(expr('"moo"')))
|
||||
.toThrowError('Unsupported syntax, expected a boolean literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isArrayLiteral()', () => {
|
||||
it('should return true if the expression is an array literal', () => {
|
||||
expect(host.isArrayLiteral(expr('[]'))).toBe(true);
|
||||
expect(host.isArrayLiteral(expr('[1, 2, 3]'))).toBe(true);
|
||||
expect(host.isArrayLiteral(expr('[[], []]'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the expression is not an array literal', () => {
|
||||
expect(host.isArrayLiteral(expr('"moo"'))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('\'moo\''))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('someIdentifier'))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('42'))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('{}'))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('null'))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('\'a\' + \'b\''))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('\`moo\`'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseArrayLiteral()', () => {
|
||||
it('should extract the expressions in the array', () => {
|
||||
const moo = expr('\'moo\'');
|
||||
expect(host.parseArrayLiteral(expr('[]'))).toEqual([]);
|
||||
expect(host.parseArrayLiteral(expr('[\'moo\']'))).toEqual([moo]);
|
||||
});
|
||||
|
||||
it('should error if there is an empty item', () => {
|
||||
expect(() => host.parseArrayLiteral(expr('[,]')))
|
||||
.toThrowError('Unsupported syntax, expected element in array not to be empty.');
|
||||
});
|
||||
|
||||
it('should error if there is a spread element', () => {
|
||||
expect(() => host.parseArrayLiteral(expr('[...[0,1]]')))
|
||||
.toThrowError('Unsupported syntax, expected element in array not to use spread syntax.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isObjectLiteral()', () => {
|
||||
it('should return true if the expression is an object literal', () => {
|
||||
expect(host.isObjectLiteral(rhs('x = {}'))).toBe(true);
|
||||
expect(host.isObjectLiteral(rhs('x = { foo: \'bar\' }'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the expression is not an object literal', () => {
|
||||
expect(host.isObjectLiteral(rhs('x = "moo"'))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = \'moo\''))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = someIdentifier'))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = 42'))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = []'))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = null'))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = \'a\' + \'b\''))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = \`moo\`'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseObjectLiteral()', () => {
|
||||
it('should extract the properties from the object', () => {
|
||||
const moo = expr('\'moo\'');
|
||||
expect(host.parseObjectLiteral(rhs('x = {}'))).toEqual(new Map());
|
||||
expect(host.parseObjectLiteral(rhs('x = {a: \'moo\'}'))).toEqual(new Map([['a', moo]]));
|
||||
});
|
||||
|
||||
it('should error if there is a method', () => {
|
||||
expect(() => host.parseObjectLiteral(rhs('x = { foo() {} }')))
|
||||
.toThrowError('Unsupported syntax, expected a property assignment.');
|
||||
});
|
||||
|
||||
it('should error if there is a spread element', () => {
|
||||
expect(() => host.parseObjectLiteral(rhs('x = {...{a:\'moo\'}}')))
|
||||
.toThrowError('Unsupported syntax, expected a property assignment.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFunctionExpression()', () => {
|
||||
it('should return true if the expression is a function', () => {
|
||||
expect(host.isFunctionExpression(rhs('x = function() {}'))).toBe(true);
|
||||
expect(host.isFunctionExpression(rhs('x = function foo() {}'))).toBe(true);
|
||||
expect(host.isFunctionExpression(rhs('x = () => {}'))).toBe(true);
|
||||
expect(host.isFunctionExpression(rhs('x = () => true'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the expression is a function declaration', () => {
|
||||
expect(host.isFunctionExpression(expr('function foo() {}'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if the expression is not a function expression', () => {
|
||||
expect(host.isFunctionExpression(expr('[]'))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('"moo"'))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('\'moo\''))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('someIdentifier'))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('42'))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('{}'))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('null'))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('\'a\' + \'b\''))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('\`moo\`'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseReturnValue()', () => {
|
||||
it('should extract the return value of a function', () => {
|
||||
const moo = expr('\'moo\'');
|
||||
expect(host.parseReturnValue(rhs('x = function() { return \'moo\'; }'))).toEqual(moo);
|
||||
});
|
||||
|
||||
it('should extract the value of a simple arrow function', () => {
|
||||
const moo = expr('\'moo\'');
|
||||
expect(host.parseReturnValue(rhs('x = () => \'moo\''))).toEqual(moo);
|
||||
});
|
||||
|
||||
it('should extract the return value of an arrow function', () => {
|
||||
const moo = expr('\'moo\'');
|
||||
expect(host.parseReturnValue(rhs('x = () => { return \'moo\' }'))).toEqual(moo);
|
||||
});
|
||||
|
||||
it('should error if the body has 0 statements', () => {
|
||||
expect(() => host.parseReturnValue(rhs('x = function () { }')))
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
expect(() => host.parseReturnValue(rhs('x = () => { }')))
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
});
|
||||
|
||||
it('should error if the body has more than 1 statement', () => {
|
||||
expect(() => host.parseReturnValue(rhs('x = function () { const x = 10; return x; }')))
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
expect(() => host.parseReturnValue(rhs('x = () => { const x = 10; return x; }')))
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
});
|
||||
|
||||
it('should error if the single statement is not a return statement', () => {
|
||||
expect(() => host.parseReturnValue(rhs('x = function () { const x = 10; }')))
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
expect(() => host.parseReturnValue(rhs('x = () => { const x = 10; }')))
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRange()', () => {
|
||||
it('should extract the range from the expression', () => {
|
||||
const file = parse('// preamble\nx = \'moo\';');
|
||||
const stmt = file.program.body[0];
|
||||
assertExpressionStatement(stmt);
|
||||
assertAssignmentExpression(stmt.expression);
|
||||
expect(host.getRange(stmt.expression.right))
|
||||
.toEqual({startLine: 1, startCol: 4, startPos: 16, endPos: 21});
|
||||
});
|
||||
|
||||
it('should error if there is no range information', () => {
|
||||
const moo = rhs('// preamble\nx = \'moo\';');
|
||||
expect(() => host.getRange(moo))
|
||||
.toThrowError('Unable to read range for node - it is missing location information.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expr(code: string): t.Expression {
|
||||
const stmt = template.ast(code);
|
||||
return (stmt as t.ExpressionStatement).expression;
|
||||
}
|
||||
|
||||
function rhs(code: string): t.Expression {
|
||||
const e = expr(code);
|
||||
assertAssignmentExpression(e);
|
||||
return e.right;
|
||||
}
|
||||
|
||||
function assertExpressionStatement(e: t.Node): asserts e is t.ExpressionStatement {
|
||||
if (!t.isExpressionStatement(e)) {
|
||||
throw new Error('Bad test - expected an expression statement');
|
||||
}
|
||||
}
|
||||
|
||||
function assertAssignmentExpression(e: t.Expression): asserts e is t.AssignmentExpression {
|
||||
if (!t.isAssignmentExpression(e)) {
|
||||
throw new Error('Bad test - expected an assignment expression');
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 o from '@angular/compiler/src/output/output_ast';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {TypeScriptAstFactory} from '../../../../src/ngtsc/translator';
|
||||
import {TypeScriptAstHost} from '../../../src/ast/typescript/typescript_ast_host';
|
||||
import {EmitScope} from '../../../src/file_linker/emit_scopes/emit_scope';
|
||||
import {LinkerEnvironment} from '../../../src/file_linker/linker_environment';
|
||||
import {DEFAULT_LINKER_OPTIONS} from '../../../src/file_linker/linker_options';
|
||||
import {generate} from '../helpers';
|
||||
|
||||
describe('EmitScope', () => {
|
||||
describe('translateDefinition()', () => {
|
||||
it('should translate the given output AST into a TExpression', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const def = emitScope.translateDefinition(o.fn([], [], null, null, 'foo'));
|
||||
expect(generate(def)).toEqual('function foo() { }');
|
||||
});
|
||||
|
||||
it('should use the `ngImport` idenfifier for imports when translating', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const coreImportRef = new o.ExternalReference('@angular/core', 'foo');
|
||||
const def = emitScope.translateDefinition(o.importExpr(coreImportRef).callMethod('bar', []));
|
||||
expect(generate(def)).toEqual('core.foo.bar()');
|
||||
});
|
||||
|
||||
it('should not emit any shared constants in the replacement expression', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const constArray = o.literalArr([o.literal('CONST')]);
|
||||
// We have to add the constant twice or it will not create a shared statement
|
||||
emitScope.constantPool.getConstLiteral(constArray);
|
||||
emitScope.constantPool.getConstLiteral(constArray);
|
||||
|
||||
const def = emitScope.translateDefinition(o.fn([], [], null, null, 'foo'));
|
||||
expect(generate(def)).toEqual('function foo() { }');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConstantStatements()', () => {
|
||||
it('should return any constant statements that were added to the `constantPool`', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const constArray = o.literalArr([o.literal('CONST')]);
|
||||
// We have to add the constant twice or it will not create a shared statement
|
||||
emitScope.constantPool.getConstLiteral(constArray);
|
||||
emitScope.constantPool.getConstLiteral(constArray);
|
||||
|
||||
const statements = emitScope.getConstantStatements();
|
||||
expect(statements.map(generate)).toEqual(['const _c0 = ["CONST"];']);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 o from '@angular/compiler/src/output/output_ast';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {TypeScriptAstFactory} from '../../../../src/ngtsc/translator';
|
||||
import {TypeScriptAstHost} from '../../../src/ast/typescript/typescript_ast_host';
|
||||
import {IifeEmitScope} from '../../../src/file_linker/emit_scopes/iife_emit_scope';
|
||||
import {LinkerEnvironment} from '../../../src/file_linker/linker_environment';
|
||||
import {DEFAULT_LINKER_OPTIONS} from '../../../src/file_linker/linker_options';
|
||||
import {generate} from '../helpers';
|
||||
|
||||
describe('IifeEmitScope', () => {
|
||||
describe('translateDefinition()', () => {
|
||||
it('should translate the given output AST into a TExpression, wrapped in an IIFE', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const def = emitScope.translateDefinition(o.fn([], [], null, null, 'foo'));
|
||||
expect(generate(def)).toEqual('function () { return function foo() { }; }()');
|
||||
});
|
||||
|
||||
it('should use the `ngImport` idenfifier for imports when translating', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const coreImportRef = new o.ExternalReference('@angular/core', 'foo');
|
||||
const def = emitScope.translateDefinition(o.importExpr(coreImportRef).callMethod('bar', []));
|
||||
expect(generate(def)).toEqual('function () { return core.foo.bar(); }()');
|
||||
});
|
||||
|
||||
it('should emit any shared constants in the replacement expression IIFE', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const constArray = o.literalArr([o.literal('CONST')]);
|
||||
// We have to add the constant twice or it will not create a shared statement
|
||||
emitScope.constantPool.getConstLiteral(constArray);
|
||||
emitScope.constantPool.getConstLiteral(constArray);
|
||||
|
||||
const def = emitScope.translateDefinition(o.fn([], [], null, null, 'foo'));
|
||||
expect(generate(def))
|
||||
.toEqual('function () { const _c0 = ["CONST"]; return function foo() { }; }()');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConstantStatements()', () => {
|
||||
it('should throw an error', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
expect(() => emitScope.getConstantStatements()).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,191 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 o from '@angular/compiler/src/output/output_ast';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {TypeScriptAstFactory} from '../../../src/ngtsc/translator';
|
||||
import {AstHost} from '../../src/ast/ast_host';
|
||||
import {TypeScriptAstHost} from '../../src/ast/typescript/typescript_ast_host';
|
||||
import {DeclarationScope} from '../../src/file_linker/declaration_scope';
|
||||
import {FileLinker} from '../../src/file_linker/file_linker';
|
||||
import {LinkerEnvironment} from '../../src/file_linker/linker_environment';
|
||||
import {DEFAULT_LINKER_OPTIONS} from '../../src/file_linker/linker_options';
|
||||
import {PartialDirectiveLinkerVersion1} from '../../src/file_linker/partial_linkers/partial_directive_linker_1';
|
||||
import {generate} from './helpers';
|
||||
|
||||
describe('FileLinker', () => {
|
||||
let factory: TypeScriptAstFactory;
|
||||
beforeEach(() => factory = new TypeScriptAstFactory());
|
||||
|
||||
describe('isPartialDeclaration()', () => {
|
||||
it('should return true if the callee is recognized', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
expect(fileLinker.isPartialDeclaration('$ngDeclareDirective')).toBe(true);
|
||||
expect(fileLinker.isPartialDeclaration('$ngDeclareComponent')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the callee is not recognized', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
expect(fileLinker.isPartialDeclaration('$foo')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('linkPartialDeclaration()', () => {
|
||||
it('should throw an error if the function name is not recognised', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
const version = factory.createLiteral(1);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const declarationArg = factory.createObjectLiteral([
|
||||
{propertyName: 'version', quoted: false, value: version},
|
||||
{propertyName: 'ngImport', quoted: false, value: ngImport},
|
||||
]);
|
||||
expect(
|
||||
() => fileLinker.linkPartialDeclaration(
|
||||
'foo', [declarationArg], new MockDeclarationScope()))
|
||||
.toThrowError('Unknown partial declaration function foo.');
|
||||
});
|
||||
|
||||
it('should throw an error if the metadata object does not have a `version` property', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const declarationArg = factory.createObjectLiteral([
|
||||
{propertyName: 'ngImport', quoted: false, value: ngImport},
|
||||
]);
|
||||
expect(
|
||||
() => fileLinker.linkPartialDeclaration(
|
||||
'$ngDeclareDirective', [declarationArg], new MockDeclarationScope()))
|
||||
.toThrowError(`Expected property 'version' to be present.`);
|
||||
});
|
||||
|
||||
it('should throw an error if the metadata object does not have a `ngImport` property', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const declarationArg = factory.createObjectLiteral([
|
||||
{propertyName: 'version', quoted: false, value: ngImport},
|
||||
]);
|
||||
expect(
|
||||
() => fileLinker.linkPartialDeclaration(
|
||||
'$ngDeclareDirective', [declarationArg], new MockDeclarationScope()))
|
||||
.toThrowError(`Expected property 'ngImport' to be present.`);
|
||||
});
|
||||
|
||||
it('should call `linkPartialDeclaration()` on the appropriate partial compiler', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
const compileSpy = spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration')
|
||||
.and.returnValue(o.literal('compilation result'));
|
||||
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const version = factory.createLiteral(1);
|
||||
const declarationArg = factory.createObjectLiteral([
|
||||
{propertyName: 'ngImport', quoted: false, value: ngImport},
|
||||
{propertyName: 'version', quoted: false, value: version},
|
||||
]);
|
||||
|
||||
const compilationResult = fileLinker.linkPartialDeclaration(
|
||||
'$ngDeclareDirective', [declarationArg], new MockDeclarationScope());
|
||||
|
||||
expect(compilationResult).toEqual(factory.createLiteral('compilation result'));
|
||||
expect(compileSpy).toHaveBeenCalled();
|
||||
expect(compileSpy.calls.mostRecent().args[3].getNode('ngImport')).toBe(ngImport);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConstantStatements()', () => {
|
||||
it('should capture shared constant values', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
|
||||
|
||||
// Here we use the `core` idenfifier for `ngImport` to trigger the use of a shared scope for
|
||||
// constant statements.
|
||||
const declarationArg = factory.createObjectLiteral([
|
||||
{propertyName: 'ngImport', quoted: false, value: factory.createIdentifier('core')},
|
||||
{propertyName: 'version', quoted: false, value: factory.createLiteral(1)},
|
||||
]);
|
||||
|
||||
const replacement = fileLinker.linkPartialDeclaration(
|
||||
'$ngDeclareDirective', [declarationArg], new MockDeclarationScope());
|
||||
expect(generate(replacement)).toEqual('"REPLACEMENT"');
|
||||
|
||||
const results = fileLinker.getConstantStatements();
|
||||
expect(results.length).toEqual(1);
|
||||
const {constantScope, statements} = results[0];
|
||||
expect(constantScope).toBe(MockConstantScopeRef.singleton);
|
||||
expect(statements.map(generate)).toEqual(['const _c0 = [1];']);
|
||||
});
|
||||
|
||||
it('should be no shared constant statements to capture when they are emitted into the replacement IIFE',
|
||||
() => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
|
||||
|
||||
// Here we use a string literal `"not-a-module"` for `ngImport` to cause constant
|
||||
// statements to be emitted in an IIFE rather than added to the shared constant scope.
|
||||
const declarationArg = factory.createObjectLiteral([
|
||||
{propertyName: 'ngImport', quoted: false, value: factory.createLiteral('not-a-module')},
|
||||
{propertyName: 'version', quoted: false, value: factory.createLiteral(1)},
|
||||
]);
|
||||
|
||||
const replacement = fileLinker.linkPartialDeclaration(
|
||||
'$ngDeclareDirective', [declarationArg], new MockDeclarationScope());
|
||||
expect(generate(replacement))
|
||||
.toEqual('function () { const _c0 = [1]; return "REPLACEMENT"; }()');
|
||||
|
||||
const results = fileLinker.getConstantStatements();
|
||||
expect(results.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
function createFileLinker(): {
|
||||
host: AstHost<ts.Expression>,
|
||||
fileLinker: FileLinker<MockConstantScopeRef, ts.Statement, ts.Expression>
|
||||
} {
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), new TypeScriptAstFactory(), DEFAULT_LINKER_OPTIONS);
|
||||
const fileLinker = new FileLinker<MockConstantScopeRef, ts.Statement, ts.Expression>(
|
||||
linkerEnvironment, 'test.js', '// test code');
|
||||
return {host: linkerEnvironment.host, fileLinker};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* This mock implementation of `DeclarationScope` will return a singleton instance of
|
||||
* `MockConstantScopeRef` if the expression is an identifier, or `null` otherwise.
|
||||
*
|
||||
* This way we can simulate whether the constants will be shared or inlined into an IIFE.
|
||||
*/
|
||||
class MockDeclarationScope implements DeclarationScope<MockConstantScopeRef, ts.Expression> {
|
||||
getConstantScopeRef(expression: ts.Expression): MockConstantScopeRef|null {
|
||||
if (ts.isIdentifier(expression)) {
|
||||
return MockConstantScopeRef.singleton;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MockConstantScopeRef {
|
||||
private constructor() {}
|
||||
static singleton = new MockDeclarationScope();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spy on the `PartialDirectiveLinkerVersion1.linkPartialDeclaration()` method, triggering
|
||||
* shared constants to be created.
|
||||
*/
|
||||
function spyOnLinkPartialDeclarationWithConstants(replacement: o.Expression) {
|
||||
let callCount = 0;
|
||||
spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration')
|
||||
.and.callFake(((sourceUrl, code, constantPool) => {
|
||||
const constArray = o.literalArr([o.literal(++callCount)]);
|
||||
// We have to add the constant twice or it will not create a shared statement
|
||||
constantPool.getConstLiteral(constArray);
|
||||
constantPool.getConstLiteral(constArray);
|
||||
return replacement;
|
||||
}) as typeof PartialDirectiveLinkerVersion1.prototype.linkPartialDeclaration);
|
||||
}
|
17
packages/compiler-cli/linker/test/file_linker/helpers.ts
Normal file
17
packages/compiler-cli/linker/test/file_linker/helpers.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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';
|
||||
|
||||
/**
|
||||
* A simple helper to render a TS Node as a string.
|
||||
*/
|
||||
export function generate(node: ts.Node): string {
|
||||
const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
|
||||
const sf = ts.createSourceFile('test.ts', '', ts.ScriptTarget.ES2015, true);
|
||||
return printer.printNode(ts.EmitHint.Unspecified, node, sf);
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {PartialComponentLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_component_linker_1';
|
||||
import {PartialDirectiveLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_directive_linker_1';
|
||||
import {PartialLinkerSelector} from '../../../src/file_linker/partial_linkers/partial_linker_selector';
|
||||
|
||||
describe('PartialLinkerSelector', () => {
|
||||
describe('supportsDeclaration()', () => {
|
||||
it('should return true if there is at least one linker that matches the given function name',
|
||||
() => {
|
||||
const selector = new PartialLinkerSelector();
|
||||
expect(selector.supportsDeclaration('$ngDeclareDirective')).toBe(true);
|
||||
expect(selector.supportsDeclaration('$ngDeclareComponent')).toBe(true);
|
||||
expect(selector.supportsDeclaration('$foo')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLinker()', () => {
|
||||
it('should return the linker that matches the name and version number', () => {
|
||||
const selector = new PartialLinkerSelector();
|
||||
expect(selector.getLinker('$ngDeclareDirective', 1))
|
||||
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
|
||||
expect(selector.getLinker('$ngDeclareComponent', 1))
|
||||
.toBeInstanceOf(PartialComponentLinkerVersion1);
|
||||
});
|
||||
|
||||
it('should throw an error if there is no linker that matches the given name or version', () => {
|
||||
const selector = new PartialLinkerSelector();
|
||||
expect(() => selector.getLinker('$foo', 1))
|
||||
.toThrowError('Unknown partial declaration function $foo.');
|
||||
expect(() => selector.getLinker('$ngDeclareDirective', 2))
|
||||
.toThrowError('Unsupported partial declaration version 2 for $ngDeclareDirective.');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 o from '@angular/compiler';
|
||||
import {ImportGenerator, NamedImport, TypeScriptAstFactory} from '@angular/compiler-cli/src/ngtsc/translator';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Translator} from '../../src/file_linker/translator';
|
||||
import {generate} from './helpers';
|
||||
|
||||
describe('Translator', () => {
|
||||
let factory: TypeScriptAstFactory;
|
||||
beforeEach(() => factory = new TypeScriptAstFactory());
|
||||
|
||||
describe('translateExpression()', () => {
|
||||
it('should generate expression specific output', () => {
|
||||
const translator = new Translator<ts.Statement, ts.Expression>(factory);
|
||||
const outputAst = new o.WriteVarExpr('foo', new o.LiteralExpr(42));
|
||||
const translated = translator.translateExpression(outputAst, new MockImportGenerator());
|
||||
expect(generate(translated)).toEqual('(foo = 42)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('translateStatement()', () => {
|
||||
it('should generate statement specific output', () => {
|
||||
const translator = new Translator<ts.Statement, ts.Expression>(factory);
|
||||
const outputAst = new o.ExpressionStatement(new o.WriteVarExpr('foo', new o.LiteralExpr(42)));
|
||||
const translated = translator.translateStatement(outputAst, new MockImportGenerator());
|
||||
expect(generate(translated)).toEqual('foo = 42;');
|
||||
});
|
||||
});
|
||||
class MockImportGenerator implements ImportGenerator<ts.Expression> {
|
||||
generateNamespaceImport(moduleName: string): ts.Expression {
|
||||
return factory.createLiteral(moduleName);
|
||||
}
|
||||
generateNamedImport(moduleName: string, originalSymbol: string): NamedImport<ts.Expression> {
|
||||
return {
|
||||
moduleImport: factory.createLiteral(moduleName),
|
||||
symbol: originalSymbol,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
@ -8,7 +8,7 @@
|
||||
import {LinkerImportGenerator} from '../src/linker_import_generator';
|
||||
|
||||
const ngImport = {
|
||||
type: 'ngImport'
|
||||
ngImport: true
|
||||
};
|
||||
|
||||
describe('LinkerImportGenerator<TExpression>', () => {
|
||||
|
Reference in New Issue
Block a user