Trampolines for large binary expressions (#36248)

* WIP

* Test no longer crashes, but emit trampoline is incomplete and skips pipeline phases

* Fix lints, use non-generator trampoline in emit (still skips pipeline)

* Final version with emitBinaryExprssion work stack that is only used if possible

* Fix lints

* retarget to es2015 for testing

* Use bespoke state machine trampolines in binder and checker

* Remove now extraneous code in parser

* Adjust fixupParentReferences to use a depth first preorder traversal rather than breadth first

* Reintroduce incremental fast bail in fixupParentReferences

* Revert target to es5

* PR feedback

* Small edit for devops rebuild with updated definition

* Fix comment nits, add internally extraneous check back into transformer
This commit is contained in:
Wesley Wigham
2020-01-24 16:29:55 -08:00
committed by GitHub
parent f3cc6f6d84
commit 08e6bc20bb
14 changed files with 15373 additions and 129 deletions
+146 -18
View File
@@ -1445,28 +1445,156 @@ namespace ts {
}
}
const enum BindBinaryExpressionFlowState {
BindThenBindChildren,
MaybeBindLeft,
BindToken,
BindRight,
FinishBind
}
function bindBinaryExpressionFlow(node: BinaryExpression) {
const operator = node.operatorToken.kind;
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) {
if (isTopLevelLogicalExpression(node)) {
const postExpressionLabel = createBranchLabel();
bindLogicalExpression(node, postExpressionLabel, postExpressionLabel);
currentFlow = finishFlowLabel(postExpressionLabel);
}
else {
bindLogicalExpression(node, currentTrueTarget!, currentFalseTarget!);
const workStacks: {
expr: BinaryExpression[],
state: BindBinaryExpressionFlowState[],
inStrictMode: (boolean | undefined)[],
parent: (Node | undefined)[],
subtreeFlags: (number | undefined)[]
} = {
expr: [node],
state: [BindBinaryExpressionFlowState.MaybeBindLeft],
inStrictMode: [undefined],
parent: [undefined],
subtreeFlags: [undefined]
};
let stackIndex = 0;
while (stackIndex >= 0) {
node = workStacks.expr[stackIndex];
switch (workStacks.state[stackIndex]) {
case BindBinaryExpressionFlowState.BindThenBindChildren: {
// This state is used only when recuring, to emulate the work that `bind` does before
// reaching `bindChildren`. A normal call to `bindBinaryExpressionFlow` will already have done this work.
node.parent = parent;
const saveInStrictMode = inStrictMode;
bindWorker(node);
const saveParent = parent;
parent = node;
let subtreeFlagsState: number | undefined;
// While this next part does the work of `bindChildren` before it descends into `bindChildrenWorker`
// and uses `subtreeFlagsState` to queue up the work that needs to be done once the node is bound.
if (skipTransformFlagAggregation) {
// do nothing extra
}
else if (node.transformFlags & TransformFlags.HasComputedFlags) {
skipTransformFlagAggregation = true;
subtreeFlagsState = -1;
}
else {
const savedSubtreeTransformFlags = subtreeTransformFlags;
subtreeTransformFlags = 0;
subtreeFlagsState = savedSubtreeTransformFlags;
}
advanceState(BindBinaryExpressionFlowState.MaybeBindLeft, saveInStrictMode, saveParent, subtreeFlagsState);
break;
}
case BindBinaryExpressionFlowState.MaybeBindLeft: {
const operator = node.operatorToken.kind;
// TODO: bindLogicalExpression is recursive - if we want to handle deeply nested `&&` expressions
// we'll need to handle the `bindLogicalExpression` scenarios in this state machine, too
// For now, though, since the common cases are chained `+`, leaving it recursive is fine
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) {
if (isTopLevelLogicalExpression(node)) {
const postExpressionLabel = createBranchLabel();
bindLogicalExpression(node, postExpressionLabel, postExpressionLabel);
currentFlow = finishFlowLabel(postExpressionLabel);
}
else {
bindLogicalExpression(node, currentTrueTarget!, currentFalseTarget!);
}
completeNode();
}
else {
advanceState(BindBinaryExpressionFlowState.BindToken);
maybeBind(node.left);
}
break;
}
case BindBinaryExpressionFlowState.BindToken: {
advanceState(BindBinaryExpressionFlowState.BindRight);
maybeBind(node.operatorToken);
break;
}
case BindBinaryExpressionFlowState.BindRight: {
advanceState(BindBinaryExpressionFlowState.FinishBind);
maybeBind(node.right);
break;
}
case BindBinaryExpressionFlowState.FinishBind: {
const operator = node.operatorToken.kind;
if (isAssignmentOperator(operator) && !isAssignmentTarget(node)) {
bindAssignmentTargetFlow(node.left);
if (operator === SyntaxKind.EqualsToken && node.left.kind === SyntaxKind.ElementAccessExpression) {
const elementAccess = <ElementAccessExpression>node.left;
if (isNarrowableOperand(elementAccess.expression)) {
currentFlow = createFlowMutation(FlowFlags.ArrayMutation, currentFlow, node);
}
}
}
completeNode();
break;
}
default: return Debug.fail(`Invalid state ${workStacks.state[stackIndex]} for bindBinaryExpressionFlow`);
}
}
else {
bindEachChild(node);
if (isAssignmentOperator(operator) && !isAssignmentTarget(node)) {
bindAssignmentTargetFlow(node.left);
if (operator === SyntaxKind.EqualsToken && node.left.kind === SyntaxKind.ElementAccessExpression) {
const elementAccess = <ElementAccessExpression>node.left;
if (isNarrowableOperand(elementAccess.expression)) {
currentFlow = createFlowMutation(FlowFlags.ArrayMutation, currentFlow, node);
}
/**
* Note that `advanceState` sets the _current_ head state, and that `maybeBind` potentially pushes on a new
* head state; so `advanceState` must be called before any `maybeBind` during a state's execution.
*/
function advanceState(state: BindBinaryExpressionFlowState, isInStrictMode?: boolean, parent?: Node, subtreeFlags?: number) {
workStacks.state[stackIndex] = state;
if (isInStrictMode !== undefined) {
workStacks.inStrictMode[stackIndex] = isInStrictMode;
}
if (parent !== undefined) {
workStacks.parent[stackIndex] = parent;
}
if (subtreeFlags !== undefined) {
workStacks.subtreeFlags[stackIndex] = subtreeFlags;
}
}
function completeNode() {
if (workStacks.inStrictMode[stackIndex] !== undefined) {
if (workStacks.subtreeFlags[stackIndex] === -1) {
skipTransformFlagAggregation = false;
subtreeTransformFlags |= node.transformFlags & ~getTransformFlagsSubtreeExclusions(node.kind);
}
else if (workStacks.subtreeFlags[stackIndex] !== undefined) {
subtreeTransformFlags = workStacks.subtreeFlags[stackIndex]! | computeTransformFlagsForNode(node, subtreeTransformFlags);
}
inStrictMode = workStacks.inStrictMode[stackIndex]!;
parent = workStacks.parent[stackIndex]!;
}
stackIndex--;
}
/**
* If `node` is a BinaryExpression, adds it to the local work stack, otherwise recursively binds it
*/
function maybeBind(node: Node) {
if (node && isBinaryExpression(node)) {
stackIndex++;
workStacks.expr[stackIndex] = node;
workStacks.state[stackIndex] = BindBinaryExpressionFlowState.BindThenBindChildren;
workStacks.inStrictMode[stackIndex] = undefined;
workStacks.parent[stackIndex] = undefined;
workStacks.subtreeFlags[stackIndex] = undefined;
}
else {
bind(node);
}
}
}
+101 -7
View File
@@ -27136,12 +27136,89 @@ namespace ts {
return (target.flags & TypeFlags.Nullable) !== 0 || isTypeComparableTo(source, target);
}
const enum CheckBinaryExpressionState {
MaybeCheckLeft,
CheckRight,
FinishCheck
}
function checkBinaryExpression(node: BinaryExpression, checkMode?: CheckMode) {
if (isInJSFile(node) && getAssignedExpandoInitializer(node)) {
return checkExpression(node.right, checkMode);
const workStacks: {
expr: BinaryExpression[],
state: CheckBinaryExpressionState[],
leftType: (Type | undefined)[]
} = {
expr: [node],
state: [CheckBinaryExpressionState.MaybeCheckLeft],
leftType: [undefined]
};
let stackIndex = 0;
let lastResult: Type | undefined;
while (stackIndex >= 0) {
node = workStacks.expr[stackIndex];
switch (workStacks.state[stackIndex]) {
case CheckBinaryExpressionState.MaybeCheckLeft: {
if (isInJSFile(node) && getAssignedExpandoInitializer(node)) {
finishInvocation(checkExpression(node.right, checkMode));
break;
}
checkGrammarNullishCoalesceWithLogicalExpression(node);
const operator = node.operatorToken.kind;
if (operator === SyntaxKind.EqualsToken && (node.left.kind === SyntaxKind.ObjectLiteralExpression || node.left.kind === SyntaxKind.ArrayLiteralExpression)) {
finishInvocation(checkDestructuringAssignment(node.left, checkExpression(node.right, checkMode), checkMode, node.right.kind === SyntaxKind.ThisKeyword));
break;
}
advanceState(CheckBinaryExpressionState.CheckRight);
maybeCheckExpression(node.left);
break;
}
case CheckBinaryExpressionState.CheckRight: {
const leftType = lastResult!;
workStacks.leftType[stackIndex] = leftType;
const operator = node.operatorToken.kind;
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) {
checkTruthinessOfType(leftType, node.left);
}
advanceState(CheckBinaryExpressionState.FinishCheck);
maybeCheckExpression(node.right);
break;
}
case CheckBinaryExpressionState.FinishCheck: {
const leftType = workStacks.leftType[stackIndex]!;
const rightType = lastResult!;
finishInvocation(checkBinaryLikeExpressionWorker(node.left, node.operatorToken, node.right, leftType, rightType, node));
break;
}
default: return Debug.fail(`Invalid state ${workStacks.state[stackIndex]} for checkBinaryExpression`);
}
}
return lastResult!;
function finishInvocation(result: Type) {
lastResult = result;
stackIndex--;
}
/**
* Note that `advanceState` sets the _current_ head state, and that `maybeCheckExpression` potentially pushes on a new
* head state; so `advanceState` must be called before any `maybeCheckExpression` during a state's execution.
*/
function advanceState(nextState: CheckBinaryExpressionState) {
workStacks.state[stackIndex] = nextState;
}
function maybeCheckExpression(node: Expression) {
if (isBinaryExpression(node)) {
stackIndex++;
workStacks.expr[stackIndex] = node;
workStacks.state[stackIndex] = CheckBinaryExpressionState.MaybeCheckLeft;
workStacks.leftType[stackIndex] = undefined;
}
else {
lastResult = checkExpression(node, checkMode);
}
}
checkGrammarNullishCoalesceWithLogicalExpression(node);
return checkBinaryLikeExpression(node.left, node.operatorToken, node.right, checkMode, node);
}
function checkGrammarNullishCoalesceWithLogicalExpression(node: BinaryExpression) {
@@ -27156,6 +27233,8 @@ namespace ts {
}
}
// Note that this and `checkBinaryExpression` above should behave mostly the same, except this elides some
// expression-wide checks and does not use a work stack to fold nested binary expressions into the same callstack frame
function checkBinaryLikeExpression(left: Expression, operatorToken: Node, right: Expression, checkMode?: CheckMode, errorNode?: Node): Type {
const operator = operatorToken.kind;
if (operator === SyntaxKind.EqualsToken && (left.kind === SyntaxKind.ObjectLiteralExpression || left.kind === SyntaxKind.ArrayLiteralExpression)) {
@@ -27169,7 +27248,19 @@ namespace ts {
leftType = checkExpression(left, checkMode);
}
let rightType = checkExpression(right, checkMode);
const rightType = checkExpression(right, checkMode);
return checkBinaryLikeExpressionWorker(left, operatorToken, right, leftType, rightType, errorNode);
}
function checkBinaryLikeExpressionWorker(
left: Expression,
operatorToken: Node,
right: Expression,
leftType: Type,
rightType: Type,
errorNode?: Node
): Type {
const operator = operatorToken.kind;
switch (operator) {
case SyntaxKind.AsteriskToken:
case SyntaxKind.AsteriskAsteriskToken:
@@ -30774,14 +30865,17 @@ namespace ts {
checkSourceElement(node.statement);
}
function checkTruthinessExpression(node: Expression, checkMode?: CheckMode) {
const type = checkExpression(node, checkMode);
function checkTruthinessOfType(type: Type, node: Node) {
if (type.flags & TypeFlags.Void) {
error(node, Diagnostics.An_expression_of_type_void_cannot_be_tested_for_truthiness);
}
return type;
}
function checkTruthinessExpression(node: Expression, checkMode?: CheckMode) {
return checkTruthinessOfType(checkExpression(node, checkMode), node);
}
function checkForStatement(node: ForStatement) {
// Grammar checking
if (!checkGrammarStatementInAmbientContext(node)) {
+91 -24
View File
@@ -379,6 +379,7 @@ namespace ts {
// transform hooks
onEmitNode: transform.emitNodeWithNotification,
isEmitNotificationEnabled: transform.isEmitNotificationEnabled,
substituteNode: transform.substituteNode,
});
@@ -437,6 +438,7 @@ namespace ts {
// transform hooks
onEmitNode: declarationTransform.emitNodeWithNotification,
isEmitNotificationEnabled: declarationTransform.isEmitNotificationEnabled,
substituteNode: declarationTransform.substituteNode,
});
const declBlocked = (!!declarationTransform.diagnostics && !!declarationTransform.diagnostics.length) || !!host.isEmitBlocked(declarationFilePath) || !!compilerOptions.noEmit;
@@ -819,6 +821,7 @@ namespace ts {
const {
hasGlobalName,
onEmitNode = noEmitNotification,
isEmitNotificationEnabled,
substituteNode = noEmitSubstitution,
onBeforeEmitNodeArray,
onAfterEmitNodeArray,
@@ -1160,7 +1163,7 @@ namespace ts {
lastNode = node;
lastSubstitution = undefined;
const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, node);
const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, emitHint, node);
pipelinePhase(emitHint, node);
Debug.assert(lastNode === node);
@@ -1172,16 +1175,16 @@ namespace ts {
return substitute || node;
}
function getPipelinePhase(phase: PipelinePhase, node: Node) {
function getPipelinePhase(phase: PipelinePhase, emitHint: EmitHint, node: Node) {
switch (phase) {
case PipelinePhase.Notification:
if (onEmitNode !== noEmitNotification) {
if (onEmitNode !== noEmitNotification && (!isEmitNotificationEnabled || isEmitNotificationEnabled(node))) {
return pipelineEmitWithNotification;
}
// falls through
case PipelinePhase.Substitution:
if (substituteNode !== noEmitSubstitution) {
if (substituteNode !== noEmitSubstitution && (lastSubstitution = substituteNode(emitHint, node)) !== node) {
return pipelineEmitWithSubstitution;
}
// falls through
@@ -1206,13 +1209,13 @@ namespace ts {
}
}
function getNextPipelinePhase(currentPhase: PipelinePhase, node: Node) {
return getPipelinePhase(currentPhase + 1, node);
function getNextPipelinePhase(currentPhase: PipelinePhase, emitHint: EmitHint, node: Node) {
return getPipelinePhase(currentPhase + 1, emitHint, node);
}
function pipelineEmitWithNotification(hint: EmitHint, node: Node) {
Debug.assert(lastNode === node);
const pipelinePhase = getNextPipelinePhase(PipelinePhase.Notification, node);
const pipelinePhase = getNextPipelinePhase(PipelinePhase.Notification, hint, node);
onEmitNode(hint, node, pipelinePhase);
Debug.assert(lastNode === node);
}
@@ -1653,9 +1656,8 @@ namespace ts {
function pipelineEmitWithSubstitution(hint: EmitHint, node: Node) {
Debug.assert(lastNode === node || lastSubstitution === node);
const pipelinePhase = getNextPipelinePhase(PipelinePhase.Substitution, node);
lastSubstitution = substituteNode(hint, node);
pipelinePhase(hint, lastSubstitution);
const pipelinePhase = getNextPipelinePhase(PipelinePhase.Substitution, hint, node);
pipelinePhase(hint, lastSubstitution!);
Debug.assert(lastNode === node || lastSubstitution === node);
}
@@ -2425,19 +2427,84 @@ namespace ts {
writeTokenText(node.operator, writeOperator);
}
function emitBinaryExpression(node: BinaryExpression) {
const isCommaOperator = node.operatorToken.kind !== SyntaxKind.CommaToken;
const indentBeforeOperator = needsIndentation(node, node.left, node.operatorToken);
const indentAfterOperator = needsIndentation(node, node.operatorToken, node.right);
const enum EmitBinaryExpressionState {
EmitLeft,
EmitRight,
FinishEmit
}
emitExpression(node.left);
increaseIndentIf(indentBeforeOperator, isCommaOperator);
emitLeadingCommentsOfPosition(node.operatorToken.pos);
writeTokenNode(node.operatorToken, node.operatorToken.kind === SyntaxKind.InKeyword ? writeKeyword : writeOperator);
emitTrailingCommentsOfPosition(node.operatorToken.end, /*prefixSpace*/ true); // Binary operators should have a space before the comment starts
increaseIndentIf(indentAfterOperator, /*writeSpaceIfNotIndenting*/ true);
emitExpression(node.right);
decreaseIndentIf(indentBeforeOperator, indentAfterOperator);
/**
* emitBinaryExpression includes an embedded work stack to attempt to handle as many nested binary expressions
* as possible without creating any additional stack frames. This can only be done when the emit pipeline does
* not require notification/substitution/comment/sourcemap decorations.
*/
function emitBinaryExpression(node: BinaryExpression) {
const nodeStack = [node];
const stateStack = [EmitBinaryExpressionState.EmitLeft];
let stackIndex = 0;
while (stackIndex >= 0) {
node = nodeStack[stackIndex];
switch (stateStack[stackIndex]) {
case EmitBinaryExpressionState.EmitLeft: {
maybePipelineEmitExpression(node.left);
break;
}
case EmitBinaryExpressionState.EmitRight: {
const isCommaOperator = node.operatorToken.kind !== SyntaxKind.CommaToken;
const indentBeforeOperator = needsIndentation(node, node.left, node.operatorToken);
const indentAfterOperator = needsIndentation(node, node.operatorToken, node.right);
increaseIndentIf(indentBeforeOperator, isCommaOperator);
emitLeadingCommentsOfPosition(node.operatorToken.pos);
writeTokenNode(node.operatorToken, node.operatorToken.kind === SyntaxKind.InKeyword ? writeKeyword : writeOperator);
emitTrailingCommentsOfPosition(node.operatorToken.end, /*prefixSpace*/ true); // Binary operators should have a space before the comment starts
increaseIndentIf(indentAfterOperator, /*writeSpaceIfNotIndenting*/ true);
maybePipelineEmitExpression(node.right);
break;
}
case EmitBinaryExpressionState.FinishEmit: {
const indentBeforeOperator = needsIndentation(node, node.left, node.operatorToken);
const indentAfterOperator = needsIndentation(node, node.operatorToken, node.right);
decreaseIndentIf(indentBeforeOperator, indentAfterOperator);
stackIndex--;
break;
}
default: return Debug.fail(`Invalid state ${stateStack[stackIndex]} for emitBinaryExpressionWorker`);
}
}
function maybePipelineEmitExpression(next: Expression) {
// Advance the state of this unit of work,
stateStack[stackIndex]++;
// Then actually do the work of emitting the node `next` returned by the prior state
// The following section should be identical to `pipelineEmit` save it assumes EmitHint.Expression and offloads
// binary expression handling, where possible, to the contained work queue
// #region trampolinePipelineEmit
const savedLastNode = lastNode;
const savedLastSubstitution = lastSubstitution;
lastNode = next;
lastSubstitution = undefined;
const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, EmitHint.Expression, next);
if (pipelinePhase === pipelineEmitWithHint && isBinaryExpression(next)) {
// If the target pipeline phase is emit directly, and the next node's also a binary expression,
// skip all the intermediate indirection and push the expression directly onto the work stack
stackIndex++;
stateStack[stackIndex] = EmitBinaryExpressionState.EmitLeft;
nodeStack[stackIndex] = next;
}
else {
pipelinePhase(EmitHint.Expression, next);
}
Debug.assert(lastNode === next);
lastNode = savedLastNode;
lastSubstitution = savedLastSubstitution;
// #endregion trampolinePipelineEmit
}
}
function emitConditionalExpression(node: ConditionalExpression) {
@@ -4773,7 +4840,7 @@ namespace ts {
forEach(getSyntheticLeadingComments(node), emitLeadingSynthesizedComment);
exitComment();
const pipelinePhase = getNextPipelinePhase(PipelinePhase.Comments, node);
const pipelinePhase = getNextPipelinePhase(PipelinePhase.Comments, hint, node);
if (emitFlags & EmitFlags.NoNestedComments) {
commentsDisabled = true;
pipelinePhase(hint, node);
@@ -5046,7 +5113,7 @@ namespace ts {
function pipelineEmitWithSourceMap(hint: EmitHint, node: Node) {
Debug.assert(lastNode === node || lastSubstitution === node);
const pipelinePhase = getNextPipelinePhase(PipelinePhase.SourceMaps, node);
const pipelinePhase = getNextPipelinePhase(PipelinePhase.SourceMaps, hint, node);
if (isUnparsedSource(node) || isUnparsedPrepend(node)) {
pipelinePhase(hint, node);
}
+20 -17
View File
@@ -904,28 +904,31 @@ namespace ts {
// overhead. This functions allows us to set all the parents, without all the expense of
// binding.
let parent: Node = rootNode;
forEachChild(rootNode, visitNode);
const stack: Node[] = [rootNode];
while (stack.length) {
const parent = stack.pop()!;
bindParentToChildren(parent, gatherChildren(parent));
}
return;
function visitNode(n: Node): void {
// walk down setting parents that differ from the parent we think it should be. This
// allows us to quickly bail out of setting parents for subtrees during incremental
// parsing
if (n.parent !== parent) {
n.parent = parent;
function gatherChildren(node: Node) {
const children: Node[] = [];
forEachChild(node, n => { children.unshift(n); }); // By using a stack above and `unshift` here, we emulate a depth-first preorder traversal
return children;
}
const saveParent = parent;
parent = n;
forEachChild(n, visitNode);
if (hasJSDocNodes(n)) {
for (const jsDoc of n.jsDoc!) {
jsDoc.parent = n;
parent = jsDoc;
forEachChild(jsDoc, visitNode);
function bindParentToChildren(parent: Node, children: readonly Node[]) {
for (const child of children) {
if (child.parent === parent) continue; // already bound, assume subtree is bound
child.parent = parent;
stack.push(child);
if (hasJSDocNodes(child)) {
for (const jsDoc of child.jsDoc!) {
jsDoc.parent = child;
stack.push(jsDoc);
}
}
parent = saveParent;
}
}
}
+3
View File
@@ -225,6 +225,7 @@ namespace ts {
transformed,
substituteNode,
emitNodeWithNotification,
isEmitNotificationEnabled,
dispose,
diagnostics
};
@@ -288,6 +289,8 @@ namespace ts {
function emitNodeWithNotification(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) {
Debug.assert(state < TransformationState.Disposed, "Cannot invoke TransformationResult callbacks after the result is disposed.");
if (node) {
// TODO: Remove check and unconditionally use onEmitNode when API is breakingly changed
// (see https://github.com/microsoft/TypeScript/pull/36248/files/5062623f39120171b98870c71344b3242eb03d23#r369766739)
if (isEmitNotificationEnabled(node)) {
onEmitNode(hint, node, emitCallback);
}
+13
View File
@@ -5972,6 +5972,13 @@ namespace ts {
*/
emitNodeWithNotification(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void;
/**
* Indicates if a given node needs an emit notification
*
* @param node The node to emit.
*/
isEmitNotificationEnabled?(node: Node): boolean;
/**
* Clean up EmitNode entries on any parse-tree nodes.
*/
@@ -6167,6 +6174,12 @@ namespace ts {
* ```
*/
onEmitNode?(hint: EmitHint, node: Node | undefined, emitCallback: (hint: EmitHint, node: Node | undefined) => void): void;
/**
* A hook used to check if an emit notification is required for a node.
* @param node The node to emit.
*/
isEmitNotificationEnabled?(node: Node | undefined): boolean;
/**
* A hook used by the Printer to perform just-in-time substitution of a node. This is
* primarily used by node transformations that need to substitute one node for another,
+1 -1
View File
@@ -509,7 +509,7 @@ namespace ts {
}
function isJSDocTypeExpressionOrChild(node: Node): boolean {
return node.kind === SyntaxKind.JSDocTypeExpression || (node.parent && isJSDocTypeExpressionOrChild(node.parent));
return !!findAncestor(node, isJSDocTypeExpression);
}
export function getTextOfNodeFromSourceText(sourceText: string, node: Node, includeTrivia = false): string {
+51 -44
View File
@@ -69,56 +69,63 @@ namespace Utils {
export const canonicalizeForHarness = ts.createGetCanonicalFileName(/*caseSensitive*/ false); // This is done so tests work on windows _and_ linux
export function assertInvariants(node: ts.Node | undefined, parent: ts.Node | undefined): void {
if (node) {
assert.isFalse(node.pos < 0, "node.pos < 0");
assert.isFalse(node.end < 0, "node.end < 0");
assert.isFalse(node.end < node.pos, "node.end < node.pos");
assert.equal(node.parent, parent, "node.parent !== parent");
export function assertInvariants(node: ts.Node | undefined, parent: ts.Node | undefined) {
const queue: [ts.Node | undefined, ts.Node | undefined][] = [[node, parent]];
for (const [node, parent] of queue) {
assertInvariantsWorker(node, parent);
}
if (parent) {
// Make sure each child is contained within the parent.
assert.isFalse(node.pos < parent.pos, "node.pos < parent.pos");
assert.isFalse(node.end > parent.end, "node.end > parent.end");
}
function assertInvariantsWorker(node: ts.Node | undefined, parent: ts.Node | undefined): void {
if (node) {
assert.isFalse(node.pos < 0, "node.pos < 0");
assert.isFalse(node.end < 0, "node.end < 0");
assert.isFalse(node.end < node.pos, "node.end < node.pos");
assert.equal(node.parent, parent, "node.parent !== parent");
ts.forEachChild(node, child => {
assertInvariants(child, node);
});
if (parent) {
// Make sure each child is contained within the parent.
assert.isFalse(node.pos < parent.pos, "node.pos < parent.pos");
assert.isFalse(node.end > parent.end, "node.end > parent.end");
}
// Make sure each of the children is in order.
let currentPos = 0;
ts.forEachChild(node,
child => {
assert.isFalse(child.pos < currentPos, "child.pos < currentPos");
currentPos = child.end;
},
array => {
assert.isFalse(array.pos < node.pos, "array.pos < node.pos");
assert.isFalse(array.end > node.end, "array.end > node.end");
assert.isFalse(array.pos < currentPos, "array.pos < currentPos");
for (const item of array) {
assert.isFalse(item.pos < currentPos, "array[i].pos < currentPos");
currentPos = item.end;
}
currentPos = array.end;
ts.forEachChild(node, child => {
queue.push([child, node]);
});
const childNodesAndArrays: any[] = [];
ts.forEachChild(node, child => { childNodesAndArrays.push(child); }, array => { childNodesAndArrays.push(array); });
// Make sure each of the children is in order.
let currentPos = 0;
ts.forEachChild(node,
child => {
assert.isFalse(child.pos < currentPos, "child.pos < currentPos");
currentPos = child.end;
},
array => {
assert.isFalse(array.pos < node.pos, "array.pos < node.pos");
assert.isFalse(array.end > node.end, "array.end > node.end");
assert.isFalse(array.pos < currentPos, "array.pos < currentPos");
for (const childName in node) {
if (childName === "parent" || childName === "nextContainer" || childName === "modifiers" || childName === "externalModuleIndicator" ||
// for now ignore jsdoc comments
childName === "jsDocComment" || childName === "checkJsDirective" || childName === "commonJsModuleIndicator") {
continue;
}
const child = (<any>node)[childName];
if (isNodeOrArray(child)) {
assert.isFalse(childNodesAndArrays.indexOf(child) < 0,
"Missing child when forEach'ing over node: " + (<any>ts).SyntaxKind[node.kind] + "-" + childName);
for (const item of array) {
assert.isFalse(item.pos < currentPos, "array[i].pos < currentPos");
currentPos = item.end;
}
currentPos = array.end;
});
const childNodesAndArrays: any[] = [];
ts.forEachChild(node, child => { childNodesAndArrays.push(child); }, array => { childNodesAndArrays.push(array); });
for (const childName in node) {
if (childName === "parent" || childName === "nextContainer" || childName === "modifiers" || childName === "externalModuleIndicator" ||
// for now ignore jsdoc comments
childName === "jsDocComment" || childName === "checkJsDirective" || childName === "commonJsModuleIndicator") {
continue;
}
const child = (<any>node)[childName];
if (isNodeOrArray(child)) {
assert.isFalse(childNodesAndArrays.indexOf(child) < 0,
"Missing child when forEach'ing over node: " + (<any>ts).SyntaxKind[node.kind] + "-" + childName);
}
}
}
}
+22 -13
View File
@@ -21,6 +21,19 @@ namespace Harness {
type?: string;
}
function* forEachASTNode(node: ts.Node) {
const work = [node];
while (work.length) {
const elem = work.pop()!;
yield elem;
const resChildren: ts.Node[] = [];
// push onto work queue in reverse order to maintain preorder traversal
ts.forEachChild(elem, c => { resChildren.unshift(c); });
work.push(...resChildren);
}
}
export class TypeWriterWalker {
currentSourceFile!: ts.SourceFile;
@@ -53,19 +66,15 @@ namespace Harness {
}
private *visitNode(node: ts.Node, isSymbolWalk: boolean): IterableIterator<TypeWriterResult> {
if (ts.isExpressionNode(node) || node.kind === ts.SyntaxKind.Identifier || ts.isDeclarationName(node)) {
const result = this.writeTypeOrSymbol(node, isSymbolWalk);
if (result) {
yield result;
}
}
const children: ts.Node[] = [];
ts.forEachChild(node, child => void children.push(child));
for (const child of children) {
const gen = this.visitNode(child, isSymbolWalk);
for (let {done, value} = gen.next(); !done; { done, value } = gen.next()) {
yield value;
const gen = forEachASTNode(node);
let res = gen.next();
for (; !res.done; res = gen.next()) {
const {value: node} = res;
if (ts.isExpressionNode(node) || node.kind === ts.SyntaxKind.Identifier || ts.isDeclarationName(node)) {
const result = this.writeTypeOrSymbol(node, isSymbolWalk);
if (result) {
yield result;
}
}
}
}
@@ -944,6 +944,11 @@ console.log(blabla);`
path: `${tscWatch.projectRoot}/tsconfig.json`,
content: "{}"
};
// Move things from staging to node_modules without triggering watch
const moduleFile: File = {
path: `${tscWatch.projectRoot}/node_modules/@angular/core/index.d.ts`,
content: `export const y = 10;`
};
const projectFiles = [main, libFile, config];
const host = createServerHost(projectFiles);
const session = createSession(host, { canUseEvents: true });
@@ -983,11 +988,6 @@ console.log(blabla);`
verifyWhileNpmInstall({ timeouts: 0, semantic: moduleNotFoundErr });
filesAndFoldersToAdd = [];
// Move things from staging to node_modules without triggering watch
const moduleFile: File = {
path: `${tscWatch.projectRoot}/node_modules/@angular/core/index.d.ts`,
content: `export const y = 10;`
};
host.ensureFileOrFolder(moduleFile, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true, /*ignoreParentWatch*/ true);
// Since we added/removed in .staging no timeout
verifyWhileNpmInstall({ timeouts: 0, semantic: moduleNotFoundErr });