diff --git a/src/compiler/factory/nodeFactory.ts b/src/compiler/factory/nodeFactory.ts index 2c2fde823ac..b5151fd3d61 100644 --- a/src/compiler/factory/nodeFactory.ts +++ b/src/compiler/factory/nodeFactory.ts @@ -2724,6 +2724,14 @@ namespace ts { node.operator = operator; node.operand = parenthesizerRules().parenthesizeOperandOfPrefixUnary(operand); node.transformFlags |= propagateChildFlags(node.operand); + // Only set this flag for non-generated identifiers and non-"local" names. See the + // comment in `visitPreOrPostfixUnaryExpression` in module.ts + if ((operator === SyntaxKind.PlusPlusToken || operator === SyntaxKind.MinusMinusToken) && + isIdentifier(node.operand) && + !isGeneratedIdentifier(node.operand) && + !isLocalName(node.operand)) { + node.transformFlags |= TransformFlags.ContainsUpdateExpressionForIdentifier; + } return node; } @@ -2739,7 +2747,14 @@ namespace ts { const node = createBaseExpression(SyntaxKind.PostfixUnaryExpression); node.operator = operator; node.operand = parenthesizerRules().parenthesizeOperandOfPostfixUnary(operand); - node.transformFlags = propagateChildFlags(node.operand); + node.transformFlags |= propagateChildFlags(node.operand); + // Only set this flag for non-generated identifiers and non-"local" names. See the + // comment in `visitPreOrPostfixUnaryExpression` in module.ts + if (isIdentifier(node.operand) && + !isGeneratedIdentifier(node.operand) && + !isLocalName(node.operand)) { + node.transformFlags |= TransformFlags.ContainsUpdateExpressionForIdentifier; + } return node; } diff --git a/src/compiler/transformers/module/module.ts b/src/compiler/transformers/module/module.ts index 2a7440bd893..225939a9df8 100644 --- a/src/compiler/transformers/module/module.ts +++ b/src/compiler/transformers/module/module.ts @@ -37,8 +37,6 @@ namespace ts { context.enableSubstitution(SyntaxKind.TaggedTemplateExpression); // Substitute calls to imported/exported symbols to avoid incorrect `this`. context.enableSubstitution(SyntaxKind.Identifier); // Substitutes expression identifiers with imported/exported symbols. context.enableSubstitution(SyntaxKind.BinaryExpression); // Substitutes assignments to exported symbols. - context.enableSubstitution(SyntaxKind.PrefixUnaryExpression); // Substitutes updates to exported symbols. - context.enableSubstitution(SyntaxKind.PostfixUnaryExpression); // Substitutes updates to exported symbols. context.enableSubstitution(SyntaxKind.ShorthandPropertyAssignment); // Substitutes shorthand property assignments for imported/exported symbols. context.enableEmitNotification(SyntaxKind.SourceFile); // Restore state when substituting nodes in a file. @@ -47,7 +45,7 @@ namespace ts { let currentSourceFile: SourceFile; // The current file. let currentModuleInfo: ExternalModuleInfo; // The ExternalModuleInfo for the current file. - let noSubstitution: boolean[]; // Set of nodes for which substitution rules should be ignored. + const noSubstitution: boolean[] = []; // Set of nodes for which substitution rules should be ignored. let needUMDDynamicImportHelper: boolean; return chainBundle(context, transformSourceFile); @@ -96,7 +94,7 @@ namespace ts { const statements: Statement[] = []; const ensureUseStrict = getStrictOptionValue(compilerOptions, "alwaysStrict") || (!compilerOptions.noImplicitUseStrict && isExternalModule(currentSourceFile)); - const statementOffset = factory.copyPrologue(node.statements, statements, ensureUseStrict && !isJsonSourceFile(node), sourceElementVisitor); + const statementOffset = factory.copyPrologue(node.statements, statements, ensureUseStrict && !isJsonSourceFile(node), topLevelVisitor); if (shouldEmitUnderscoreUnderscoreESModule()) { append(statements, createUnderscoreUnderscoreESModule()); @@ -117,8 +115,8 @@ namespace ts { } } - append(statements, visitNode(currentModuleInfo.externalHelpersImportDeclaration, sourceElementVisitor, isStatement)); - addRange(statements, visitNodes(node.statements, sourceElementVisitor, isStatement, statementOffset)); + append(statements, visitNode(currentModuleInfo.externalHelpersImportDeclaration, topLevelVisitor, isStatement)); + addRange(statements, visitNodes(node.statements, topLevelVisitor, isStatement, statementOffset)); addExportEqualsIfNeeded(statements, /*emitAsReturn*/ false); insertStatementsAfterStandardPrologue(statements, endLexicalEnvironment()); @@ -430,7 +428,7 @@ namespace ts { startLexicalEnvironment(); const statements: Statement[] = []; - const statementOffset = factory.copyPrologue(node.statements, statements, /*ensureUseStrict*/ !compilerOptions.noImplicitUseStrict, sourceElementVisitor); + const statementOffset = factory.copyPrologue(node.statements, statements, /*ensureUseStrict*/ !compilerOptions.noImplicitUseStrict, topLevelVisitor); if (shouldEmitUnderscoreUnderscoreESModule()) { append(statements, createUnderscoreUnderscoreESModule()); @@ -440,11 +438,11 @@ namespace ts { } // Visit each statement of the module body. - append(statements, visitNode(currentModuleInfo.externalHelpersImportDeclaration, sourceElementVisitor, isStatement)); + append(statements, visitNode(currentModuleInfo.externalHelpersImportDeclaration, topLevelVisitor, isStatement)); if (moduleKind === ModuleKind.AMD) { addRange(statements, mapDefined(currentModuleInfo.externalImports, getAMDImportExpressionForImport)); } - addRange(statements, visitNodes(node.statements, sourceElementVisitor, isStatement, statementOffset)); + addRange(statements, visitNodes(node.statements, topLevelVisitor, isStatement, statementOffset)); // Append the 'export =' statement if provided. addExportEqualsIfNeeded(statements, /*emitAsReturn*/ true); @@ -471,7 +469,7 @@ namespace ts { */ function addExportEqualsIfNeeded(statements: Statement[], emitAsReturn: boolean) { if (currentModuleInfo.exportEquals) { - const expressionResult = visitNode(currentModuleInfo.exportEquals.expression, moduleExpressionElementVisitor); + const expressionResult = visitNode(currentModuleInfo.exportEquals.expression, visitor); if (expressionResult) { if (emitAsReturn) { const statement = factory.createReturnStatement(expressionResult); @@ -507,7 +505,7 @@ namespace ts { * * @param node The node to visit. */ - function sourceElementVisitor(node: Node): VisitResult { + function topLevelVisitor(node: Node): VisitResult { switch (node.kind) { case SyntaxKind.ImportDeclaration: return visitImportDeclaration(node as ImportDeclaration); @@ -537,26 +535,50 @@ namespace ts { return visitEndOfDeclarationMarker(node as EndOfDeclarationMarker); default: - return visitEachChild(node, moduleExpressionElementVisitor, context); + return visitor(node); } } - function moduleExpressionElementVisitor(node: Expression): VisitResult { - // This visitor does not need to descend into the tree if there is no dynamic import or destructuring assignment, + function visitorWorker(node: Node, valueIsDiscarded: boolean): VisitResult { + // This visitor does not need to descend into the tree if there is no dynamic import, destructuring assignment, or update expression // as export/import statements are only transformed at the top level of a file. - if (!(node.transformFlags & TransformFlags.ContainsDynamicImport) && !(node.transformFlags & TransformFlags.ContainsDestructuringAssignment)) { + if (!(node.transformFlags & (TransformFlags.ContainsDynamicImport | TransformFlags.ContainsDestructuringAssignment | TransformFlags.ContainsUpdateExpressionForIdentifier))) { return node; } - if (isImportCall(node)) { - return visitImportCallExpression(node); - } - else if (isDestructuringAssignment(node)) { - return visitDestructuringAssignment(node); - } - else { - return visitEachChild(node, moduleExpressionElementVisitor, context); + switch (node.kind) { + case SyntaxKind.ForStatement: + return visitForStatement(node as ForStatement); + case SyntaxKind.ExpressionStatement: + return visitExpressionStatement(node as ExpressionStatement); + case SyntaxKind.ParenthesizedExpression: + return visitParenthesizedExpression(node as ParenthesizedExpression, valueIsDiscarded); + case SyntaxKind.PartiallyEmittedExpression: + return visitPartiallyEmittedExpression(node as PartiallyEmittedExpression, valueIsDiscarded); + case SyntaxKind.CallExpression: + if (isImportCall(node)) { + return visitImportCallExpression(node); + } + break; + case SyntaxKind.BinaryExpression: + if (isDestructuringAssignment(node)) { + return visitDestructuringAssignment(node, valueIsDiscarded); + } + break; + case SyntaxKind.PrefixUnaryExpression: + case SyntaxKind.PostfixUnaryExpression: + return visitPreOrPostfixUnaryExpression(node as PrefixUnaryExpression | PostfixUnaryExpression, valueIsDiscarded); } + + return visitEachChild(node, visitor, context); + } + + function visitor(node: Node): VisitResult { + return visitorWorker(node, /*valueIsDiscarded*/ false); + } + + function discardedValueVisitor(node: Node): VisitResult { + return visitorWorker(node, /*valueIsDiscarded*/ true); } function destructuringNeedsFlattening(node: Expression): boolean { @@ -604,16 +626,91 @@ namespace ts { return false; } - function visitDestructuringAssignment(node: DestructuringAssignment): Expression { + function visitDestructuringAssignment(node: DestructuringAssignment, valueIsDiscarded: boolean): Expression { if (destructuringNeedsFlattening(node.left)) { - return flattenDestructuringAssignment(node, moduleExpressionElementVisitor, context, FlattenLevel.All, /*needsValue*/ false, createAllExportExpressions); + return flattenDestructuringAssignment(node, visitor, context, FlattenLevel.All, !valueIsDiscarded, createAllExportExpressions); } - return visitEachChild(node, moduleExpressionElementVisitor, context); + return visitEachChild(node, visitor, context); + } + + function visitForStatement(node: ForStatement) { + return factory.updateForStatement( + node, + visitNode(node.initializer, discardedValueVisitor, isForInitializer), + visitNode(node.condition, visitor, isExpression), + visitNode(node.incrementor, discardedValueVisitor, isExpression), + visitIterationBody(node.statement, visitor, context) + ); + } + + function visitExpressionStatement(node: ExpressionStatement) { + return factory.updateExpressionStatement( + node, + visitNode(node.expression, discardedValueVisitor, isExpression) + ); + } + + function visitParenthesizedExpression(node: ParenthesizedExpression, valueIsDiscarded: boolean) { + return factory.updateParenthesizedExpression(node, visitNode(node.expression, valueIsDiscarded ? discardedValueVisitor : visitor, isExpression)); + } + + function visitPartiallyEmittedExpression(node: PartiallyEmittedExpression, valueIsDiscarded: boolean) { + return factory.updatePartiallyEmittedExpression(node, visitNode(node.expression, valueIsDiscarded ? discardedValueVisitor : visitor, isExpression)); + } + + function visitPreOrPostfixUnaryExpression(node: PrefixUnaryExpression | PostfixUnaryExpression, valueIsDiscarded: boolean) { + // When we see a prefix or postfix increment expression whose operand is an exported + // symbol, we should ensure all exports of that symbol are updated with the correct + // value. + // + // - We do not transform generated identifiers for any reason. + // - We do not transform identifiers tagged with the LocalName flag. + // - We do not transform identifiers that were originally the name of an enum or + // namespace due to how they are transformed in TypeScript. + // - We only transform identifiers that are exported at the top level. + if ((node.operator === SyntaxKind.PlusPlusToken || node.operator === SyntaxKind.MinusMinusToken) + && isIdentifier(node.operand) + && !isGeneratedIdentifier(node.operand) + && !isLocalName(node.operand) + && !isDeclarationNameOfEnumOrNamespace(node.operand)) { + const exportedNames = getExports(node.operand); + if (exportedNames) { + let temp: Identifier | undefined; + let expression: Expression = visitNode(node.operand, visitor, isExpression); + if (isPrefixUnaryExpression(node)) { + expression = factory.updatePrefixUnaryExpression(node, expression); + } + else { + expression = factory.updatePostfixUnaryExpression(node, expression); + if (!valueIsDiscarded) { + temp = factory.createTempVariable(hoistVariableDeclaration); + expression = factory.createAssignment(temp, expression); + setTextRange(expression, node); + } + expression = factory.createComma(expression, factory.cloneNode(node.operand)); + setTextRange(expression, node); + } + + for (const exportName of exportedNames) { + noSubstitution[getNodeId(expression)] = true; + expression = createExportExpression(exportName, expression); + setTextRange(expression, node); + } + + if (temp) { + noSubstitution[getNodeId(expression)] = true; + expression = factory.createComma(expression, temp); + setTextRange(expression, node); + } + return expression; + } + } + return visitEachChild(node, visitor, context); } function visitImportCallExpression(node: ImportCall): Expression { const externalModuleName = getExternalModuleNameLiteral(factory, node, currentSourceFile, host, resolver, compilerOptions); - const firstArgument = visitNode(firstOrUndefined(node.arguments), moduleExpressionElementVisitor); + const firstArgument = visitNode(firstOrUndefined(node.arguments), visitor); // Only use the external module name if it differs from the first argument. This allows us to preserve the quote style of the argument on output. const argument = externalModuleName && (!firstArgument || !isStringLiteral(firstArgument) || firstArgument.text !== externalModuleName.text) ? externalModuleName : firstArgument; const containsLexicalThis = !!(node.transformFlags & TransformFlags.ContainsLexicalThis); @@ -1110,10 +1207,10 @@ namespace ts { if (original && hasAssociatedEndOfDeclarationMarker(original)) { // Defer exports until we encounter an EndOfDeclarationMarker node const id = getOriginalNodeId(node); - deferredExports[id] = appendExportStatement(deferredExports[id], factory.createIdentifier("default"), visitNode(node.expression, moduleExpressionElementVisitor), /*location*/ node, /*allowComments*/ true); + deferredExports[id] = appendExportStatement(deferredExports[id], factory.createIdentifier("default"), visitNode(node.expression, visitor), /*location*/ node, /*allowComments*/ true); } else { - statements = appendExportStatement(statements, factory.createIdentifier("default"), visitNode(node.expression, moduleExpressionElementVisitor), /*location*/ node, /*allowComments*/ true); + statements = appendExportStatement(statements, factory.createIdentifier("default"), visitNode(node.expression, visitor), /*location*/ node, /*allowComments*/ true); } return singleOrMany(statements); @@ -1136,9 +1233,9 @@ namespace ts { node.asteriskToken, factory.getDeclarationName(node, /*allowComments*/ true, /*allowSourceMaps*/ true), /*typeParameters*/ undefined, - visitNodes(node.parameters, moduleExpressionElementVisitor), + visitNodes(node.parameters, visitor), /*type*/ undefined, - visitEachChild(node.body, moduleExpressionElementVisitor, context) + visitEachChild(node.body, visitor, context) ), /*location*/ node ), @@ -1147,7 +1244,7 @@ namespace ts { ); } else { - statements = append(statements, visitEachChild(node, moduleExpressionElementVisitor, context)); + statements = append(statements, visitEachChild(node, visitor, context)); } if (hasAssociatedEndOfDeclarationMarker(node)) { @@ -1178,8 +1275,8 @@ namespace ts { visitNodes(node.modifiers, modifierVisitor, isModifier), factory.getDeclarationName(node, /*allowComments*/ true, /*allowSourceMaps*/ true), /*typeParameters*/ undefined, - visitNodes(node.heritageClauses, moduleExpressionElementVisitor), - visitNodes(node.members, moduleExpressionElementVisitor) + visitNodes(node.heritageClauses, visitor), + visitNodes(node.members, visitor) ), node ), @@ -1188,7 +1285,7 @@ namespace ts { ); } else { - statements = append(statements, visitEachChild(node, moduleExpressionElementVisitor, context)); + statements = append(statements, visitEachChild(node, visitor, context)); } if (hasAssociatedEndOfDeclarationMarker(node)) { @@ -1242,7 +1339,7 @@ namespace ts { variable.name, variable.exclamationToken, variable.type, - visitNode(variable.initializer, moduleExpressionElementVisitor) + visitNode(variable.initializer, visitor) ); variables = append(variables, updatedVariable); @@ -1268,7 +1365,7 @@ namespace ts { } } else { - statements = append(statements, visitEachChild(node, moduleExpressionElementVisitor, context)); + statements = append(statements, visitEachChild(node, visitor, context)); } if (hasAssociatedEndOfDeclarationMarker(node)) { @@ -1307,7 +1404,7 @@ namespace ts { function transformInitializedVariable(node: InitializedVariableDeclaration): Expression { if (isBindingPattern(node.name)) { return flattenDestructuringAssignment( - visitNode(node, moduleExpressionElementVisitor), + visitNode(node, visitor), /*visitor*/ undefined, context, FlattenLevel.All, @@ -1324,7 +1421,7 @@ namespace ts { ), /*location*/ node.name ), - node.initializer ? visitNode(node.initializer, moduleExpressionElementVisitor) : factory.createVoidZero() + node.initializer ? visitNode(node.initializer, visitor) : factory.createVoidZero() ); } } @@ -1674,13 +1771,11 @@ namespace ts { if (node.kind === SyntaxKind.SourceFile) { currentSourceFile = node as SourceFile; currentModuleInfo = moduleInfoMap[getOriginalNodeId(currentSourceFile)]; - noSubstitution = []; previousOnEmitNode(hint, node, emitCallback); currentSourceFile = undefined!; currentModuleInfo = undefined!; - noSubstitution = undefined!; } else { previousOnEmitNode(hint, node, emitCallback); @@ -1749,9 +1844,6 @@ namespace ts { return substituteTaggedTemplateExpression(node as TaggedTemplateExpression); case SyntaxKind.BinaryExpression: return substituteBinaryExpression(node as BinaryExpression); - case SyntaxKind.PostfixUnaryExpression: - case SyntaxKind.PrefixUnaryExpression: - return substituteUnaryExpression(node as PrefixUnaryExpression | PostfixUnaryExpression); } return node; @@ -1881,54 +1973,6 @@ namespace ts { return node; } - /** - * Substitution for a UnaryExpression that may contain an imported or exported symbol. - * - * @param node The node to substitute. - */ - function substituteUnaryExpression(node: PrefixUnaryExpression | PostfixUnaryExpression): Expression { - // When we see a prefix or postfix increment expression whose operand is an exported - // symbol, we should ensure all exports of that symbol are updated with the correct - // value. - // - // - We do not substitute generated identifiers for any reason. - // - We do not substitute identifiers tagged with the LocalName flag. - // - We do not substitute identifiers that were originally the name of an enum or - // namespace due to how they are transformed in TypeScript. - // - We only substitute identifiers that are exported at the top level. - if ((node.operator === SyntaxKind.PlusPlusToken || node.operator === SyntaxKind.MinusMinusToken) - && isIdentifier(node.operand) - && !isGeneratedIdentifier(node.operand) - && !isLocalName(node.operand) - && !isDeclarationNameOfEnumOrNamespace(node.operand)) { - const exportedNames = getExports(node.operand); - if (exportedNames) { - let expression: Expression = node.kind === SyntaxKind.PostfixUnaryExpression - ? setTextRange( - factory.createPrefixUnaryExpression( - node.operator, - node.operand - ), - /*location*/ node) - : node; - for (const exportName of exportedNames) { - // Mark the node to prevent triggering this rule again. - noSubstitution[getNodeId(expression)] = true; - expression = createExportExpression(exportName, expression); - } - if (node.kind === SyntaxKind.PostfixUnaryExpression) { - noSubstitution[getNodeId(expression)] = true; - expression = node.operator === SyntaxKind.PlusPlusToken - ? factory.createSubtract(expression, factory.createNumericLiteral(1)) - : factory.createAdd(expression, factory.createNumericLiteral(1)); - } - return expression; - } - } - - return node; - } - /** * Gets the additional exports of a name. * diff --git a/src/compiler/transformers/module/system.ts b/src/compiler/transformers/module/system.ts index 5d01e30be47..14216e5cab6 100644 --- a/src/compiler/transformers/module/system.ts +++ b/src/compiler/transformers/module/system.ts @@ -23,8 +23,6 @@ namespace ts { context.enableSubstitution(SyntaxKind.Identifier); // Substitutes expression identifiers for imported symbols. context.enableSubstitution(SyntaxKind.ShorthandPropertyAssignment); // Substitutes expression identifiers for imported symbols context.enableSubstitution(SyntaxKind.BinaryExpression); // Substitutes assignments to exported symbols. - context.enableSubstitution(SyntaxKind.PrefixUnaryExpression); // Substitutes updates to exported symbols. - context.enableSubstitution(SyntaxKind.PostfixUnaryExpression); // Substitutes updates to exported symbols. context.enableSubstitution(SyntaxKind.MetaProperty); // Substitutes 'import.meta' context.enableEmitNotification(SyntaxKind.SourceFile); // Restore state when substituting nodes in a file. @@ -225,7 +223,7 @@ namespace ts { // Add any prologue directives. const ensureUseStrict = getStrictOptionValue(compilerOptions, "alwaysStrict") || (!compilerOptions.noImplicitUseStrict && isExternalModule(currentSourceFile)); - const statementOffset = factory.copyPrologue(node.statements, statements, ensureUseStrict, sourceElementVisitor); + const statementOffset = factory.copyPrologue(node.statements, statements, ensureUseStrict, topLevelVisitor); // var __moduleName = context_1 && context_1.id; statements.push( @@ -246,14 +244,14 @@ namespace ts { ); // Visit the synthetic external helpers import declaration if present - visitNode(moduleInfo.externalHelpersImportDeclaration, sourceElementVisitor, isStatement); + visitNode(moduleInfo.externalHelpersImportDeclaration, topLevelVisitor, isStatement); // Visit the statements of the source file, emitting any transformations into // the `executeStatements` array. We do this *before* we fill the `setters` array // as we both emit transformations as well as aggregate some data used when creating // setters. This allows us to reduce the number of times we need to loop through the // statements of the source file. - const executeStatements = visitNodes(node.statements, sourceElementVisitor, isStatement, statementOffset); + const executeStatements = visitNodes(node.statements, topLevelVisitor, isStatement, statementOffset); // Emit early exports for function declarations. addRange(statements, hoistedStatements); @@ -566,7 +564,7 @@ namespace ts { * * @param node The node to visit. */ - function sourceElementVisitor(node: Node): VisitResult { + function topLevelVisitor(node: Node): VisitResult { switch (node.kind) { case SyntaxKind.ImportDeclaration: return visitImportDeclaration(node as ImportDeclaration); @@ -581,7 +579,7 @@ namespace ts { return visitExportAssignment(node as ExportAssignment); default: - return nestedElementVisitor(node); + return topLevelNestedVisitor(node); } } @@ -647,7 +645,7 @@ namespace ts { return undefined; } - const expression = visitNode(node.expression, destructuringAndImportCallVisitor, isExpression); + const expression = visitNode(node.expression, visitor, isExpression); const original = node.original; if (original && hasAssociatedEndOfDeclarationMarker(original)) { // Defer exports until we encounter an EndOfDeclarationMarker node @@ -674,12 +672,12 @@ namespace ts { node.asteriskToken, factory.getDeclarationName(node, /*allowComments*/ true, /*allowSourceMaps*/ true), /*typeParameters*/ undefined, - visitNodes(node.parameters, destructuringAndImportCallVisitor, isParameterDeclaration), + visitNodes(node.parameters, visitor, isParameterDeclaration), /*type*/ undefined, - visitNode(node.body, destructuringAndImportCallVisitor, isBlock))); + visitNode(node.body, visitor, isBlock))); } else { - hoistedStatements = append(hoistedStatements, visitEachChild(node, destructuringAndImportCallVisitor, context)); + hoistedStatements = append(hoistedStatements, visitEachChild(node, visitor, context)); } if (hasAssociatedEndOfDeclarationMarker(node)) { @@ -714,12 +712,12 @@ namespace ts { name, setTextRange( factory.createClassExpression( - visitNodes(node.decorators, destructuringAndImportCallVisitor, isDecorator), + visitNodes(node.decorators, visitor, isDecorator), /*modifiers*/ undefined, node.name, /*typeParameters*/ undefined, - visitNodes(node.heritageClauses, destructuringAndImportCallVisitor, isHeritageClause), - visitNodes(node.members, destructuringAndImportCallVisitor, isClassElement) + visitNodes(node.heritageClauses, visitor, isHeritageClause), + visitNodes(node.members, visitor, isClassElement) ), node ) @@ -749,7 +747,7 @@ namespace ts { */ function visitVariableStatement(node: VariableStatement): VisitResult { if (!shouldHoistVariableDeclarationList(node.declarationList)) { - return visitNode(node, destructuringAndImportCallVisitor, isStatement); + return visitNode(node, visitor, isStatement); } let expressions: Expression[] | undefined; @@ -822,13 +820,13 @@ namespace ts { return isBindingPattern(node.name) ? flattenDestructuringAssignment( node, - destructuringAndImportCallVisitor, + visitor, context, FlattenLevel.All, /*needsValue*/ false, createAssignment ) - : node.initializer ? createAssignment(node.name, visitNode(node.initializer, destructuringAndImportCallVisitor, isExpression)) : node.name; + : node.initializer ? createAssignment(node.name, visitNode(node.initializer, visitor, isExpression)) : node.name; } /** @@ -1153,7 +1151,7 @@ namespace ts { * * @param node The node to visit. */ - function nestedElementVisitor(node: Node): VisitResult { + function topLevelNestedVisitor(node: Node): VisitResult { switch (node.kind) { case SyntaxKind.VariableStatement: return visitVariableStatement(node as VariableStatement); @@ -1165,7 +1163,7 @@ namespace ts { return visitClassDeclaration(node as ClassDeclaration); case SyntaxKind.ForStatement: - return visitForStatement(node as ForStatement); + return visitForStatement(node as ForStatement, /*isTopLevel*/ true); case SyntaxKind.ForInStatement: return visitForInStatement(node as ForInStatement); @@ -1213,7 +1211,7 @@ namespace ts { return visitEndOfDeclarationMarker(node as EndOfDeclarationMarker); default: - return destructuringAndImportCallVisitor(node); + return visitor(node); } } @@ -1222,16 +1220,16 @@ namespace ts { * * @param node The node to visit. */ - function visitForStatement(node: ForStatement): VisitResult { + function visitForStatement(node: ForStatement, isTopLevel: boolean): VisitResult { const savedEnclosingBlockScopedContainer = enclosingBlockScopedContainer; enclosingBlockScopedContainer = node; node = factory.updateForStatement( node, - node.initializer && visitForInitializer(node.initializer), - visitNode(node.condition, destructuringAndImportCallVisitor, isExpression), - visitNode(node.incrementor, destructuringAndImportCallVisitor, isExpression), - visitIterationBody(node.statement, nestedElementVisitor, context) + visitNode(node.initializer, isTopLevel ? visitForInitializer : discardedValueVisitor, isForInitializer), + visitNode(node.condition, visitor, isExpression), + visitNode(node.incrementor, discardedValueVisitor, isExpression), + visitIterationBody(node.statement, isTopLevel ? topLevelNestedVisitor : visitor, context) ); enclosingBlockScopedContainer = savedEnclosingBlockScopedContainer; @@ -1250,8 +1248,8 @@ namespace ts { node = factory.updateForInStatement( node, visitForInitializer(node.initializer), - visitNode(node.expression, destructuringAndImportCallVisitor, isExpression), - visitIterationBody(node.statement, nestedElementVisitor, context) + visitNode(node.expression, visitor, isExpression), + visitIterationBody(node.statement, topLevelNestedVisitor, context) ); enclosingBlockScopedContainer = savedEnclosingBlockScopedContainer; @@ -1271,8 +1269,8 @@ namespace ts { node, node.awaitModifier, visitForInitializer(node.initializer), - visitNode(node.expression, destructuringAndImportCallVisitor, isExpression), - visitIterationBody(node.statement, nestedElementVisitor, context) + visitNode(node.expression, visitor, isExpression), + visitIterationBody(node.statement, topLevelNestedVisitor, context) ); enclosingBlockScopedContainer = savedEnclosingBlockScopedContainer; @@ -1308,7 +1306,7 @@ namespace ts { return expressions ? factory.inlineExpressions(expressions) : factory.createOmittedExpression(); } else { - return visitEachChild(node, nestedElementVisitor, context); + return visitNode(node, discardedValueVisitor, isExpression); } } @@ -1320,8 +1318,8 @@ namespace ts { function visitDoStatement(node: DoStatement): VisitResult { return factory.updateDoStatement( node, - visitIterationBody(node.statement, nestedElementVisitor, context), - visitNode(node.expression, destructuringAndImportCallVisitor, isExpression) + visitIterationBody(node.statement, topLevelNestedVisitor, context), + visitNode(node.expression, visitor, isExpression) ); } @@ -1333,8 +1331,8 @@ namespace ts { function visitWhileStatement(node: WhileStatement): VisitResult { return factory.updateWhileStatement( node, - visitNode(node.expression, destructuringAndImportCallVisitor, isExpression), - visitIterationBody(node.statement, nestedElementVisitor, context) + visitNode(node.expression, visitor, isExpression), + visitIterationBody(node.statement, topLevelNestedVisitor, context) ); } @@ -1347,7 +1345,7 @@ namespace ts { return factory.updateLabeledStatement( node, node.label, - visitNode(node.statement, nestedElementVisitor, isStatement, factory.liftToBlock) + visitNode(node.statement, topLevelNestedVisitor, isStatement, factory.liftToBlock) ); } @@ -1359,8 +1357,8 @@ namespace ts { function visitWithStatement(node: WithStatement): VisitResult { return factory.updateWithStatement( node, - visitNode(node.expression, destructuringAndImportCallVisitor, isExpression), - visitNode(node.statement, nestedElementVisitor, isStatement, factory.liftToBlock) + visitNode(node.expression, visitor, isExpression), + visitNode(node.statement, topLevelNestedVisitor, isStatement, factory.liftToBlock) ); } @@ -1372,8 +1370,8 @@ namespace ts { function visitSwitchStatement(node: SwitchStatement): VisitResult { return factory.updateSwitchStatement( node, - visitNode(node.expression, destructuringAndImportCallVisitor, isExpression), - visitNode(node.caseBlock, nestedElementVisitor, isCaseBlock) + visitNode(node.expression, visitor, isExpression), + visitNode(node.caseBlock, topLevelNestedVisitor, isCaseBlock) ); } @@ -1388,7 +1386,7 @@ namespace ts { node = factory.updateCaseBlock( node, - visitNodes(node.clauses, nestedElementVisitor, isCaseOrDefaultClause) + visitNodes(node.clauses, topLevelNestedVisitor, isCaseOrDefaultClause) ); enclosingBlockScopedContainer = savedEnclosingBlockScopedContainer; @@ -1403,8 +1401,8 @@ namespace ts { function visitCaseClause(node: CaseClause): VisitResult { return factory.updateCaseClause( node, - visitNode(node.expression, destructuringAndImportCallVisitor, isExpression), - visitNodes(node.statements, nestedElementVisitor, isStatement) + visitNode(node.expression, visitor, isExpression), + visitNodes(node.statements, topLevelNestedVisitor, isStatement) ); } @@ -1414,7 +1412,7 @@ namespace ts { * @param node The node to visit. */ function visitDefaultClause(node: DefaultClause): VisitResult { - return visitEachChild(node, nestedElementVisitor, context); + return visitEachChild(node, topLevelNestedVisitor, context); } /** @@ -1423,7 +1421,7 @@ namespace ts { * @param node The node to visit. */ function visitTryStatement(node: TryStatement): VisitResult { - return visitEachChild(node, nestedElementVisitor, context); + return visitEachChild(node, topLevelNestedVisitor, context); } /** @@ -1438,7 +1436,7 @@ namespace ts { node = factory.updateCatchClause( node, node.variableDeclaration, - visitNode(node.block, nestedElementVisitor, isBlock) + visitNode(node.block, topLevelNestedVisitor, isBlock) ); enclosingBlockScopedContainer = savedEnclosingBlockScopedContainer; @@ -1454,7 +1452,7 @@ namespace ts { const savedEnclosingBlockScopedContainer = enclosingBlockScopedContainer; enclosingBlockScopedContainer = node; - node = visitEachChild(node, nestedElementVisitor, context); + node = visitEachChild(node, topLevelNestedVisitor, context); enclosingBlockScopedContainer = savedEnclosingBlockScopedContainer; return node; @@ -1469,19 +1467,59 @@ namespace ts { * * @param node The node to visit. */ - function destructuringAndImportCallVisitor(node: Node): VisitResult { - if (isDestructuringAssignment(node)) { - return visitDestructuringAssignment(node); - } - else if (isImportCall(node)) { - return visitImportCallExpression(node); - } - else if ((node.transformFlags & TransformFlags.ContainsDestructuringAssignment) || (node.transformFlags & TransformFlags.ContainsDynamicImport)) { - return visitEachChild(node, destructuringAndImportCallVisitor, context); - } - else { + function visitorWorker(node: Node, valueIsDiscarded: boolean): VisitResult { + if (!(node.transformFlags & (TransformFlags.ContainsDestructuringAssignment | TransformFlags.ContainsDynamicImport | TransformFlags.ContainsUpdateExpressionForIdentifier))) { return node; } + switch (node.kind) { + case SyntaxKind.ForStatement: + return visitForStatement(node as ForStatement, /*isTopLevel*/ false); + case SyntaxKind.ExpressionStatement: + return visitExpressionStatement(node as ExpressionStatement); + case SyntaxKind.ParenthesizedExpression: + return visitParenthesizedExpression(node as ParenthesizedExpression, valueIsDiscarded); + case SyntaxKind.PartiallyEmittedExpression: + return visitPartiallyEmittedExpression(node as PartiallyEmittedExpression, valueIsDiscarded); + case SyntaxKind.BinaryExpression: + if (isDestructuringAssignment(node)) { + return visitDestructuringAssignment(node, valueIsDiscarded); + } + break; + case SyntaxKind.CallExpression: + if (isImportCall(node)) { + return visitImportCallExpression(node); + } + break; + case SyntaxKind.PrefixUnaryExpression: + case SyntaxKind.PostfixUnaryExpression: + return visitPrefixOrPostfixUnaryExpression(node as PrefixUnaryExpression | PostfixUnaryExpression, valueIsDiscarded); + } + return visitEachChild(node, visitor, context); + } + + /** + * Visit nodes to flatten destructuring assignments to exported symbols. + * + * @param node The node to visit. + */ + function visitor(node: Node): VisitResult { + return visitorWorker(node, /*valueIsDiscarded*/ false); + } + + function discardedValueVisitor(node: Node): VisitResult { + return visitorWorker(node, /*valueIsDiscarded*/ true); + } + + function visitExpressionStatement(node: ExpressionStatement) { + return factory.updateExpressionStatement(node, visitNode(node.expression, discardedValueVisitor, isExpression)); + } + + function visitParenthesizedExpression(node: ParenthesizedExpression, valueIsDiscarded: boolean) { + return factory.updateParenthesizedExpression(node, visitNode(node.expression, valueIsDiscarded ? discardedValueVisitor : visitor, isExpression)); + } + + function visitPartiallyEmittedExpression(node: PartiallyEmittedExpression, valueIsDiscarded: boolean) { + return factory.updatePartiallyEmittedExpression(node, visitNode(node.expression, valueIsDiscarded ? discardedValueVisitor : visitor, isExpression)); } function visitImportCallExpression(node: ImportCall): Expression { @@ -1496,7 +1534,7 @@ namespace ts { // }; // }); const externalModuleName = getExternalModuleNameLiteral(factory, node, currentSourceFile, host, resolver, compilerOptions); - const firstArgument = visitNode(firstOrUndefined(node.arguments), destructuringAndImportCallVisitor); + const firstArgument = visitNode(firstOrUndefined(node.arguments), visitor); // Only use the external module name if it differs from the first argument. This allows us to preserve the quote style of the argument on output. const argument = externalModuleName && (!firstArgument || !isStringLiteral(firstArgument) || firstArgument.text !== externalModuleName.text) ? externalModuleName : firstArgument; return factory.createCallExpression( @@ -1514,18 +1552,18 @@ namespace ts { * * @param node The node to visit. */ - function visitDestructuringAssignment(node: DestructuringAssignment): VisitResult { + function visitDestructuringAssignment(node: DestructuringAssignment, valueIsDiscarded: boolean): VisitResult { if (hasExportedReferenceInDestructuringTarget(node.left)) { return flattenDestructuringAssignment( node, - destructuringAndImportCallVisitor, + visitor, context, FlattenLevel.All, - /*needsValue*/ true + !valueIsDiscarded ); } - return visitEachChild(node, destructuringAndImportCallVisitor, context); + return visitEachChild(node, visitor, context); } /** @@ -1561,6 +1599,54 @@ namespace ts { } } + function visitPrefixOrPostfixUnaryExpression(node: PrefixUnaryExpression | PostfixUnaryExpression, valueIsDiscarded: boolean) { + // When we see a prefix or postfix increment expression whose operand is an exported + // symbol, we should ensure all exports of that symbol are updated with the correct + // value. + // + // - We do not transform generated identifiers for any reason. + // - We do not transform identifiers tagged with the LocalName flag. + // - We do not transform identifiers that were originally the name of an enum or + // namespace due to how they are transformed in TypeScript. + // - We only transform identifiers that are exported at the top level. + if ((node.operator === SyntaxKind.PlusPlusToken || node.operator === SyntaxKind.MinusMinusToken) + && isIdentifier(node.operand) + && !isGeneratedIdentifier(node.operand) + && !isLocalName(node.operand) + && !isDeclarationNameOfEnumOrNamespace(node.operand)) { + const exportedNames = getExports(node.operand); + if (exportedNames) { + let temp: Identifier | undefined; + let expression: Expression = visitNode(node.operand, visitor, isExpression); + if (isPrefixUnaryExpression(node)) { + expression = factory.updatePrefixUnaryExpression(node, expression); + } + else { + expression = factory.updatePostfixUnaryExpression(node, expression); + if (!valueIsDiscarded) { + temp = factory.createTempVariable(hoistVariableDeclaration); + expression = factory.createAssignment(temp, expression); + setTextRange(expression, node); + } + expression = factory.createComma(expression, factory.cloneNode(node.operand)); + setTextRange(expression, node); + } + + for (const exportName of exportedNames) { + expression = createExportExpression(exportName, preventSubstitution(expression)); + } + + if (temp) { + expression = factory.createComma(expression, temp); + setTextRange(expression, node); + } + + return expression; + } + } + return visitEachChild(node, visitor, context); + } + // // Modifier Visitors // @@ -1705,9 +1791,6 @@ namespace ts { return substituteExpressionIdentifier(node as Identifier); case SyntaxKind.BinaryExpression: return substituteBinaryExpression(node as BinaryExpression); - case SyntaxKind.PrefixUnaryExpression: - case SyntaxKind.PostfixUnaryExpression: - return substituteUnaryExpression(node as PrefixUnaryExpression | PostfixUnaryExpression); case SyntaxKind.MetaProperty: return substituteMetaProperty(node as MetaProperty); } @@ -1797,55 +1880,6 @@ namespace ts { return node; } - /** - * Substitution for a UnaryExpression that may contain an imported or exported symbol. - * - * @param node The node to substitute. - */ - function substituteUnaryExpression(node: PrefixUnaryExpression | PostfixUnaryExpression): Expression { - // When we see a prefix or postfix increment expression whose operand is an exported - // symbol, we should ensure all exports of that symbol are updated with the correct - // value. - // - // - We do not substitute generated identifiers for any reason. - // - We do not substitute identifiers tagged with the LocalName flag. - // - We do not substitute identifiers that were originally the name of an enum or - // namespace due to how they are transformed in TypeScript. - // - We only substitute identifiers that are exported at the top level. - if ((node.operator === SyntaxKind.PlusPlusToken || node.operator === SyntaxKind.MinusMinusToken) - && isIdentifier(node.operand) - && !isGeneratedIdentifier(node.operand) - && !isLocalName(node.operand) - && !isDeclarationNameOfEnumOrNamespace(node.operand)) { - const exportedNames = getExports(node.operand); - if (exportedNames) { - let expression: Expression = node.kind === SyntaxKind.PostfixUnaryExpression - ? setTextRange( - factory.createPrefixUnaryExpression( - node.operator, - node.operand - ), - node - ) - : node; - - for (const exportName of exportedNames) { - expression = createExportExpression(exportName, preventSubstitution(expression)); - } - - if (node.kind === SyntaxKind.PostfixUnaryExpression) { - expression = node.operator === SyntaxKind.PlusPlusToken - ? factory.createSubtract(preventSubstitution(expression), factory.createNumericLiteral(1)) - : factory.createAdd(preventSubstitution(expression), factory.createNumericLiteral(1)); - } - - return expression; - } - } - - return node; - } - function substituteMetaProperty(node: MetaProperty) { if (isImportMeta(node)) { return factory.createPropertyAccessExpression(contextObject, factory.createIdentifier("meta")); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index b82a47e7e64..cea584d3e98 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6640,6 +6640,7 @@ namespace ts { ContainsClassFields = 1 << 23, ContainsPossibleTopLevelAwait = 1 << 24, ContainsLexicalSuper = 1 << 25, + ContainsUpdateExpressionForIdentifier = 1 << 26, // Please leave this as 1 << 29. // It is the maximum bit we can set before we outgrow the size of a v8 small integer (SMI) on an x86 system. // It is a good reminder of how much room we have left diff --git a/src/harness/evaluatorImpl.ts b/src/harness/evaluatorImpl.ts index 451f68e7cb4..8bd849ee7c4 100644 --- a/src/harness/evaluatorImpl.ts +++ b/src/harness/evaluatorImpl.ts @@ -36,98 +36,424 @@ namespace evaluator { const output = result.getOutput(source.main, "js")!; assert.isDefined(output); - globals = { Symbol: FakeSymbol, ...globals }; - return createLoader(fs, globals)(output.file); - } - - function createLoader(fs: vfs.FileSystem, globals: Record) { - interface Module { - exports: any; - } - - const moduleCache = new ts.Map(); - return load; - - function evaluate(text: string, file: string, module: Module) { - const globalNames: string[] = []; - const globalArgs: any[] = []; - for (const name in globals) { - if (ts.hasProperty(globals, name)) { - globalNames.push(name); - globalArgs.push(globals[name]); - } - } - const base = vpath.dirname(file); - const localRequire = (id: string) => requireModule(id, base); - const evaluateText = `(function (module, exports, require, __dirname, __filename, ${globalNames.join(", ")}) { ${text} })`; - // eslint-disable-next-line no-eval - const evaluateThunk = (void 0, eval)(evaluateText) as (module: any, exports: any, require: (id: string) => any, dirname: string, filename: string, ...globalArgs: any[]) => void; - evaluateThunk.call(globals, module, module.exports, localRequire, vpath.dirname(file), file, FakeSymbol, ...globalArgs); - } - - function loadModule(file: string): Module { - if (!ts.isExternalModuleNameRelative(file)) throw new Error(`Module '${file}' could not be found.`); - let module = moduleCache.get(file); - if (module) return module; - moduleCache.set(file, module = { exports: {} }); - try { - const sourceText = fs.readFileSync(file, "utf8"); - evaluate(sourceText, file, module); - return module; - } - catch (e) { - moduleCache.delete(file); - throw e; - } - } - - function isFile(file: string) { - return fs.existsSync(file) && fs.statSync(file).isFile(); - } - - function loadAsFile(file: string): Module | undefined { - if (isFile(file)) return loadModule(file); - if (isFile(file + ".js")) return loadModule(file + ".js"); - return undefined; - } - - function loadIndex(dir: string): Module | undefined { - const indexFile = vpath.resolve(dir, "index.js"); - if (isFile(indexFile)) return loadModule(indexFile); - return undefined; - } - - function loadAsDirectory(dir: string): Module | undefined { - const packageFile = vpath.resolve(dir, "package.json"); - if (isFile(packageFile)) { - const text = fs.readFileSync(packageFile, "utf8"); - const json = JSON.parse(text); - if (json.main) { - const main = vpath.resolve(dir, json.main); - const result = loadAsFile(main) || loadIndex(main); - if (result === undefined) throw new Error("Module not found"); - } - } - return loadIndex(dir); - } - - function requireModule(id: string, base: string) { - if (!ts.isExternalModuleNameRelative(id)) throw new Error(`Module '${id}' could not be found.`); - const file = vpath.resolve(base, id); - const module = loadAsFile(file) || loadAsDirectory(file); - if (!module) throw new Error(`Module '${id}' could not be found.`); - return module.exports; - } - - function load(file: string) { - return requireModule(file, fs.cwd()); - } + const loader = getLoader(compilerOptions, fs, globals); + return loader.import(output.file); } export function evaluateJavaScript(sourceText: string, globals?: Record, sourceFile = sourceFileJs) { globals = { Symbol: FakeSymbol, ...globals }; const fs = new vfs.FileSystem(/*ignoreCase*/ false, { files: { [sourceFile]: sourceText } }); - return createLoader(fs, globals)(sourceFile); + return new CommonJsLoader(fs, globals).import(sourceFile); + } + + function getLoader(compilerOptions: ts.CompilerOptions, fs: vfs.FileSystem, globals: Record): Loader { + const moduleKind = ts.getEmitModuleKind(compilerOptions); + switch (moduleKind) { + case ts.ModuleKind.UMD: + case ts.ModuleKind.CommonJS: + return new CommonJsLoader(fs, globals); + case ts.ModuleKind.System: + return new SystemLoader(fs, globals); + case ts.ModuleKind.AMD: + case ts.ModuleKind.None: + default: + throw new Error(`ModuleKind '${ts.ModuleKind[moduleKind]}' not supported by evaluator.`); + } + } + + abstract class Loader { + protected readonly fs: vfs.FileSystem; + protected readonly globals: Record; + + private moduleCache = new ts.Map(); + + constructor(fs: vfs.FileSystem, globals: Record) { + this.fs = fs; + this.globals = globals; + } + + protected isFile(file: string) { + return this.fs.existsSync(file) && this.fs.statSync(file).isFile(); + } + + protected abstract evaluate(text: string, file: string, module: TModule): void; + protected abstract createModule(file: string): TModule; + protected abstract getExports(module: TModule): any; + + protected load(file: string): TModule { + if (!ts.isExternalModuleNameRelative(file)) throw new Error(`Module '${file}' could not be found.`); + let module = this.moduleCache.get(file); + if (module) return module; + this.moduleCache.set(file, module = this.createModule(file)); + try { + const sourceText = this.fs.readFileSync(file, "utf8"); + this.evaluate(sourceText, file, module); + return module; + } + catch (e) { + this.moduleCache.delete(file); + throw e; + } + } + + protected resolve(id: string, base: string) { + return vpath.resolve(base, id); + } + + import(id: string, base = this.fs.cwd()) { + if (!ts.isExternalModuleNameRelative(id)) throw new Error(`Module '${id}' could not be found.`); + const file = this.resolve(id, base); + const module = this.load(file); + if (!module) throw new Error(`Module '${id}' could not be found.`); + return this.getExports(module); + } + } + + interface CommonJSModule { + exports: any; + } + + class CommonJsLoader extends Loader { + private resolveAsFile(file: string) { + if (this.isFile(file)) return file; + if (this.isFile(file + ".js")) return file + ".js"; + return undefined; + } + + private resolveIndex(dir: string) { + const indexFile = vpath.resolve(dir, "index.js"); + if (this.isFile(indexFile)) return indexFile; + return undefined; + } + + private resolveAsDirectory(dir: string) { + const packageFile = vpath.resolve(dir, "package.json"); + if (this.isFile(packageFile)) { + const text = this.fs.readFileSync(packageFile, "utf8"); + const json = JSON.parse(text); + if (json.main) { + const main = vpath.resolve(dir, json.main); + const result = this.resolveAsFile(main) || this.resolveIndex(main); + if (result === undefined) throw new Error("Module not found"); + } + } + return this.resolveIndex(dir); + } + + protected resolve(id: string, base: string) { + const file = vpath.resolve(base, id); + const resolved = this.resolveAsFile(file) || this.resolveAsDirectory(file); + if (!resolved) throw new Error(`Module '${id}' could not be found.`); + return resolved; + } + + protected createModule(): CommonJSModule { + return { exports: {} }; + } + + protected getExports(module: CommonJSModule) { + return module.exports; + } + + protected evaluate(text: string, file: string, module: CommonJSModule): void { + const globalNames: string[] = []; + const globalArgs: any[] = []; + for (const name in this.globals) { + if (ts.hasProperty(this.globals, name)) { + globalNames.push(name); + globalArgs.push(this.globals[name]); + } + } + const base = vpath.dirname(file); + const localRequire = (id: string) => this.import(id, base); + const evaluateText = `(function (module, exports, require, __dirname, __filename, ${globalNames.join(", ")}) { ${text} })`; + // eslint-disable-next-line no-eval + const evaluateThunk = (void 0, eval)(evaluateText) as (module: any, exports: any, require: (id: string) => any, dirname: string, filename: string, ...globalArgs: any[]) => void; + evaluateThunk.call(this.globals, module, module.exports, localRequire, vpath.dirname(file), file, ...globalArgs); + } + } + + interface SystemModule { + file: string; + exports: any; + hasExports: boolean; + state: SystemModuleState; + dependencies: SystemModule[]; + dependers: SystemModule[]; + setters: SystemModuleDependencySetter[]; + requestedDependencies?: string[]; + declaration?: SystemModuleDeclaration; + hasError?: boolean; + error?: any; + } + + const enum SystemModuleState { + // Instantiation phases: + Uninstantiated, + Instantiated, + + // Linker phases: + AddingDependencies, + AllDependenciesAdded, + AllDependenciesInstantiated, + WiringSetters, + Linked, + + // Evaluation phases: + Evaluating, + Ready, + } + + interface SystemModuleExporter { + (name: string, value: T): T; + (values: T): T; + } + + interface SystemModuleContext { + import: (id: string) => Promise; + meta: any; + } + + type SystemModuleRegisterCallback = (exporter: SystemModuleExporter, context: SystemModuleContext) => SystemModuleDeclaration; + type SystemModuleDependencySetter = (dependency: any) => void; + + interface SystemModuleDeclaration { + setters: SystemModuleDependencySetter[]; + execute: () => void; + } + + interface SystemGlobal { + register(dependencies: string[], declare: SystemModuleRegisterCallback): void; + } + + class SystemLoader extends Loader { + protected createModule(file: string): SystemModule { + return { + file, + // eslint-disable-next-line no-null/no-null + exports: Object.create(/*o*/ null), + dependencies: [], + dependers: [], + setters: [], + hasExports: false, + state: SystemModuleState.Uninstantiated + }; + } + + protected getExports(module: SystemModule) { + if (module.state < SystemModuleState.Ready) { + this.resetDependers(module, []); + this.evaluateModule(module, []); + if (module.state < SystemModuleState.Ready) { + const error = new Error("Module graph could not be loaded"); + this.handleError(module, error); + throw error; + } + } + if (module.hasError) { + throw module.error; + } + return module.exports; + } + + private handleError(module: SystemModule, error: any) { + if (!module.hasError) { + module.hasError = true; + module.error = error; + module.state = SystemModuleState.Ready; + } + } + + protected evaluate(text: string, _file: string, module: SystemModule): void { + const globalNames: string[] = []; + const globalArgs: any[] = []; + for (const name in this.globals) { + if (ts.hasProperty(this.globals, name)) { + globalNames.push(name); + globalArgs.push(this.globals[name]); + } + } + const localSystem: SystemGlobal = { + register: (dependencies, declare) => this.instantiateModule(module, dependencies, declare) + }; + const evaluateText = `(function (System, ${globalNames.join(", ")}) { ${text} })`; + try { + // eslint-disable-next-line no-eval + const evaluateThunk = (void 0, eval)(evaluateText) as (System: any, ...globalArgs: any[]) => void; + evaluateThunk.call(this.globals, localSystem, ...globalArgs); + } + catch (e) { + this.handleError(module, e); + throw e; + } + } + + private instantiateModule(module: SystemModule, dependencies: string[], registration?: SystemModuleRegisterCallback) { + function exporter(name: string, value: T): T; + function exporter(value: T): T; + function exporter(...args: [string, T] | [T]) { + module.hasExports = true; + const name = args.length === 1 ? undefined : args[0]; + const value = args.length === 1 ? args[0] : args[1]; + if (name !== undefined) { + module.exports[name] = value; + } + else { + for (const name in value) { + module.exports[name] = value[name]; + } + } + for (const setter of module.setters) { + setter(module.exports); + } + return value; + } + + const context: SystemModuleContext = { + import: (_id) => { throw new Error("Dynamic import not implemented."); }, + meta: { + url: ts.isUrl(module.file) ? module.file : `file:///${ts.normalizeSlashes(module.file).replace(/^\//, "").split("/").map(encodeURIComponent).join("/")}` + } + }; + + module.requestedDependencies = dependencies; + + try { + module.declaration = registration?.(exporter, context); + module.state = SystemModuleState.Instantiated; + + for (const depender of module.dependers) { + this.linkModule(depender); + } + + this.linkModule(module); + } + catch (e) { + this.handleError(module, e); + throw e; + } + } + + private linkModule(module: SystemModule) { + try { + for (;;) { + switch (module.state) { + case SystemModuleState.Uninstantiated: { + throw new Error("Module not yet instantiated"); + } + case SystemModuleState.Instantiated: { + // Module has been instantiated, start requesting dependencies. + // Set state so that re-entry while adding dependencies does nothing. + module.state = SystemModuleState.AddingDependencies; + const base = vpath.dirname(module.file); + const dependencies = module.requestedDependencies || []; + + for (const dependencyId of dependencies) { + const dependency = this.load(this.resolve(dependencyId, base)); + module.dependencies.push(dependency); + dependency.dependers.push(module); + } + + // All dependencies have been added, switch state + // to check whether all dependencies are instantiated + module.state = SystemModuleState.AllDependenciesAdded; + continue; + } + case SystemModuleState.AddingDependencies: { + // in the middle of adding dependencies for this module, do nothing + return; + } + case SystemModuleState.AllDependenciesAdded: { + // all dependencies have been added, advance state if all dependencies are instantiated. + for (const dependency of module.dependencies) { + if (dependency.state === SystemModuleState.Uninstantiated) { + return; + } + } + + // indicate all dependencies are instantiated for this module. + module.state = SystemModuleState.AllDependenciesInstantiated; + + // trigger links for dependers of this module. + for (const depender of module.dependers) { + this.linkModule(depender); + } + continue; + } + case SystemModuleState.AllDependenciesInstantiated: { + // all dependencies have been instantiated, start wiring setters + module.state = SystemModuleState.WiringSetters; + for (let i = 0; i < module.dependencies.length; i++) { + const dependency = module.dependencies[i]; + const setter = module.declaration?.setters[i]; + if (setter) { + dependency.setters.push(setter); + if (dependency.hasExports || dependency.state === SystemModuleState.Ready) { + // wire hoisted exports or ready dependencies. + setter(dependency.exports); + } + } + } + + module.state = SystemModuleState.Linked; + + // ensure graph is fully linked + for (const depender of module.dependers) { + this.linkModule(depender); + } + continue; + } + + case SystemModuleState.WiringSetters: // in the middle of wiring setters for this module, nothing to do + case SystemModuleState.Linked: // module has already been linked, nothing to do + case SystemModuleState.Evaluating: // module is currently evaluating, nothing to do + case SystemModuleState.Ready: // module is done evaluating, nothing to do + return; + } + } + } + catch (e) { + this.handleError(module, e); + throw e; + } + } + + private resetDependers(module: SystemModule, stack: SystemModule[]) { + if (stack.lastIndexOf(module) !== -1) { + return; + } + + stack.push(module); + module.dependers.length = 0; + for (const dependency of module.dependencies) { + this.resetDependers(dependency, stack); + } + stack.pop(); + } + + private evaluateModule(module: SystemModule, stack: SystemModule[]) { + if (module.state < SystemModuleState.Linked) throw new Error("Invalid state for evaluation."); + if (module.state !== SystemModuleState.Linked) return; + + if (stack.lastIndexOf(module) !== -1) { + // we are already evaluating this module + return; + } + + stack.push(module); + module.state = SystemModuleState.Evaluating; + try { + for (const dependency of module.dependencies) { + this.evaluateModule(dependency, stack); + } + module.declaration?.execute?.(); + module.state = SystemModuleState.Ready; + } + catch (e) { + this.handleError(module, e); + throw e; + } + } } } diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index dfda1f40b0b..e4274fabe1b 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -99,6 +99,7 @@ "unittests/evaluation/optionalCall.ts", "unittests/evaluation/objectRest.ts", "unittests/evaluation/superInStaticInitializer.ts", + "unittests/evaluation/updateExpressionInModule.ts", "unittests/services/cancellableLanguageServiceOperations.ts", "unittests/services/colorization.ts", "unittests/services/convertToAsyncFunction.ts", diff --git a/src/testRunner/unittests/evaluation/updateExpressionInModule.ts b/src/testRunner/unittests/evaluation/updateExpressionInModule.ts new file mode 100644 index 00000000000..443a1fb71a1 --- /dev/null +++ b/src/testRunner/unittests/evaluation/updateExpressionInModule.ts @@ -0,0 +1,125 @@ +describe("unittests:: evaluation:: updateExpressionInModule", () => { + // only run BigInt tests if BigInt is supported in the host environment + const itIfBigInt = typeof BigInt === "function" ? it : it.skip; + + it("pre-increment in commonjs using Number", () => { + const result = evaluator.evaluateTypeScript({ + files: { + "/.src/main.ts": ` + let a = 1; + export { a }; + export const b = ++a; + ` + }, + rootFiles: ["/.src/main.ts"], + main: "/.src/main.ts" + }, { module: ts.ModuleKind.CommonJS }); + assert.equal(result.a, 2); + assert.equal(result.b, 2); + }); + it("pre-increment in System using Number", () => { + const result = evaluator.evaluateTypeScript({ + files: { + "/.src/main.ts": ` + let a = 1; + export { a }; + export const b = ++a; + ` + }, + rootFiles: ["/.src/main.ts"], + main: "/.src/main.ts" + }, { module: ts.ModuleKind.System }); + assert.equal(result.a, 2); + assert.equal(result.b, 2); + }); + itIfBigInt("pre-increment in commonjs using BigInt", () => { + const result = evaluator.evaluateTypeScript({ + files: { + "/.src/main.ts": ` + let a = BigInt(1); + export { a }; + export const b = ++a; + ` + }, + rootFiles: ["/.src/main.ts"], + main: "/.src/main.ts" + }, { module: ts.ModuleKind.CommonJS }, { BigInt }); + assert.equal(result.a, BigInt(2)); + assert.equal(result.b, BigInt(2)); + }); + itIfBigInt("pre-increment in System using BigInt", () => { + const result = evaluator.evaluateTypeScript({ + files: { + "/.src/main.ts": ` + let a = BigInt(1); + export { a }; + export const b = ++a; + ` + }, + rootFiles: ["/.src/main.ts"], + main: "/.src/main.ts" + }, { module: ts.ModuleKind.System }, { BigInt }); + assert.equal(result.a, BigInt(2)); + assert.equal(result.b, BigInt(2)); + }); + it("post-increment in commonjs using Number", () => { + const result = evaluator.evaluateTypeScript({ + files: { + "/.src/main.ts": ` + let a = 1; + export { a }; + export const b = a++; + ` + }, + rootFiles: ["/.src/main.ts"], + main: "/.src/main.ts" + }, { module: ts.ModuleKind.CommonJS }); + assert.equal(result.a, 2); + assert.equal(result.b, 1); + }); + it("post-increment in System using Number", () => { + const result = evaluator.evaluateTypeScript({ + files: { + "/.src/main.ts": ` + let a = 1; + export { a }; + export const b = a++; + ` + }, + rootFiles: ["/.src/main.ts"], + main: "/.src/main.ts" + }, { module: ts.ModuleKind.System }); + assert.equal(result.a, 2); + assert.equal(result.b, 1); + }); + itIfBigInt("post-increment in commonjs using BigInt", () => { + const result = evaluator.evaluateTypeScript({ + files: { + "/.src/main.ts": ` + let a = BigInt(1); + export { a }; + export const b = a++; + ` + }, + rootFiles: ["/.src/main.ts"], + main: "/.src/main.ts" + }, { module: ts.ModuleKind.CommonJS }, { BigInt }); + assert.equal(result.a, BigInt(2)); + assert.equal(result.b, BigInt(1)); + }); + itIfBigInt("post-increment in System using BigInt", () => { + const result = evaluator.evaluateTypeScript({ + files: { + "/.src/main.ts": ` + let a = BigInt(1); + export { a }; + export const b = a++; + ` + }, + rootFiles: ["/.src/main.ts"], + main: "/.src/main.ts" + }, { module: ts.ModuleKind.System }, { BigInt }); + assert.equal(result.a, BigInt(2)); + assert.equal(result.b, BigInt(1)); + }); +}); \ No newline at end of file diff --git a/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.js b/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.js index b57fce233b6..6ce67f8a45e 100644 --- a/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.js +++ b/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.js @@ -9,7 +9,7 @@ var buzz = 10; buzz += 3; var bizz = 8; -bizz++; // compiles to exports.bizz = bizz += 1 +bizz++; // compiles to exports.bizz = (bizz++, bizz) bizz--; // similarly ++bizz; // compiles to exports.bizz = ++bizz @@ -32,6 +32,6 @@ exports.buzz = buzz; exports.buzz = buzz += 3; var bizz = 8; exports.bizz = bizz; -(exports.bizz = ++bizz) - 1; // compiles to exports.bizz = bizz += 1 -(exports.bizz = --bizz) + 1; // similarly +exports.bizz = (bizz++, bizz); // compiles to exports.bizz = (bizz++, bizz) +exports.bizz = (bizz--, bizz); // similarly exports.bizz = ++bizz; // compiles to exports.bizz = ++bizz diff --git a/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.symbols b/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.symbols index f6991e8d095..a7731889122 100644 --- a/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.symbols +++ b/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.symbols @@ -20,7 +20,7 @@ buzz += 3; var bizz = 8; >bizz : Symbol(bizz, Decl(server.ts, 9, 3)) -bizz++; // compiles to exports.bizz = bizz += 1 +bizz++; // compiles to exports.bizz = (bizz++, bizz) >bizz : Symbol(bizz, Decl(server.ts, 9, 3)) bizz--; // similarly diff --git a/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.types b/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.types index ece6e7b90a8..7a5dd9fafc4 100644 --- a/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.types +++ b/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.types @@ -30,7 +30,7 @@ var bizz = 8; >bizz : number >8 : 8 -bizz++; // compiles to exports.bizz = bizz += 1 +bizz++; // compiles to exports.bizz = (bizz++, bizz) >bizz++ : number >bizz : number diff --git a/tests/baselines/reference/moduleExportsUnaryExpression.js b/tests/baselines/reference/moduleExportsUnaryExpression.js index cb11efbd375..d752d996c26 100644 --- a/tests/baselines/reference/moduleExportsUnaryExpression.js +++ b/tests/baselines/reference/moduleExportsUnaryExpression.js @@ -18,21 +18,22 @@ export { x }; //// [moduleExportsUnaryExpression.js] "use strict"; +var _a, _b, _c, _d; exports.__esModule = true; exports.x = exports.foo = void 0; var x = 1; exports.x = x; function foo(y) { - if (y <= (exports.x = ++x) - 1) - return y <= (exports.x = ++x) - 1; - if (y <= (exports.x = --x) + 1) - return y <= (exports.x = --x) + 1; + if (y <= (exports.x = (_a = x++, x), _a)) + return y <= (exports.x = (_b = x++, x), _b); + if (y <= (exports.x = (_c = x--, x), _c)) + return y <= (exports.x = (_d = x--, x), _d); if (y <= (exports.x = ++x)) return y <= (exports.x = ++x); if (y <= (exports.x = --x)) return y <= (exports.x = --x); - (exports.x = ++x) - 1; - (exports.x = --x) + 1; + exports.x = (x++, x); + exports.x = (x--, x); exports.x = ++x; exports.x = --x; } diff --git a/tests/baselines/reference/systemModule8.js b/tests/baselines/reference/systemModule8.js index b46c56b703d..e169701455c 100644 --- a/tests/baselines/reference/systemModule8.js +++ b/tests/baselines/reference/systemModule8.js @@ -41,8 +41,8 @@ System.register([], function (exports_1, context_1) { setters: [], execute: function () { exports_1("x", x = 1); - exports_1("x", ++x) - 1; - exports_1("x", --x) + 1; + exports_1("x", (x++, x)); + exports_1("x", (x--, x)); exports_1("x", ++x); exports_1("x", --x); exports_1("x", x += 1); @@ -55,8 +55,8 @@ System.register([], function (exports_1, context_1) { x - 1; x & 1; x | 1; - for (exports_1("x", x = 5);; exports_1("x", ++x) - 1) { } - for (exports_1("x", x = 8);; exports_1("x", --x) + 1) { } + for (exports_1("x", x = 5);; exports_1("x", (x++, x))) { } + for (exports_1("x", x = 8);; exports_1("x", (x--, x))) { } for (exports_1("x", x = 15);; exports_1("x", ++x)) { } for (exports_1("x", x = 18);; exports_1("x", --x)) { } for (var x_1 = 50;;) { } diff --git a/tests/cases/compiler/es6ExportClauseWithAssignmentInEs5.ts b/tests/cases/compiler/es6ExportClauseWithAssignmentInEs5.ts index 2e86ba7aeea..fb33aee3557 100644 --- a/tests/cases/compiler/es6ExportClauseWithAssignmentInEs5.ts +++ b/tests/cases/compiler/es6ExportClauseWithAssignmentInEs5.ts @@ -12,7 +12,7 @@ var buzz = 10; buzz += 3; var bizz = 8; -bizz++; // compiles to exports.bizz = bizz += 1 +bizz++; // compiles to exports.bizz = (bizz++, bizz) bizz--; // similarly ++bizz; // compiles to exports.bizz = ++bizz