Skip to content

Commit

Permalink
feat: Better error message when using a forgotten node.
Browse files Browse the repository at this point in the history
It will output the node's text that was forgotten.
  • Loading branch information
dsherret committed Oct 6, 2018
1 parent 8b2b925 commit 762254f
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 8 deletions.
37 changes: 33 additions & 4 deletions src/compiler/ast/common/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export class Node<NodeType extends ts.Node = ts.Node> {
/** @internal */
private _compilerNode: NodeType | undefined;
/** @internal */
private _forgottenText: string | undefined;
/** @internal */
private _childStringRanges: [number, number][] | undefined;
/** @internal */
private _leadingCommentRanges: CommentRange[] | undefined;
Expand All @@ -65,8 +67,12 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Gets the underlying compiler node.
*/
get compilerNode(): NodeType {
if (this._compilerNode == null)
throw new errors.InvalidOperationError("Attempted to get information from a node that was removed or forgotten.");
if (this._compilerNode == null) {
let message = "Attempted to get information from a node that was removed or forgotten.";
if (this._forgottenText != null)
message += `\n\nNode text: ${this._forgottenText}`;
throw new errors.InvalidOperationError(message);
}
return this._compilerNode;
}

Expand Down Expand Up @@ -114,14 +120,15 @@ export class Node<NodeType extends ts.Node = ts.Node> {
if (this.wasForgotten())
return;

this._storeTextForForgetting();
this.context.compilerFactory.removeNodeFromCache(this);
this._clearInternals();
}

/**
* Gets if the node was forgotten.
* Gets if the compiler node was forgotten.
*
* This will be true when the node was forgotten or removed.
* This will be true when the compiler node was forgotten or removed.
*/
wasForgotten() {
return this._compilerNode == null;
Expand All @@ -133,10 +140,32 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* WARNING: This should only be called by the compiler factory!
*/
replaceCompilerNodeFromFactory(compilerNode: NodeType) {
if (compilerNode == null)
this._storeTextForForgetting();
this._clearInternals();
this._compilerNode = compilerNode;
}

/** @internal */
private _storeTextForForgetting() {
// check for undefined here just in case
const sourceFileCompilerNode = this.sourceFile && this.sourceFile.compilerNode;
const compilerNode = this._compilerNode;

if (sourceFileCompilerNode == null || compilerNode == null)
return;

this._forgottenText = getText();

function getText() {
const start = compilerNode!.getStart(sourceFileCompilerNode);
const length = compilerNode!.end - start;
const trimmedLength = Math.min(length, 100);
const text = sourceFileCompilerNode.text.substr(start, trimmedLength);
return trimmedLength !== length ? text + "..." : text;
}
}

/** @internal */
private _clearInternals() {
this._compilerNode = undefined;
Expand Down
38 changes: 34 additions & 4 deletions src/tests/compiler/common/nodeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,32 @@ describe(nameof(Node), () => {

describe(nameof<Node>(n => n.compilerNode), () => {
it("should get the underlying compiler node", () => {
const {sourceFile} = getInfoFromText("enum MyEnum {}\n");
const { sourceFile } = getInfoFromText("enum MyEnum {}\n");
// just compare that the texts are the same
expect(sourceFile.getFullText()).to.equal(sourceFile.compilerNode.getFullText());
});

it("should throw an error when using a removed node", () => {
const {firstChild} = getInfoFromText<EnumDeclaration>("enum MyEnum { member }\n");
const { firstChild } = getInfoFromText<EnumDeclaration>("enum MyEnum { member }");
const member = firstChild.getMembers()[0];
member.remove();
expect(() => member.compilerNode).to.throw();
expect(() => member.compilerNode).to.throw(errors.InvalidOperationError, getExpectedForgottenMessage("member"));
});

it("should throw an error when using a removed node and trim the message's node text when it's sufficiently long", () => {
const trimLength = 100;
const nodeText = getTestNodeText();
const { firstChild } = getInfoFromText<EnumDeclaration>(nodeText);
firstChild.remove();
expect(() => firstChild.compilerNode).to.throw(errors.InvalidOperationError, getExpectedForgottenMessage(nodeText.substr(0, trimLength) + "..."));

function getTestNodeText() {
let result = "enum MyEnum { ";
while (result.length < trimLength)
result += `member, `;
result += "}";
return result;
}
});
});

Expand Down Expand Up @@ -882,7 +898,7 @@ class MyClass {
newNode = propAccess.replaceWithText(replaceText);
expect(newNode.getText()).to.equal(getReplaceTextAsString());
expect(sourceFile.getFullText()).to.equal(expectedText);
expect(() => propAccess.compilerNode).to.throw(); // should be forgotten
expect(propAccess.wasForgotten()).to.be.true;

function getReplaceTextAsString() {
if (typeof replaceText === "string")
Expand Down Expand Up @@ -1498,4 +1514,18 @@ class MyClass {
expect(syntaxList.getTrailingTriviaEnd()).to.equal(expectedTriviaEnd);
});
});

describe(nameof<Node>(n => n.forget), () => {
it("should throw an error when using a forgotten node", () => {
const { firstChild } = getInfoFromText<EnumDeclaration>("enum MyEnum { member }");
const member = firstChild.getMembers()[0];
member.forget();
expect(member.wasForgotten()).to.be.true;
expect(() => member.compilerNode).to.throw(errors.InvalidOperationError, getExpectedForgottenMessage("member"));
});
});

function getExpectedForgottenMessage(nodeText: string) {
return `Attempted to get information from a node that was removed or forgotten.\n\nNode text: ${nodeText}`;
}
});

0 comments on commit 762254f

Please sign in to comment.