refactor(compiler-cli): implement `BabelAstFactory` and `AstHost`s (#38866)
This commit adds the `AstHost` interface, along with implementations for both Babel and TS. It also implements the Babel vesion of the `AstFactory` interface, along with a linker specific implementation of the `ImportGenerator` interface. These classes will be used by the new "ng-linker" to transform prelinked library code using a Babel plugin. PR Close #38866
This commit is contained in:
parent
1f5d7dc394
commit
7dd0db6d4f
|
@ -47,6 +47,7 @@ merge:
|
||||||
- "packages/bazel/src/ng_package/**"
|
- "packages/bazel/src/ng_package/**"
|
||||||
- "packages/bazel/src/protractor/**"
|
- "packages/bazel/src/protractor/**"
|
||||||
- "packages/bazel/src/schematics/**"
|
- "packages/bazel/src/schematics/**"
|
||||||
|
- "packages/compiler-cli/linker/**"
|
||||||
- "packages/compiler-cli/ngcc/**"
|
- "packages/compiler-cli/ngcc/**"
|
||||||
- "packages/compiler-cli/src/ngtsc/sourcemaps/**"
|
- "packages/compiler-cli/src/ngtsc/sourcemaps/**"
|
||||||
- "packages/docs/**"
|
- "packages/docs/**"
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
load("//tools:defaults.bzl", "ts_library")
|
||||||
|
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "linker",
|
||||||
|
srcs = ["index.ts"] + glob([
|
||||||
|
"src/**/*.ts",
|
||||||
|
]),
|
||||||
|
deps = [
|
||||||
|
"//packages/compiler-cli/src/ngtsc/translator",
|
||||||
|
"@npm//@babel/core",
|
||||||
|
"@npm//@babel/types",
|
||||||
|
"@npm//@types/babel__core",
|
||||||
|
"@npm//@types/babel__traverse",
|
||||||
|
"@npm//typescript",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
|
@ -0,0 +1,95 @@
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstraction for getting information from an AST while being agnostic to the underlying AST
|
||||||
|
* implementation.
|
||||||
|
*/
|
||||||
|
export interface AstHost<TExpression> {
|
||||||
|
/**
|
||||||
|
* Get the name of the symbol represented by the given expression node, or `null` if it is not a
|
||||||
|
* symbol.
|
||||||
|
*/
|
||||||
|
getSymbolName(node: TExpression): string|null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return `true` if the given expression is a string literal, or false otherwise.
|
||||||
|
*/
|
||||||
|
isStringLiteral(node: TExpression): boolean;
|
||||||
|
/**
|
||||||
|
* Parse the string value from the given expression, or throw if it is not a string literal.
|
||||||
|
*/
|
||||||
|
parseStringLiteral(str: TExpression): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return `true` if the given expression is a numeric literal, or false otherwise.
|
||||||
|
*/
|
||||||
|
isNumericLiteral(node: TExpression): boolean;
|
||||||
|
/**
|
||||||
|
* Parse the numeric value from the given expression, or throw if it is not a numeric literal.
|
||||||
|
*/
|
||||||
|
parseNumericLiteral(num: TExpression): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return `true` if the given expression is a boolean literal, or false otherwise.
|
||||||
|
*/
|
||||||
|
isBooleanLiteral(node: TExpression): boolean;
|
||||||
|
/**
|
||||||
|
* Parse the boolean value from the given expression, or throw if it is not a boolean literal.
|
||||||
|
*/
|
||||||
|
parseBooleanLiteral(bool: TExpression): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return `true` if the given expression is an array literal, or false otherwise.
|
||||||
|
*/
|
||||||
|
isArrayLiteral(node: TExpression): boolean;
|
||||||
|
/**
|
||||||
|
* Parse an array of expressions from the given expression, or throw if it is not an array
|
||||||
|
* literal.
|
||||||
|
*/
|
||||||
|
parseArrayLiteral(array: TExpression): TExpression[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return `true` if the given expression is an object literal, or false otherwise.
|
||||||
|
*/
|
||||||
|
isObjectLiteral(node: TExpression): boolean;
|
||||||
|
/**
|
||||||
|
* Parse the given expression into a map of object property names to property expressions, or
|
||||||
|
* throw if it is not an object literal.
|
||||||
|
*/
|
||||||
|
parseObjectLiteral(obj: TExpression): Map<string, TExpression>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return `true` if the given expression is a function, or false otherwise.
|
||||||
|
*/
|
||||||
|
isFunctionExpression(node: TExpression): boolean;
|
||||||
|
/**
|
||||||
|
* Compute the "value" of a function expression by parsing its body for a single `return`
|
||||||
|
* statement, extracting the returned expression, or throw if it is not possible.
|
||||||
|
*/
|
||||||
|
parseReturnValue(fn: TExpression): TExpression;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the location range of the expression in the source file, to be used for source-mapping.
|
||||||
|
*/
|
||||||
|
getRange(node: TExpression): Range;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The location of the start and end of an expression in the original source file.
|
||||||
|
*/
|
||||||
|
export interface Range {
|
||||||
|
/** 0-based character position of the range start in the source file text. */
|
||||||
|
startPos: number;
|
||||||
|
/** 0-based line index of the range start in the source file text. */
|
||||||
|
startLine: number;
|
||||||
|
/** 0-based column position of the range start in the source file text. */
|
||||||
|
startCol: number;
|
||||||
|
/** 0-based character position of the range end in the source file text. */
|
||||||
|
endPos: number;
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
/**
|
||||||
|
* @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 {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapRange, TemplateLiteral, VariableDeclarationType} from '../../../../src/ngtsc/translator';
|
||||||
|
import {assert} from '../utils';
|
||||||
|
|
||||||
|
export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {
|
||||||
|
attachComments(statement: t.Statement, leadingComments: LeadingComment[]|undefined): t.Statement {
|
||||||
|
if (leadingComments === undefined) {
|
||||||
|
return statement;
|
||||||
|
}
|
||||||
|
// We must process the comments in reverse because `t.addComment()` will add new ones in front.
|
||||||
|
for (let i = leadingComments.length - 1; i >= 0; i--) {
|
||||||
|
const comment = leadingComments[i];
|
||||||
|
t.addComment(statement, 'leading', comment.toString(), !comment.multiline);
|
||||||
|
}
|
||||||
|
return statement;
|
||||||
|
}
|
||||||
|
|
||||||
|
createArrayLiteral = t.arrayExpression;
|
||||||
|
|
||||||
|
createAssignment(target: t.Expression, value: t.Expression): t.Expression {
|
||||||
|
assert(target, isLExpression, 'must be a left hand side expression');
|
||||||
|
return t.assignmentExpression('=', target, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
createBinaryExpression(
|
||||||
|
leftOperand: t.Expression, operator: BinaryOperator,
|
||||||
|
rightOperand: t.Expression): t.Expression {
|
||||||
|
switch (operator) {
|
||||||
|
case '&&':
|
||||||
|
case '||':
|
||||||
|
return t.logicalExpression(operator, leftOperand, rightOperand);
|
||||||
|
default:
|
||||||
|
return t.binaryExpression(operator, leftOperand, rightOperand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createBlock = t.blockStatement;
|
||||||
|
|
||||||
|
createCallExpression(callee: t.Expression, args: t.Expression[], pure: boolean): t.Expression {
|
||||||
|
const call = t.callExpression(callee, args);
|
||||||
|
if (pure) {
|
||||||
|
t.addComment(call, 'leading', ' @__PURE__ ', /* line */ false);
|
||||||
|
}
|
||||||
|
return call;
|
||||||
|
}
|
||||||
|
|
||||||
|
createConditional = t.conditionalExpression;
|
||||||
|
|
||||||
|
createElementAccess(expression: t.Expression, element: t.Expression): t.Expression {
|
||||||
|
return t.memberExpression(expression, element, /* computed */ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
createExpressionStatement = t.expressionStatement;
|
||||||
|
|
||||||
|
createFunctionDeclaration(functionName: string, parameters: string[], body: t.Statement):
|
||||||
|
t.Statement {
|
||||||
|
assert(body, t.isBlockStatement, 'a block');
|
||||||
|
return t.functionDeclaration(
|
||||||
|
t.identifier(functionName), parameters.map(param => t.identifier(param)), body);
|
||||||
|
}
|
||||||
|
|
||||||
|
createFunctionExpression(functionName: string|null, parameters: string[], 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)), body);
|
||||||
|
}
|
||||||
|
|
||||||
|
createIdentifier = t.identifier;
|
||||||
|
|
||||||
|
createIfStatement = t.ifStatement;
|
||||||
|
|
||||||
|
createLiteral(value: string|number|boolean|null|undefined): t.Expression {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return t.stringLiteral(value);
|
||||||
|
} else if (typeof value === 'number') {
|
||||||
|
return t.numericLiteral(value);
|
||||||
|
} else if (typeof value === 'boolean') {
|
||||||
|
return t.booleanLiteral(value);
|
||||||
|
} else if (value === undefined) {
|
||||||
|
return t.identifier('undefined');
|
||||||
|
} else if (value === null) {
|
||||||
|
return t.nullLiteral();
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid literal: ${value} (${typeof value})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createNewExpression = t.newExpression;
|
||||||
|
|
||||||
|
createObjectLiteral(properties: ObjectLiteralProperty<t.Expression>[]): t.Expression {
|
||||||
|
return t.objectExpression(properties.map(prop => {
|
||||||
|
const key =
|
||||||
|
prop.quoted ? t.stringLiteral(prop.propertyName) : t.identifier(prop.propertyName);
|
||||||
|
return t.objectProperty(key, prop.value);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
createParenthesizedExpression = t.parenthesizedExpression;
|
||||||
|
|
||||||
|
createPropertyAccess(expression: t.Expression, propertyName: string): t.Expression {
|
||||||
|
return t.memberExpression(expression, t.identifier(propertyName), /* computed */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
createReturnStatement = t.returnStatement;
|
||||||
|
|
||||||
|
createTaggedTemplate(tag: t.Expression, template: TemplateLiteral<t.Expression>): t.Expression {
|
||||||
|
const elements = template.elements.map(
|
||||||
|
(element, i) => this.setSourceMapRange(
|
||||||
|
t.templateElement(element, i === template.elements.length - 1), element.range));
|
||||||
|
return t.taggedTemplateExpression(tag, t.templateLiteral(elements, template.expressions));
|
||||||
|
}
|
||||||
|
|
||||||
|
createThrowStatement = t.throwStatement;
|
||||||
|
|
||||||
|
createTypeOfExpression(expression: t.Expression): t.Expression {
|
||||||
|
return t.unaryExpression('typeof', expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
createUnaryExpression = t.unaryExpression;
|
||||||
|
|
||||||
|
createVariableDeclaration(
|
||||||
|
variableName: string, initializer: t.Expression|null,
|
||||||
|
type: VariableDeclarationType): t.Statement {
|
||||||
|
return t.variableDeclaration(
|
||||||
|
type, [t.variableDeclarator(t.identifier(variableName), initializer)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSourceMapRange<T extends t.Statement|t.Expression|t.TemplateElement>(
|
||||||
|
node: T, sourceMapRange: SourceMapRange|null): T {
|
||||||
|
if (sourceMapRange === null) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
// Note that the linker only works on a single file at a time, so there is no need to track the
|
||||||
|
// filename. Babel will just use the current filename in the source-map.
|
||||||
|
node.loc = {
|
||||||
|
start: {
|
||||||
|
line: sourceMapRange.start.line + 1, // lines are 1-based in Babel.
|
||||||
|
column: sourceMapRange.start.column,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
line: sourceMapRange.end.line + 1, // lines are 1-based in Babel.
|
||||||
|
column: sourceMapRange.end.column,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
node.start = sourceMapRange.start.offset;
|
||||||
|
node.end = sourceMapRange.end.offset;
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLExpression(expr: t.Expression): expr is Extract<t.LVal, t.Expression> {
|
||||||
|
// Some LVal types are not expressions, which prevents us from using `t.isLVal()`
|
||||||
|
// directly with `assert()`.
|
||||||
|
return t.isLVal(expr);
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
/**
|
||||||
|
* @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 {FatalLinkerError} from '../../fatal_linker_error';
|
||||||
|
import {AstHost, Range} from '../ast_host';
|
||||||
|
import {assert} from '../utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation of `AstHost` is able to get information from Babel AST nodes.
|
||||||
|
*/
|
||||||
|
export class BabelAstHost implements AstHost<t.Expression> {
|
||||||
|
getSymbolName(node: t.Expression): string|null {
|
||||||
|
if (t.isIdentifier(node)) {
|
||||||
|
return node.name;
|
||||||
|
} else if (t.isMemberExpression(node) && t.isIdentifier(node.property)) {
|
||||||
|
return node.property.name;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isStringLiteral = t.isStringLiteral;
|
||||||
|
|
||||||
|
parseStringLiteral(str: t.Expression): string {
|
||||||
|
assert(str, t.isStringLiteral, 'a string literal');
|
||||||
|
return str.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNumericLiteral = t.isNumericLiteral;
|
||||||
|
|
||||||
|
parseNumericLiteral(num: t.Expression): number {
|
||||||
|
assert(num, t.isNumericLiteral, 'a numeric literal');
|
||||||
|
return num.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
isBooleanLiteral = t.isBooleanLiteral;
|
||||||
|
|
||||||
|
parseBooleanLiteral(bool: t.Expression): boolean {
|
||||||
|
assert(bool, t.isBooleanLiteral, 'a boolean literal');
|
||||||
|
return bool.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
isArrayLiteral = t.isArrayExpression;
|
||||||
|
|
||||||
|
parseArrayLiteral(array: t.Expression): t.Expression[] {
|
||||||
|
assert(array, t.isArrayExpression, 'an array literal');
|
||||||
|
return array.elements.map(element => {
|
||||||
|
assert(element, isNotEmptyElement, 'element in array not to be empty');
|
||||||
|
assert(element, isNotSpreadElement, 'element in array not to use spread syntax');
|
||||||
|
return element;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isObjectLiteral = t.isObjectExpression;
|
||||||
|
|
||||||
|
parseObjectLiteral(obj: t.Expression): Map<string, t.Expression> {
|
||||||
|
assert(obj, t.isObjectExpression, 'an object literal');
|
||||||
|
|
||||||
|
const result = new Map<string, t.Expression>();
|
||||||
|
for (const property of obj.properties) {
|
||||||
|
assert(property, t.isObjectProperty, 'a property assignment');
|
||||||
|
assert(property.value, t.isExpression, 'an expression');
|
||||||
|
assert(property.key, isPropertyName, 'a property name');
|
||||||
|
const key = t.isIdentifier(property.key) ? property.key.name : property.key.value;
|
||||||
|
result.set(key, property.value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFunctionExpression(node: t.Expression): node is Extract<t.Function, t.Expression> {
|
||||||
|
return t.isFunction(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseReturnValue(fn: t.Expression): t.Expression {
|
||||||
|
assert(fn, this.isFunctionExpression, 'a function');
|
||||||
|
if (!t.isBlockStatement(fn.body)) {
|
||||||
|
// it is a simple array function expression: `(...) => expr`
|
||||||
|
return fn.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// it is a function (arrow or normal) with a body. E.g.:
|
||||||
|
// * `(...) => { stmt; ... }`
|
||||||
|
// * `function(...) { stmt; ... }`
|
||||||
|
|
||||||
|
if (fn.body.body.length !== 1) {
|
||||||
|
throw new FatalLinkerError(
|
||||||
|
fn.body, 'Unsupported syntax, expected a function body with a single return statement.');
|
||||||
|
}
|
||||||
|
const stmt = fn.body.body[0];
|
||||||
|
assert(stmt, t.isReturnStatement, 'a function body with a single return statement');
|
||||||
|
if (stmt.argument === null) {
|
||||||
|
throw new FatalLinkerError(stmt, 'Unsupported syntax, expected function to return a value.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return stmt.argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRange(node: t.Expression): Range {
|
||||||
|
if (node.loc == null || node.start === null || node.end === null) {
|
||||||
|
throw new FatalLinkerError(
|
||||||
|
node, 'Unable to read range for node - it is missing location information.');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
startLine: node.loc.start.line - 1, // Babel lines are 1-based
|
||||||
|
startCol: node.loc.start.column,
|
||||||
|
startPos: node.start,
|
||||||
|
endPos: node.end,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the expression does not represent an empty element in an array literal.
|
||||||
|
* For example in `[,foo]` the first element is "empty".
|
||||||
|
*/
|
||||||
|
function isNotEmptyElement(e: t.Expression|t.SpreadElement|null): e is t.Expression|
|
||||||
|
t.SpreadElement {
|
||||||
|
return e !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the expression is not a spread element of an array literal.
|
||||||
|
* For example in `[x, ...rest]` the `...rest` expression is a spread element.
|
||||||
|
*/
|
||||||
|
function isNotSpreadElement(e: t.Expression|t.SpreadElement): e is t.Expression {
|
||||||
|
return !t.isSpreadElement(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the expression can be considered a text based property name.
|
||||||
|
*/
|
||||||
|
function isPropertyName(e: t.Expression): e is t.Identifier|t.StringLiteral|t.NumericLiteral {
|
||||||
|
return t.isIdentifier(e) || t.isStringLiteral(e) || t.isNumericLiteral(e);
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
/**
|
||||||
|
* @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';
|
||||||
|
|
||||||
|
import {FatalLinkerError} from '../../fatal_linker_error';
|
||||||
|
import {AstHost, Range} from '../ast_host';
|
||||||
|
import {assert} from '../utils';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation of `AstHost` is able to get information from TypeScript AST nodes.
|
||||||
|
*
|
||||||
|
* This host is not actually used at runtime in the current code.
|
||||||
|
*
|
||||||
|
* It is implemented here to ensure that the `AstHost` abstraction is not unfairly skewed towards
|
||||||
|
* the Babel implementation. It could also provide a basis for a 3rd TypeScript compiler plugin to
|
||||||
|
* do linking in the future.
|
||||||
|
*/
|
||||||
|
export class TypeScriptAstHost implements AstHost<ts.Expression> {
|
||||||
|
getSymbolName(node: ts.Expression): string|null {
|
||||||
|
if (ts.isIdentifier(node)) {
|
||||||
|
return node.text;
|
||||||
|
} else if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
|
||||||
|
return node.name.text;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isStringLiteral = ts.isStringLiteral;
|
||||||
|
|
||||||
|
parseStringLiteral(str: ts.Expression): string {
|
||||||
|
assert(str, this.isStringLiteral, 'a string literal');
|
||||||
|
return str.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNumericLiteral = ts.isNumericLiteral;
|
||||||
|
|
||||||
|
parseNumericLiteral(num: ts.Expression): number {
|
||||||
|
assert(num, this.isNumericLiteral, 'a numeric literal');
|
||||||
|
return parseInt(num.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
isBooleanLiteral(node: ts.Expression): node is ts.FalseLiteral|ts.TrueLiteral {
|
||||||
|
return node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseBooleanLiteral(bool: ts.Expression): boolean {
|
||||||
|
assert(bool, this.isBooleanLiteral, 'a boolean literal');
|
||||||
|
return bool.kind === ts.SyntaxKind.TrueKeyword;
|
||||||
|
}
|
||||||
|
|
||||||
|
isArrayLiteral = ts.isArrayLiteralExpression;
|
||||||
|
|
||||||
|
parseArrayLiteral(array: ts.Expression): ts.Expression[] {
|
||||||
|
assert(array, this.isArrayLiteral, 'an array literal');
|
||||||
|
return array.elements.map(element => {
|
||||||
|
assert(element, isNotEmptyElement, 'element in array not to be empty');
|
||||||
|
assert(element, isNotSpreadElement, 'element in array not to use spread syntax');
|
||||||
|
return element;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isObjectLiteral = ts.isObjectLiteralExpression;
|
||||||
|
|
||||||
|
parseObjectLiteral(obj: ts.Expression): Map<string, ts.Expression> {
|
||||||
|
assert(obj, this.isObjectLiteral, 'an object literal');
|
||||||
|
|
||||||
|
const result = new Map<string, ts.Expression>();
|
||||||
|
for (const property of obj.properties) {
|
||||||
|
assert(property, ts.isPropertyAssignment, 'a property assignment');
|
||||||
|
assert(property.name, isPropertyName, 'a property name');
|
||||||
|
result.set(property.name.text, property.initializer);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFunctionExpression(node: ts.Expression): node is ts.FunctionExpression|ts.ArrowFunction {
|
||||||
|
return ts.isFunctionExpression(node) || ts.isArrowFunction(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseReturnValue(fn: ts.Expression): ts.Expression {
|
||||||
|
assert(fn, this.isFunctionExpression, 'a function');
|
||||||
|
if (!ts.isBlock(fn.body)) {
|
||||||
|
// it is a simple array function expression: `(...) => expr`
|
||||||
|
return fn.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// it is a function (arrow or normal) with a body. E.g.:
|
||||||
|
// * `(...) => { stmt; ... }`
|
||||||
|
// * `function(...) { stmt; ... }`
|
||||||
|
|
||||||
|
if (fn.body.statements.length !== 1) {
|
||||||
|
throw new FatalLinkerError(
|
||||||
|
fn.body, 'Unsupported syntax, expected a function body with a single return statement.');
|
||||||
|
}
|
||||||
|
const stmt = fn.body.statements[0];
|
||||||
|
assert(stmt, ts.isReturnStatement, 'a function body with a single return statement');
|
||||||
|
if (stmt.expression === undefined) {
|
||||||
|
throw new FatalLinkerError(stmt, 'Unsupported syntax, expected function to return a value.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return stmt.expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRange(node: ts.Expression): Range {
|
||||||
|
const file = node.getSourceFile();
|
||||||
|
if (file === undefined) {
|
||||||
|
throw new FatalLinkerError(
|
||||||
|
node, 'Unable to read range for node - it is missing parent information.');
|
||||||
|
}
|
||||||
|
const startPos = node.getStart();
|
||||||
|
const endPos = node.getEnd();
|
||||||
|
const {line: startLine, character: startCol} = ts.getLineAndCharacterOfPosition(file, startPos);
|
||||||
|
return {startLine, startCol, startPos, endPos};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the expression does not represent an empty element in an array literal.
|
||||||
|
* For example in `[,foo]` the first element is "empty".
|
||||||
|
*/
|
||||||
|
function isNotEmptyElement(e: ts.Expression|ts.SpreadElement|
|
||||||
|
ts.OmittedExpression): e is ts.Expression|ts.SpreadElement {
|
||||||
|
return !ts.isOmittedExpression(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the expression is not a spread element of an array literal.
|
||||||
|
* For example in `[x, ...rest]` the `...rest` expression is a spread element.
|
||||||
|
*/
|
||||||
|
function isNotSpreadElement(e: ts.Expression|ts.SpreadElement): e is ts.Expression {
|
||||||
|
return !ts.isSpreadElement(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the expression can be considered a text based property name.
|
||||||
|
*/
|
||||||
|
function isPropertyName(e: ts.PropertyName): e is ts.Identifier|ts.StringLiteral|ts.NumericLiteral {
|
||||||
|
return ts.isIdentifier(e) || ts.isStringLiteral(e) || ts.isNumericLiteral(e);
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* @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 {FatalLinkerError} from '../fatal_linker_error';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that the given `node` is of the type guarded by the `predicate` function.
|
||||||
|
*/
|
||||||
|
export function assert<T, K extends T>(
|
||||||
|
node: T, predicate: (node: T) => node is K, expected: string): asserts node is K {
|
||||||
|
if (!predicate(node)) {
|
||||||
|
throw new FatalLinkerError(node, `Unsupported syntax, expected ${expected}.`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An unrecoverable error during linking.
|
||||||
|
*/
|
||||||
|
export class FatalLinkerError extends Error {
|
||||||
|
private readonly type = 'FatalLinkerError';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new FatalLinkerError.
|
||||||
|
*
|
||||||
|
* @param node The AST node where the error occurred.
|
||||||
|
* @param message A description of the error.
|
||||||
|
*/
|
||||||
|
constructor(public node: unknown, message: string) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the given object `e` is a FatalLinkerError.
|
||||||
|
*/
|
||||||
|
export function isFatalLinkerError(e: any): e is FatalLinkerError {
|
||||||
|
return e && e.type === 'FatalLinkerError';
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* @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 {ImportGenerator, NamedImport} from '../../src/ngtsc/translator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that is used to generate imports when translating from Angular Output AST to an AST to
|
||||||
|
* render, such as Babel.
|
||||||
|
*
|
||||||
|
* Note that, in the linker, there can only be imports from `@angular/core` and that these imports
|
||||||
|
* must be achieved by property access on an `ng` namespace identifer, which is passed in via the
|
||||||
|
* constructor.
|
||||||
|
*/
|
||||||
|
export class LinkerImportGenerator<TExpression> implements ImportGenerator<TExpression> {
|
||||||
|
constructor(private ngImport: TExpression) {}
|
||||||
|
|
||||||
|
generateNamespaceImport(moduleName: string): TExpression {
|
||||||
|
this.assertModuleName(moduleName);
|
||||||
|
return this.ngImport;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateNamedImport(moduleName: string, originalSymbol: string): NamedImport<TExpression> {
|
||||||
|
this.assertModuleName(moduleName);
|
||||||
|
return {moduleImport: this.ngImport, symbol: originalSymbol};
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertModuleName(moduleName: string): void {
|
||||||
|
if (moduleName !== '@angular/core') {
|
||||||
|
throw new Error(`Unable to import from anything other than '@angular/core'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
|
||||||
|
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "test_lib",
|
||||||
|
testonly = True,
|
||||||
|
srcs = glob([
|
||||||
|
"**/*.ts",
|
||||||
|
]),
|
||||||
|
deps = [
|
||||||
|
"//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",
|
||||||
|
"@npm//typescript",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
jasmine_node_test(
|
||||||
|
name = "test",
|
||||||
|
bootstrap = ["//tools/testing:node_no_angular_es5"],
|
||||||
|
deps = [
|
||||||
|
":test_lib",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,382 @@
|
||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,305 @@
|
||||||
|
/**
|
||||||
|
* @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,289 @@
|
||||||
|
/**
|
||||||
|
* @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';
|
||||||
|
import {TypeScriptAstHost} from '../../../src/ast/typescript/typescript_ast_host';
|
||||||
|
|
||||||
|
describe('TypeScriptAstHost', () => {
|
||||||
|
let host: TypeScriptAstHost;
|
||||||
|
beforeEach(() => host = new TypeScriptAstHost());
|
||||||
|
|
||||||
|
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(rhs('x = {}'))).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(rhs('x = {}'))).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(rhs('x = {}'))).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(rhs('x = {}'))).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 = jasmine.objectContaining({text: 'moo', kind: ts.SyntaxKind.StringLiteral});
|
||||||
|
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 = jasmine.objectContaining({text: 'moo', kind: ts.SyntaxKind.StringLiteral});
|
||||||
|
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 not a function', () => {
|
||||||
|
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(rhs('x = {}'))).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 = jasmine.objectContaining({text: 'moo', kind: ts.SyntaxKind.StringLiteral});
|
||||||
|
expect(host.parseReturnValue(rhs('x = function() { return \'moo\'; }'))).toEqual(moo);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract the value of a simple arrow function', () => {
|
||||||
|
const moo = jasmine.objectContaining({text: 'moo', kind: ts.SyntaxKind.StringLiteral});
|
||||||
|
expect(host.parseReturnValue(rhs('x = () => \'moo\''))).toEqual(moo);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract the return value of an arrow function', () => {
|
||||||
|
const moo = jasmine.objectContaining({text: 'moo', kind: ts.SyntaxKind.StringLiteral});
|
||||||
|
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 moo = rhs('// preamble\nx = \'moo\';');
|
||||||
|
expect(host.getRange(moo)).toEqual({startLine: 1, startCol: 4, startPos: 16, endPos: 21});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error if the nodes do not have attached parents', () => {
|
||||||
|
const moo = rhs('// preamble\nx = \'moo\';', false);
|
||||||
|
expect(() => host.getRange(moo))
|
||||||
|
.toThrowError('Unable to read range for node - it is missing parent information.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function stmt(code: string, attachParents = true): ts.Statement {
|
||||||
|
const sf = ts.createSourceFile('test.ts', code, ts.ScriptTarget.ES2015, attachParents);
|
||||||
|
return sf.statements[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function expr(code: string, attachParents = true): ts.Expression {
|
||||||
|
return (stmt(code, attachParents) as ts.ExpressionStatement).expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rhs(code: string, attachParents = true): ts.Expression {
|
||||||
|
const e = expr(code, attachParents);
|
||||||
|
if (!ts.isBinaryExpression(e)) {
|
||||||
|
throw new Error('Bad test - expected a binary expression');
|
||||||
|
}
|
||||||
|
return e.right;
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* @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 {FatalLinkerError, isFatalLinkerError} from '../src/fatal_linker_error';
|
||||||
|
|
||||||
|
describe('FatalLinkerError', () => {
|
||||||
|
it('should expose the `node` and `message`', () => {
|
||||||
|
const node = {};
|
||||||
|
expect(new FatalLinkerError(node, 'Some message'))
|
||||||
|
.toEqual(jasmine.objectContaining({node, message: 'Some message'}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isFatalLinkerError()', () => {
|
||||||
|
it('should return true if the error is of type `FatalLinkerError`', () => {
|
||||||
|
const error = new FatalLinkerError({}, 'Some message');
|
||||||
|
expect(isFatalLinkerError(error)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if the error is not of type `FatalLinkerError`', () => {
|
||||||
|
const error = new Error('Some message');
|
||||||
|
expect(isFatalLinkerError(error)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 {LinkerImportGenerator} from '../src/linker_import_generator';
|
||||||
|
|
||||||
|
const ngImport = {
|
||||||
|
type: 'ngImport'
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('LinkerImportGenerator<TExpression>', () => {
|
||||||
|
describe('generateNamespaceImport()', () => {
|
||||||
|
it('should error if the import is not `@angular/core`', () => {
|
||||||
|
const generator = new LinkerImportGenerator(ngImport);
|
||||||
|
expect(() => generator.generateNamespaceImport('other/import'))
|
||||||
|
.toThrowError(`Unable to import from anything other than '@angular/core'`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the ngImport expression for `@angular/core`', () => {
|
||||||
|
const generator = new LinkerImportGenerator(ngImport);
|
||||||
|
expect(generator.generateNamespaceImport('@angular/core')).toBe(ngImport);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateNamedImport()', () => {
|
||||||
|
it('should error if the import is not `@angular/core`', () => {
|
||||||
|
const generator = new LinkerImportGenerator(ngImport);
|
||||||
|
expect(() => generator.generateNamedImport('other/import', 'someSymbol'))
|
||||||
|
.toThrowError(`Unable to import from anything other than '@angular/core'`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a `NamedImport` object containing the ngImport expression', () => {
|
||||||
|
const generator = new LinkerImportGenerator(ngImport);
|
||||||
|
expect(generator.generateNamedImport('@angular/core', 'someSymbol'))
|
||||||
|
.toEqual({moduleImport: ngImport, symbol: 'someSymbol'});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -11,6 +11,8 @@
|
||||||
"ng-xi18n": "./src/extract_i18n.js"
|
"ng-xi18n": "./src/extract_i18n.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.8.6",
|
||||||
|
"@babel/types": "^7.8.6",
|
||||||
"reflect-metadata": "^0.1.2",
|
"reflect-metadata": "^0.1.2",
|
||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.0",
|
||||||
"canonical-path": "1.0.0",
|
"canonical-path": "1.0.0",
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapLocation, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator} from './src/api/ast_factory';
|
export {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapLocation, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator, VariableDeclarationType} from './src/api/ast_factory';
|
||||||
export {Import, ImportGenerator, NamedImport} from './src/api/import_generator';
|
export {Import, ImportGenerator, NamedImport} from './src/api/import_generator';
|
||||||
export {ImportManager} from './src/import_manager';
|
export {ImportManager} from './src/import_manager';
|
||||||
export {RecordWrappedNodeExprFn} from './src/translator';
|
export {RecordWrappedNodeExprFn} from './src/translator';
|
||||||
|
|
|
@ -21,7 +21,7 @@ export interface AstFactory<TStatement, TExpression> {
|
||||||
* @param leadingComments the comments to attach.
|
* @param leadingComments the comments to attach.
|
||||||
* @returns the node passed in as `statement` with the comments attached.
|
* @returns the node passed in as `statement` with the comments attached.
|
||||||
*/
|
*/
|
||||||
attachComments(statement: TStatement, leadingComments?: LeadingComment[]): TStatement;
|
attachComments(statement: TStatement, leadingComments: LeadingComment[]|undefined): TStatement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a literal array expresion (e.g. `[expr1, expr2]`).
|
* Create a literal array expresion (e.g. `[expr1, expr2]`).
|
||||||
|
|
|
@ -235,7 +235,7 @@ export function createTemplateTail(cooked: string, raw: string): ts.TemplateTail
|
||||||
* @param leadingComments The comments to attach to the statement.
|
* @param leadingComments The comments to attach to the statement.
|
||||||
*/
|
*/
|
||||||
export function attachComments<T extends ts.Statement>(
|
export function attachComments<T extends ts.Statement>(
|
||||||
statement: T, leadingComments?: LeadingComment[]): T {
|
statement: T, leadingComments: LeadingComment[]|undefined): T {
|
||||||
if (leadingComments === undefined) {
|
if (leadingComments === undefined) {
|
||||||
return statement;
|
return statement;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue