Skip to content

Commit

Permalink
feat(navigation): #140 - Forget blocks.
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret committed Nov 27, 2017
1 parent 17d86c6 commit f5a8b39
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 4 deletions.
53 changes: 52 additions & 1 deletion docs/manipulation/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ nameProperty.getText(); // "name: number;"

When thinking about performance, the key point here is that if you have a lot of previously navigated nodes and a very large file, then manipulation might start to become sluggish.

### Forgetting Nodes
### Forgetting Nodes (Advanced)

The main way to improve performance when manipulating, is to "forget" a node when you're done with it.

Expand All @@ -46,3 +46,54 @@ personInterface.forget();

That will stop tracking the node and all its previously navigated descendants (ex. in this case, `nameProperty` as well).
It won't be updated when manipulation happens again. Note that after doing this, the node will throw an error if one of its properties or methods is accessed.

### Forget Blocks (Advanced)

It's possible to make sure all created nodes within a block are forgotten:

```typescript
import {Ast, NamespaceDeclaration, InterfaceDeclaration, ClassDeclaration} from "ts-simple-ast";

const ast = new Ast();
const text = "namespace Namespace { interface Interface {} class Class {} }";
const sourceFile = ast.addSourceFileFromText("file.ts", text);

let namespaceDeclaration: NamespaceDeclaration;
let interfaceDeclaration: InterfaceDeclaration;
let classDeclaration: ClassDeclaration;

ast.forgetNodesCreatedInBlock(remember => {
namespaceDeclaration = sourceFile.getNamespaceOrThrow("Namespace");
interfaceDeclaration = namespaceDeclaration.getInterfaceOrThrow("Interface");
classDeclaration = namespaceDeclaration.getClassDeclarationOrThrow("Class");

// you can mark nodes to remember outside the scope of this block...
// this will remember the specified node and all its ancestors
remember(interfaceDeclaration); // or pass in multiple nodes
});

namespaceDeclaration.getText(); // ok, child was marked to remember
interfaceDeclaration.getText(); // ok, was explicitly marked to remember
classDeclaration.getText(); // throws, was forgotten
```

Also, do not be concerned about nesting forget blocks. That is perfectly fine to do:

```typescript
ast.forgetNodesCreatedInBlock(() => {
namespaceDeclaration = sourceFile.getNamespaceOrThrow("Namespace");
interfaceDeclaration = namespaceDeclaration.getInterfaceOrThrow("Interface");

ast.forgetNodesCreatedInBlock(remember => {
classDeclaration = namespaceDeclaration.getClassDeclarationOrThrow("Class");
remember(namespaceDeclaration);
});

classDeclaration.getText(); // throws, was forgotten outside the block above
interfaceDeclaration.getText(); // ok, hasn't been forgotten yet
});

namespaceDeclaration.getText(); // ok, was marked to remember in one of the blocks
interfaceDeclaration.getText(); // throws, was forgotten
classDeclaration.getText(); // throws, was forgotten
```
10 changes: 10 additions & 0 deletions src/TsSimpleAst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ export class TsSimpleAst {
// return a copy
return {...this.global.compilerOptions};
}

/**
* Forgets the nodes created in the scope of the passed in block.
*
* This is an advanced method that can be used to easily "forget" all the nodes created within the scope of the block.
* @param block - Block of code to run.
*/
forgetNodesCreatedInBlock(block: (remember: (...node: compiler.Node[]) => void) => void) {
this.global.compilerFactory.forgetNodesCreatedInBlock(block);
}
}

function getCompilerOptionsFromOptions(options: Options, fileSystem: FileSystemHost) {
Expand Down
14 changes: 14 additions & 0 deletions src/compiler/common/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* This is useful if you want to improve the performance of manipulation by not tracking this node anymore.
*/
forget() {
if (this.wasForgotten())
return;

for (const child of this.getChildrenInCacheIterator())
child.forget();

Expand All @@ -66,10 +69,21 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* @internal
*/
forgetOnlyThis() {
if (this.wasForgotten())
return;

this.global.compilerFactory.removeNodeFromCache(this);
this._compilerNode = undefined;
}

/**
* Gets if the node was forgotten.
* @internal
*/
wasForgotten() {
return this._compilerNode == null;
}

/**
* Gets the syntax kind.
*/
Expand Down
19 changes: 18 additions & 1 deletion src/factories/CompilerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {GlobalContainer} from "./../GlobalContainer";
import {VirtualFileSystemHost} from "./../fileSystem";
import {createWrappedNode} from "./../createWrappedNode";
import {nodeToWrapperMappings} from "./nodeToWrapperMappings";
import {ForgetfulNodeCache} from "./ForgetfulNodeCache";

/**
* Factory for creating compiler wrappers.
Expand All @@ -14,7 +15,7 @@ import {nodeToWrapperMappings} from "./nodeToWrapperMappings";
export class CompilerFactory {
private readonly sourceFileCacheByFilePath = new KeyValueCache<string, compiler.SourceFile>();
private readonly normalizedDirectories = createHashSet<string>();
private readonly nodeCache = new KeyValueCache<ts.Node, compiler.Node>();
private readonly nodeCache = new ForgetfulNodeCache();
private readonly sourceFileAddedEventContainer = new EventContainer<{ addedSourceFile: compiler.SourceFile; }>();

/**
Expand Down Expand Up @@ -266,4 +267,20 @@ export class CompilerFactory {
this.sourceFileCacheByFilePath.removeByKey(sourceFile.fileName);
}
}

/**
* Forgets the nodes created in the block.
* @param block - Block of code to run.
*/
forgetNodesCreatedInBlock(block: (remember: (...node: compiler.Node[]) => void) => void) {
this.nodeCache.setForgetPoint();
try {
block((...nodes) => {
for (const node of nodes)
this.nodeCache.rememberNode(node);
});
} finally {
this.nodeCache.forgetLastPoint();
}
}
}
58 changes: 58 additions & 0 deletions src/factories/ForgetfulNodeCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as ts from "typescript";
import {Node} from "./../compiler";
import {KeyValueCache, createHashSet, HashSet} from "./../utils";

/**
* Extension of KeyValueCache that allows for "forget points."
*/
export class ForgetfulNodeCache extends KeyValueCache<ts.Node, Node> {
private readonly forgetStack: HashSet<Node>[] = [];

getOrCreate<TCreate extends Node>(key: ts.Node, createFunc: () => TCreate) {
return super.getOrCreate(key, () => {
const node = createFunc();
if (this.forgetStack.length > 0)
this.forgetStack[this.forgetStack.length - 1].add(node);
return node;
});
}

setForgetPoint() {
this.forgetStack.push(createHashSet());
}

forgetLastPoint() {
const nodes = this.forgetStack.pop();
if (nodes != null)
this.forgetNodes(nodes.values());
}

rememberNode(node: Node) {
let wasInForgetStack = false;
for (const stackItem of this.forgetStack) {
if (stackItem.delete(node)) {
wasInForgetStack = true;
break;
}
}

if (wasInForgetStack)
this.rememberParentOfNode(node);

return wasInForgetStack;
}

private rememberParentOfNode(node: Node) {
const parent = node.getParentSyntaxList() || node.getParent();
if (parent != null)
this.rememberNode(parent);
}

private forgetNodes(nodes: IterableIterator<Node>) {
for (const node of nodes) {
if (node.getKind() === ts.SyntaxKind.SourceFile)
continue;
node.forgetOnlyThis();
}
}
}
45 changes: 45 additions & 0 deletions src/tests/factories/forgetfulNodeCacheTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as ts from "typescript";
import {expect} from "chai";
import {ArrayUtils} from "./../../utils";
import {getInfoFromText} from "./../compiler/testHelpers";
import {ForgetfulNodeCache} from "./../../factories/ForgetfulNodeCache";

describe(nameof(ForgetfulNodeCache), () => {
it("should forget nodes created after a forget point", () => {
const {firstChild} = getInfoFromText("class MyClass { prop: string; }");
const cache = new ForgetfulNodeCache();
cache.getOrCreate(firstChild.compilerNode, () => firstChild);

cache.setForgetPoint();
const classKeyword = firstChild.getFirstChildByKindOrThrow(ts.SyntaxKind.ClassKeyword);
cache.getOrCreate(classKeyword.compilerNode, () => classKeyword);

cache.setForgetPoint();
const openBraceToken = firstChild.getFirstChildByKindOrThrow(ts.SyntaxKind.OpenBraceToken);
cache.getOrCreate(openBraceToken.compilerNode, () => openBraceToken);

cache.forgetLastPoint();
expect(openBraceToken.wasForgotten()).to.be.true;
expect(classKeyword.wasForgotten()).to.be.false;
expect(firstChild.wasForgotten()).to.be.false;

cache.setForgetPoint();
const closeBraceToken = firstChild.getFirstChildByKindOrThrow(ts.SyntaxKind.CloseBraceToken);
cache.getOrCreate(closeBraceToken.compilerNode, () => closeBraceToken);

cache.forgetLastPoint();
expect(closeBraceToken.wasForgotten()).to.be.true;
expect(firstChild.wasForgotten()).to.be.false;
const syntaxList = firstChild.getChildSyntaxListOrThrow();
const property = syntaxList.getChildren()[0];
cache.rememberNode(property);

cache.forgetLastPoint();
expect(openBraceToken.wasForgotten()).to.be.true;
expect(closeBraceToken.wasForgotten()).to.be.true;
expect(classKeyword.wasForgotten()).to.be.true;
expect(property.wasForgotten()).to.be.false; // it was remembered
expect(syntaxList.wasForgotten()).to.be.false; // it should remember the parents
expect(firstChild.wasForgotten()).to.be.false;
});
});
69 changes: 68 additions & 1 deletion src/tests/tsSimpleAstTests.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as path from "path";
import * as ts from "typescript";
import {expect} from "chai";
import {EmitResult} from "./../compiler";
import {EmitResult, Node, SourceFile, NamespaceDeclaration} from "./../compiler";
import {TsSimpleAst} from "./../TsSimpleAst";
import {IndentationText} from "./../ManipulationSettings";
import {FileUtils} from "./../utils";
Expand Down Expand Up @@ -297,6 +297,73 @@ describe(nameof(TsSimpleAst), () => {
});
});

