diff --git a/src/compiler/core.ts b/src/compiler/core.ts index fec9b9092c2..2da7eaaa5af 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -740,6 +740,14 @@ namespace ts { return to; } + /** + * Gets the actual offset into an array for a relative offset. Negative offsets indicate a + * position offset from the end of the array. + */ + function toOffset(array: any[], offset: number) { + return offset < 0 ? array.length + offset : offset; + } + /** * Appends a range of value to an array, returning the array. * @@ -747,11 +755,19 @@ namespace ts { * is created if `value` was appended. * @param from The values to append to the array. If `from` is `undefined`, nothing is * appended. If an element of `from` is `undefined`, that element is not appended. + * @param start The offset in `from` at which to start copying values. + * @param end The offset in `from` at which to stop copying values (non-inclusive). */ - export function addRange(to: T[] | undefined, from: T[] | undefined): T[] | undefined { + export function addRange(to: T[] | undefined, from: T[] | undefined, start?: number, end?: number): T[] | undefined { if (from === undefined) return to; - for (const v of from) { - to = append(to, v); + if (to === undefined) return from.slice(start, end); + start = start === undefined ? 0 : toOffset(from, start); + end = end === undefined ? from.length : toOffset(from, end); + for (let i = start; i < end && i < from.length; i++) { + const v = from[i]; + if (v !== undefined) { + to.push(from[i]); + } } return to; } @@ -781,9 +797,13 @@ namespace ts { * A negative offset indicates the element should be retrieved from the end of the array. */ export function elementAt(array: T[] | undefined, offset: number): T | undefined { - return array && array.length > 0 && (offset < 0 ? ~offset : offset) < array.length - ? array[offset < 0 ? array.length + offset : offset] - : undefined; + if (array) { + offset = toOffset(array, offset); + if (offset < array.length) { + return array[offset]; + } + } + return undefined; } /** diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 5d4457fdfd8..a3380d5cd9c 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -3736,7 +3736,7 @@ namespace ts { All = Parentheses | Assertions | PartiallyEmittedExpressions } - export type OuterExpression = ParenthesizedExpression | TypeAssertion | AsExpression | PartiallyEmittedExpression; + export type OuterExpression = ParenthesizedExpression | TypeAssertion | AsExpression | NonNullExpression | PartiallyEmittedExpression; export function isOuterExpression(node: Node, kinds = OuterExpressionKinds.All): node is OuterExpression { switch (node.kind) { @@ -3744,6 +3744,7 @@ namespace ts { return (kinds & OuterExpressionKinds.Parentheses) !== 0; case SyntaxKind.TypeAssertionExpression: case SyntaxKind.AsExpression: + case SyntaxKind.NonNullExpression: return (kinds & OuterExpressionKinds.Assertions) !== 0; case SyntaxKind.PartiallyEmittedExpression: return (kinds & OuterExpressionKinds.PartiallyEmittedExpressions) !== 0; @@ -3787,8 +3788,8 @@ namespace ts { export function skipAssertions(node: Expression): Expression; export function skipAssertions(node: Node): Node; export function skipAssertions(node: Node): Node { - while (isAssertionExpression(node)) { - node = (node).expression; + while (isAssertionExpression(node) || node.kind === SyntaxKind.NonNullExpression) { + node = (node).expression; } return node; @@ -3809,6 +3810,7 @@ namespace ts { case SyntaxKind.ParenthesizedExpression: return updateParen(outerExpression, expression); case SyntaxKind.TypeAssertionExpression: return updateTypeAssertion(outerExpression, outerExpression.type, expression); case SyntaxKind.AsExpression: return updateAsExpression(outerExpression, expression, outerExpression.type); + case SyntaxKind.NonNullExpression: return updateNonNullExpression(outerExpression, expression); case SyntaxKind.PartiallyEmittedExpression: return updatePartiallyEmittedExpression(outerExpression, expression); } } diff --git a/src/compiler/transformers/es2015.ts b/src/compiler/transformers/es2015.ts index 4b9c577805e..71a2fa0e2e0 100644 --- a/src/compiler/transformers/es2015.ts +++ b/src/compiler/transformers/es2015.ts @@ -339,6 +339,58 @@ namespace ts { && !(node).expression; } + function isClassLikeVariableStatement(node: Node) { + if (!isVariableStatement(node)) return false; + const variable = singleOrUndefined((node).declarationList.declarations); + return variable + && variable.initializer + && isIdentifier(variable.name) + && (isClassLike(variable.initializer) + || (isAssignmentExpression(variable.initializer) + && isIdentifier(variable.initializer.left) + && isClassLike(variable.initializer.right))); + } + + function isTypeScriptClassWrapper(node: Node) { + const call = tryCast(node, isCallExpression); + if (!call || isParseTreeNode(call) || + some(call.typeArguments) || + some(call.arguments)) { + return false; + } + + const func = tryCast(skipOuterExpressions(call.expression), isFunctionExpression); + if (!func || isParseTreeNode(func) || + some(func.typeParameters) || + some(func.parameters) || + func.type || + !func.body) { + return false; + } + + const statements = func.body.statements; + if (statements.length < 2) { + return false; + } + + const firstStatement = statements[0]; + if (isParseTreeNode(firstStatement) || + !isClassLike(firstStatement) && + !isClassLikeVariableStatement(firstStatement)) { + return false; + } + + const lastStatement = elementAt(statements, -1); + const returnStatement = tryCast(isVariableStatement(lastStatement) ? elementAt(statements, -2) : lastStatement, isReturnStatement); + if (!returnStatement || + !returnStatement.expression || + !isIdentifier(skipOuterExpressions(returnStatement.expression))) { + return false; + } + + return true; + } + function shouldVisitNode(node: Node): boolean { return (node.transformFlags & TransformFlags.ContainsES2015) !== 0 || convertedLoopState !== undefined @@ -3326,23 +3378,24 @@ namespace ts { // var C_1; // }()) // - const aliasAssignment = isAssignmentExpression(initializer) ? initializer : undefined; + const aliasAssignment = tryCast(initializer, isAssignmentExpression); // The underlying call (3) is another IIFE that may contain a '_super' argument. const call = cast(aliasAssignment ? skipOuterExpressions(aliasAssignment.right) : initializer, isCallExpression); const func = cast(skipOuterExpressions(call.expression), isFunctionExpression); - // When we extract the statements of the inner IIFE, we exclude the 'return' statement (4) - // as we already have one that has been introduced by the 'ts' transformer. - const funcStatements = func.body.statements.slice(0, -1); + const funcStatements = func.body.statements; + let classBodyStart = 0; + let classBodyEnd = -1; const statements: Statement[] = []; if (aliasAssignment) { // If we have a class alias assignment, we need to move it to the down-level constructor // function we generated for the class. - const hasExtendsCall = isExpressionStatement(funcStatements[0]); - if (hasExtendsCall) { - statements.push(funcStatements[0]); + const extendsCall = tryCast(funcStatements[classBodyStart], isExpressionStatement); + if (extendsCall) { + statements.push(extendsCall); + classBodyStart++; } // We reuse the comment and source-map positions from the original variable statement @@ -3359,25 +3412,37 @@ namespace ts { updateBinary(aliasAssignment, aliasAssignment.left, convertFunctionDeclarationToExpression( - cast(funcStatements[hasExtendsCall ? 1 : 0], isFunctionDeclaration) + cast(funcStatements[classBodyStart], isFunctionDeclaration) ) ) ) ]) ) ); - - addRange(statements, funcStatements.slice(hasExtendsCall ? 2 : 1)); - } - else { - addRange(statements, funcStatements); + classBodyStart++; } + // Find the trailing 'return' statement (4) + while (!isReturnStatement(elementAt(funcStatements, classBodyEnd))) { + classBodyEnd--; + } + + // When we extract the statements of the inner IIFE, we exclude the 'return' statement (4) + // as we already have one that has been introduced by the 'ts' transformer. + addRange(statements, funcStatements, classBodyStart, classBodyEnd); + + if (classBodyEnd < -1) { + // If there were any hoisted declarations following the return statement, we should + // append them. + addRange(statements, funcStatements, classBodyEnd + 1); + } + + // Add the remaining statements of the outer wrapper. addRange(statements, remainingStatements); // The 'es2015' class transform may add an end-of-declaration marker. If so we will add it // after the remaining statements from the 'ts' transformer. - addRange(statements, classStatements.slice(1)); + addRange(statements, classStatements, /*start*/ 1); // Recreate any outer parentheses or partially-emitted expressions to preserve source map // and comment locations. @@ -3952,7 +4017,6 @@ namespace ts { return false; } if (isClassElement(currentNode) && currentNode.parent === declaration) { - // we are in the class body, but we treat static fields as outside of the class body return true; } currentNode = currentNode.parent; diff --git a/src/compiler/transformers/ts.ts b/src/compiler/transformers/ts.ts index 876a7ff2d72..4fb21f7ec9e 100644 --- a/src/compiler/transformers/ts.ts +++ b/src/compiler/transformers/ts.ts @@ -19,10 +19,11 @@ namespace ts { } const enum ClassFacts { + None = 0, HasStaticInitializedProperties = 1 << 0, HasConstructorDecorators = 1 << 1, HasMemberDecorators = 1 << 2, - IsNamespaceExport = 1 << 3, + IsExportOfNamespace = 1 << 3, IsNamedExternalExport = 1 << 4, IsDefaultExternalExport = 1 << 5, HasExtendsClause = 1 << 6, @@ -31,7 +32,7 @@ namespace ts { HasAnyDecorators = HasConstructorDecorators | HasMemberDecorators, NeedsName = HasStaticInitializedProperties | HasMemberDecorators, MayNeedImmediatelyInvokedFunctionExpression = HasAnyDecorators | HasStaticInitializedProperties, - IsExported = IsNamespaceExport | IsDefaultExternalExport | IsNamedExternalExport, + IsExported = IsExportOfNamespace | IsDefaultExternalExport | IsNamedExternalExport, } export function transformTypeScript(context: TransformationContext) { @@ -519,14 +520,14 @@ namespace ts { } function getClassFacts(node: ClassDeclaration, staticProperties: PropertyDeclaration[]) { - let facts: ClassFacts = 0; + let facts = ClassFacts.None; if (some(staticProperties)) facts |= ClassFacts.HasStaticInitializedProperties; if (getClassExtendsHeritageClauseElement(node)) facts |= ClassFacts.HasExtendsClause; if (shouldEmitDecorateCallForClass(node)) facts |= ClassFacts.HasConstructorDecorators; if (childIsDecorated(node)) facts |= ClassFacts.HasMemberDecorators; - if (isNamespaceExport(node)) facts |= ClassFacts.IsNamespaceExport; - if ((facts & ClassFacts.IsExported) === 0 && isDefaultExternalModuleExport(node)) facts |= ClassFacts.IsDefaultExternalExport; - if ((facts & ClassFacts.IsExported) === 0 && isNamedExternalModuleExport(node)) facts |= ClassFacts.IsNamedExternalExport; + if (isExportOfNamespace(node)) facts |= ClassFacts.IsExportOfNamespace; + else if (isDefaultExternalModuleExport(node)) facts |= ClassFacts.IsDefaultExternalExport; + else if (isNamedExternalModuleExport(node)) facts |= ClassFacts.IsNamedExternalExport; if (languageVersion <= ScriptTarget.ES5 && (facts & ClassFacts.MayNeedImmediatelyInvokedFunctionExpression)) facts |= ClassFacts.UseImmediatelyInvokedFunctionExpression; return facts; } @@ -609,7 +610,7 @@ namespace ts { // If the class is exported as part of a TypeScript namespace, emit the namespace export. // Otherwise, if the class was exported at the top level and was decorated, emit an export // declaration or export default for the class. - if (facts & ClassFacts.IsNamespaceExport) { + if (facts & ClassFacts.IsExportOfNamespace) { addExportMemberAssignment(statements, node); } else if (facts & ClassFacts.UseImmediatelyInvokedFunctionExpression || facts & ClassFacts.HasConstructorDecorators) { @@ -672,11 +673,6 @@ namespace ts { /** * Transforms a decorated class declaration and appends the resulting statements. If * the class requires an alias to avoid issues with double-binding, the alias is returned. - * - * @param statements A statement list to which to add the declaration. - * @param node A ClassDeclaration node. - * @param name The name of the class. - * @param facts Precomputed facts about the clas. */ function createClassDeclarationHeadWithDecorators(node: ClassDeclaration, name: Identifier, facts: ClassFacts) { // When we emit an ES6 class that has a class decorator, we must tailor the @@ -2223,7 +2219,7 @@ namespace ts { /*type*/ undefined, visitFunctionBody(node.body, visitor, context) || createBlock([]) ); - if (isNamespaceExport(node)) { + if (isExportOfNamespace(node)) { const statements: Statement[] = [updated]; addExportMemberAssignment(statements, node); return statements; @@ -2316,7 +2312,7 @@ namespace ts { * - The node is exported from a TypeScript namespace. */ function visitVariableStatement(node: VariableStatement): Statement { - if (isNamespaceExport(node)) { + if (isExportOfNamespace(node)) { const variables = getInitializedVariables(node.declarationList); if (variables.length === 0) { // elide statement if there are no initialized variables. @@ -2620,7 +2616,7 @@ namespace ts { * or `exports.x`). */ function hasNamespaceQualifiedExportName(node: Node) { - return isNamespaceExport(node) + return isExportOfNamespace(node) || (isExternalModuleExport(node) && moduleKind !== ModuleKind.ES2015 && moduleKind !== ModuleKind.System); @@ -3062,7 +3058,7 @@ namespace ts { const moduleReference = createExpressionFromEntityName(node.moduleReference); setEmitFlags(moduleReference, EmitFlags.NoComments | EmitFlags.NoNestedComments); - if (isNamedExternalModuleExport(node) || !isNamespaceExport(node)) { + if (isNamedExternalModuleExport(node) || !isExportOfNamespace(node)) { // export var ${name} = ${moduleReference}; // var ${name} = ${moduleReference}; return setOriginalNode( @@ -3103,7 +3099,7 @@ namespace ts { * * @param node The node to test. */ - function isNamespaceExport(node: Node) { + function isExportOfNamespace(node: Node) { return currentNamespace !== undefined && hasModifier(node, ModifierFlags.Export); } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index b5d3fe348d9..5ab6cb3c336 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -850,72 +850,10 @@ namespace ts { return node && (node.kind === SyntaxKind.GetAccessor || node.kind === SyntaxKind.SetAccessor); } - export function isClassLike(node: Node | undefined): node is ClassLikeDeclaration { + export function isClassLike(node: Node): node is ClassLikeDeclaration { return node && (node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.ClassExpression); } - export function isImmediatelyInvokedFunctionExpression(node: Node) { - if (!isCallExpression(node) || - some(node.typeArguments) || - some(node.arguments)) { - return false; - } - const expression = skipParentheses(node.expression); - if (!isFunctionExpression(expression) || - some(expression.typeParameters) || - some(expression.parameters) || - expression.type || - !expression.body) { - return false; - } - return true; - } - - export function isTypeScriptClassWrapper(node: Node) { - if (!isImmediatelyInvokedFunctionExpression(node) || - isParseTreeNode(node)) { - return false; - } - - const func = skipParentheses((node).expression); - if (isParseTreeNode(func)) { - return false; - } - - const statements = func.body.statements; - if (statements.length < 2) { - return false; - } - - const firstStatement = statements[0]; - if (isParseTreeNode(firstStatement) || - !isClassLike(firstStatement) && - !isClassLikeVariableStatement(firstStatement)) { - return false; - } - - const returnStatement = tryCast(elementAt(statements, isVariableStatement(lastOrUndefined(statements)) ? -2 : -1), isReturnStatement); - if (!isReturnStatement(returnStatement) || - !returnStatement.expression || - !isIdentifier(skipOuterExpressions(returnStatement.expression))) { - return false; - } - - return true; - } - - function isClassLikeVariableStatement(node: Node) { - if (!isVariableStatement(node)) return false; - const variable = singleOrUndefined((node).declarationList.declarations); - return variable - && variable.initializer - && isIdentifier(variable.name) - && (isClassLike(variable.initializer) - || (isAssignmentExpression(variable.initializer) - && isIdentifier(variable.initializer.left) - && isClassLike(variable.initializer.right))); - } - export function isExpressionStatement(node: Node): node is ExpressionStatement { return node.kind === SyntaxKind.ExpressionStatement; } @@ -3438,53 +3376,32 @@ namespace ts { /** * Formats an enum value as a string for debugging and debug assertions. */ - function formatEnum(value = 0, enumObject: any, isFlags: boolean, formatCache?: string[]) { - const cached = formatCache && formatCache[value]; - if (cached !== undefined) { - return cached; - } - const result = isFlags ? formatFlagsEnum(value, enumObject) : getEnumName(value, enumObject); - if (formatCache) { - formatCache[value] = result; - } - return result; - } - - /** - * Gets the name for an enum value for debugging and debug assertions. - */ - function getEnumName(value: number, enumObject: any) { - for (const name in enumObject) { - if (enumObject[name] === value) { - return name; - } - } - return value.toString(); - } - - /** - * Formats the bitwise flag values of an enum value for debugging and debug assertions. - */ - function formatFlagsEnum(value: number, enumObject: any) { + function formatEnum(value = 0, enumObject: any, isFlags?: boolean) { const members = getEnumMembers(enumObject); - let result = ""; if (value === 0) { return members.length > 0 && members[0][0] === 0 ? members[0][1] : "0"; } - - let remainingFlags = value; - for (let i = members.length - 1; i >= 0 && remainingFlags !== 0; i--) { - const [enumValue, enumName] = members[i]; - if (enumValue !== 0 && (remainingFlags & enumValue) === enumValue) { - remainingFlags &= ~enumValue; - result = `${enumName}${result ? ", " : ""}${result}`; + if (isFlags) { + let result = ""; + let remainingFlags = value; + for (let i = members.length - 1; i >= 0 && remainingFlags !== 0; i--) { + const [enumValue, enumName] = members[i]; + if (enumValue !== 0 && (remainingFlags & enumValue) === enumValue) { + remainingFlags &= ~enumValue; + result = `${enumName}${result ? ", " : ""}${result}`; + } + } + if (remainingFlags === 0) { + return result; } } - - if (remainingFlags === 0) { - return result; + else { + for (const [enumValue, enumName] of members) { + if (enumValue === value) { + return enumName; + } + } } - return value.toString(); } @@ -3500,39 +3417,32 @@ namespace ts { return stableSort(result, (x, y) => compareValues(x[0], y[0])); } - const syntaxKindCache: string[] = []; export function formatSyntaxKind(kind: SyntaxKind): string { - return formatEnum(kind, (ts).SyntaxKind, /*isFlags*/ false, syntaxKindCache); + return formatEnum(kind, (ts).SyntaxKind, /*isFlags*/ false); } - const modifierFlagsCache: string[] = []; export function formatModifierFlags(flags: ModifierFlags): string { - return formatEnum(flags, (ts).ModifierFlags, /*isFlags*/ true, modifierFlagsCache); + return formatEnum(flags, (ts).ModifierFlags, /*isFlags*/ true); } - const transformFlagsCache: string[] = []; export function formatTransformFlags(flags: TransformFlags): string { - return formatEnum(flags, (ts).TransformFlags, /*isFlags*/ true, transformFlagsCache); + return formatEnum(flags, (ts).TransformFlags, /*isFlags*/ true); } - const emitFlagsCache: string[] = []; export function formatEmitFlags(flags: EmitFlags): string { - return formatEnum(flags, (ts).EmitFlags, /*isFlags*/ true, emitFlagsCache); + return formatEnum(flags, (ts).EmitFlags, /*isFlags*/ true); } - const symbolFlagsCache: string[] = []; export function formatSymbolFlags(flags: SymbolFlags): string { - return formatEnum(flags, (ts).SymbolFlags, /*isFlags*/ true, symbolFlagsCache); + return formatEnum(flags, (ts).SymbolFlags, /*isFlags*/ true); } - const typeFlagsCache: string[] = []; export function formatTypeFlags(flags: TypeFlags): string { - return formatEnum(flags, (ts).TypeFlags, /*isFlags*/ true, typeFlagsCache); + return formatEnum(flags, (ts).TypeFlags, /*isFlags*/ true); } - const objectFlagsCache: string[] = []; export function formatObjectFlags(flags: ObjectFlags): string { - return formatEnum(flags, (ts).ObjectFlags, /*isFlags*/ true, objectFlagsCache); + return formatEnum(flags, (ts).ObjectFlags, /*isFlags*/ true); } export function getRangePos(range: TextRange | undefined) {