describe(nameof<TsSimpleAst>(t => t.forgetNodesCreatedInBlock), () => {
const ast = new TsSimpleAst();
let sourceFile: SourceFile;
let sourceFileNotNavigated: SourceFile;
let classNode: Node;
let namespaceNode: NamespaceDeclaration;
let namespaceKeywordNode: Node;
let interfaceNode1: Node;
let interfaceNode2: Node;
let interfaceNode3: Node;
let interfaceNode4: Node;
ast.forgetNodesCreatedInBlock(remember => {
sourceFile = ast.addSourceFileFromText("test.ts", "class MyClass {} namespace MyNamespace { interface Interface1 {} interface Interface2 {} " +
"interface Interface3 {} interface Interface4 {} }");
sourceFileNotNavigated = ast.addSourceFileFromText("test2.ts", "class MyClass {}");
classNode = sourceFile.getClassOrThrow("MyClass");
namespaceNode = sourceFile.getNamespaceOrThrow("MyNamespace");

ast.forgetNodesCreatedInBlock(remember2 => {
interfaceNode2 = namespaceNode.getInterfaceOrThrow("Interface2");
interfaceNode3 = namespaceNode.getInterfaceOrThrow("Interface3");
interfaceNode4 = namespaceNode.getInterfaceOrThrow("Interface4");
remember2(interfaceNode3, interfaceNode4);
});

namespaceKeywordNode = namespaceNode.getFirstChildByKindOrThrow(ts.SyntaxKind.NamespaceKeyword);
interfaceNode1 = namespaceNode.getInterfaceOrThrow("Interface1");
remember(interfaceNode1);
});

it("should not have forgotten the source file", () => {
expect(sourceFile.wasForgotten()).to.be.false;
});

it("should not have forgotten the not navigated source file", () => {
expect(sourceFileNotNavigated.wasForgotten()).to.be.false;
});

it("should have forgotten the class", () => {
expect(classNode.wasForgotten()).to.be.true;
});

it("should not have forgotten the namespace because one of its children was remembered", () => {
expect(namespaceNode.wasForgotten()).to.be.false;
});

it("should have forgotten the namespace keyword", () => {
expect(namespaceKeywordNode.wasForgotten()).to.be.true;
});

it("should not have forgotten the first interface because it was remembered", () => {
expect(interfaceNode1.wasForgotten()).to.be.false;
});

it("should have forgotten the second interface", () => {
expect(interfaceNode2.wasForgotten()).to.be.true;
});

it("should not have forgotten the third interface because it was remembered", () => {
expect(interfaceNode3.wasForgotten()).to.be.false;
});

it("should not have forgotten the third interface because it was remembered", () => {
expect(interfaceNode4.wasForgotten()).to.be.false;
});
});

describe("manipulating then getting something from the type checker", () => {
it("should not error after manipulation", () => {
const ast = new TsSimpleAst();
Expand Down
8 changes: 7 additions & 1 deletion src/tests/utils/hashSetTests.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import {expect} from "chai";
import {Es5HashSet} from "./../../utils";
import {Es5HashSet, ArrayUtils} from "./../../utils";

describe(nameof(Es5HashSet), () => {
it("should add values to the hash set and say they exist when they do", () => {
const hashSet = new Es5HashSet<string>();
expect(ArrayUtils.from(hashSet.values())).to.deep.equal([]);
expect(hashSet.has("")).to.be.false;
hashSet.add("");
expect(ArrayUtils.from(hashSet.values())).to.deep.equal([""]);
expect(hashSet.has("")).to.be.true;
expect(hashSet.delete("")).to.be.true;
expect(hashSet.delete("")).to.be.false;
expect(ArrayUtils.from(hashSet.values())).to.deep.equal([]);
expect(hashSet.has("")).to.be.false;
});
});
15 changes: 15 additions & 0 deletions src/utils/HashSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

export interface HashSet<T> {
has(value: T): boolean;
delete(value: T): boolean;
add(value: T): void;
values(): IterableIterator<T>;
}

export class Es5HashSet<T> implements HashSet<T> {
Expand All @@ -21,4 +23,17 @@ export class Es5HashSet<T> implements HashSet<T> {
if (!this.has(value))
this.items.push(value);
}

delete(value: T) {
const index = this.items.indexOf(value);
if (index === -1)
return false;
this.items.splice(index, 1);
return true;
}

*values() {
for (const item of this.items)
yield item;
}
}

0 comments on commit f5a8b39

Please sign in to comment.