Skip to content

Commit

Permalink
feat: #306 - Add Node.forEachChild and Node.forEachDescendant
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret committed Apr 11, 2018
1 parent 7fd8d43 commit 9eabe57
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 42 deletions.
9 changes: 9 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/dist-scripts
/dist/tests
/src
/docs
*.js.map
/obj
/temp
Expand All @@ -14,3 +15,11 @@
*.csproj
*.csproj.user
*.sln
.travis.yml
.npmignore
breaking-changes.md
CHANGELOG.md
gulpfile.js
tsconfig.json
tslint.json
wrapped-nodes.md
1 change: 0 additions & 1 deletion docs/_layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ <h1 onclick="document.location.href = '{{ "/" | prepend: site.baseurl }}'" class
<li class="{% if page.path == 'navigation/directories.md' %}active{% endif %}"><a href="{{ "/navigation/directories" | prepend: site.baseurl }}">Directories</a></li>
<li class="{% if page.path == 'navigation/example.md' %}active{% endif %}"><a href="{{ "/navigation/example" | prepend: site.baseurl }}">Example</a></li>
<li class="{% if page.path == 'navigation/compiler-nodes.md' %}active{% endif %}"><a href="{{ "/navigation/compiler-nodes" | prepend: site.baseurl }}">Compiler Nodes</a></li>
<li class="{% if page.path == 'navigation/existing-nodes.md' %}active{% endif %}"><a href="{{ "/navigation/existing-nodes" | prepend: site.baseurl }}">Existing Nodes</a></li>
<li class="{% if page.path == 'navigation/finding-references.md' %}active{% endif %}"><a href="{{ "/navigation/finding-references" | prepend: site.baseurl }}">Finding References</a></li>
<li class="{% if page.path == 'navigation/language-service.md' %}active{% endif %}"><a href="{{ "/navigation/language-service" | prepend: site.baseurl }}">Language Service</a></li>
<li class="{% if page.path == 'navigation/program.md' %}active{% endif %}"><a href="{{ "/navigation/program" | prepend: site.baseurl }}">Program</a></li>
Expand Down
36 changes: 36 additions & 0 deletions docs/navigation/compiler-nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,39 @@ const nameNode = propertyAccessExpression.getNodeProperty("name"); // returns: N
```

**Note:** This currently only works on properties that are a single node.

## Navigating Existing Compiler Nodes

Sometimes you might want to easily navigate an existing compiler node.

Do that by using the `createWrappedNode` function:

```ts
import {createWrappedNode, ClassDeclaration, ts} from "ts-simple-ast";

// some code that creates a class declaration using the ts object
const classNode: ts.ClassDeclaration = ...;

// create and use a wrapped node
const classDec = createWrappedNode(classNode) as ClassDeclaration;
const firstProperty = classDec.getProperties()[0];

// ... do more stuff here ...
```

**Note:** This is a lightweight way to navigate a node, but there are certian functionalities which will throw an error since there is no
language service, type checker, or program. For example, finding references will not work because that requires a language service.

### Providing Type Checker

If you would like to easily get the type information of the types in the provided source file, then provide a type checker:

```ts
// given an existing node and type checker
const classNode: ts.ClassDeclaration = ...;
const typeChecker: ts.TypeChecker = ...;

// create and use a wrapped node
const classDec = createWrappedNode(classNode, { typeChecker }) as ClassDeclaration;
console.log(classDec.getPropertyOrThrow("propName").getType().getText()); // ok, because a type checker was provided
```
39 changes: 0 additions & 39 deletions docs/navigation/existing-nodes.md

This file was deleted.

Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 61 additions & 1 deletion docs/navigation/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,67 @@ title: Navigating the AST

Navigating the AST should be simple and straightforward.

I will slowly be adding documentation for this area. Right now, the best way to explore what's implemented is to look at the autocompletion/intellisense results
Right now, the best way to explore what's implemented is to look at the autocompletion/intellisense results
or view [this report](https://github.com/dsherret/ts-simple-ast/blob/master/wrapped-nodes.md).

If you can't find something that means it's most likely not implemented and you should [open an issue](https://github.com/dsherret/ts-simple-ast/issues) on GitHub.

### General methods

Search autocomplete for methods like `.getChildren()`, `.getParent()`, `.getFirstChildBySyntaxKind(kind)`, etc...

Many exist. If you find one you would really like, then please [open an issue](https://github.com/dsherret/ts-simple-ast/issues).

### `getChildren()` and `forEachChild`

In general, you can easily navigate the tree by using methods such as `.getClasses()`, `.getClass('MyClass')`, `.getNamespaces()`, and so on, but in some cases you might want to get all the child nodes.

In the compiler API, there exists a `node.getChildren()` method and a `ts.forEachChild(node, cb)`/`node.forEachChild(cb)` function/method.

* `.getChildren()` - Returns all the children including the all the tokens (ex. `OpenBraceToken`, `SemiColonToken` etc.).
* `.forEachChild(child => {})` - Iterates all the child nodes that are properties of the node.

[![getChildren vs forEachKind](images/getChildrenVsForEachKind.gif)](http://ts-ast-viewer.com)

In ts-simple-ast, these methods also exist and they can be used similarly to the compiler API:

```ts
const allChildren = node.getChildren();

node.forEachChild(node => {
console.log(node.getText());
});
```

One major difference between the `.forEachChild` method in ts-simple-ast and the compiler API, is that returning a truthy value in the callback will not stop iteration. If you wish to stop iteration, then use the stop parameter function:

```ts
node.forEachChild((node, stop) => {
console.log(node.getText());

// stop iterating when the node is a class declaration
if (node.getKind() === SyntaxKind.ClassDeclaration)
stop();
});
```

### `forEachDescendant`

If you wish to iterate all the descendants, then use the `forEachDescendant` method:

```ts
node.forEachDescendant((node, stop) => console.log(node.getText()));
```

This is especially useful when writing code that implements a visitor pattern:

```ts
interface Visitor {
visit(node: Node): void;
}

const myVisitors: Visitor[] = ...;

for (const sourceFile of sourceFiles)
sourceFile.forEachDescendant(node => myVisitors.forEach(v => v.visit(node)));
```
59 changes: 58 additions & 1 deletion src/compiler/common/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ export class Node<NodeType extends ts.Node = ts.Node> {
}

/**
* Gets the children of the node.
* Gets all the children of the node.
*/
getChildren(): Node[] {
return this.getCompilerChildren().map(n => this.getNodeFromCompilerNode(n));
Expand Down Expand Up @@ -414,6 +414,63 @@ export class Node<NodeType extends ts.Node = ts.Node> {
return undefined;
}

/**
* Invokes the `cbNode` callback for each child and the `cbNodeArray` for every array of nodes stored in properties of the node.
* If `cbNodeArray` is not defined, then it will pass every element of the array to `cbNode`.
*
* @remarks There exists a `stop` function that exists to stop iteration.
* @param cbNode - Callback invoked for each child.
* @param cbNodeArray - Callback invoked for each array of nodes.
*/
forEachChild(cbNode: (node: Node, stop: () => void) => void, cbNodeArray?: (nodes: Node[], stop: () => void) => void) {
let stop = false;
const stopFunc = () => stop = true;
const nodeCallback = (node: ts.Node) => {
cbNode(this.getNodeFromCompilerNode(node), stopFunc);
return stop;
};
const arrayCallback = cbNodeArray == null ? undefined : (nodes: ts.NodeArray<ts.Node>) => {
cbNodeArray(nodes.map(n => this.getNodeFromCompilerNode(n)), stopFunc);
return stop;
};

this.compilerNode.forEachChild(nodeCallback, arrayCallback);
}

/**
* Invokes the `cbNode` callback for each descendant and the `cbNodeArray` for every array of nodes stored in properties of the node and descendant nodes.
* If `cbNodeArray` is not defined, then it will pass every element of the array to `cbNode`.
*
* @remarks There exists a `stop` function that exists to stop iteration.
* @param cbNode - Callback invoked for each descendant.
* @param cbNodeArray - Callback invoked for each array of nodes.
*/
forEachDescendant(cbNode: (node: Node, stop: () => void) => void, cbNodeArray?: (nodes: Node[], stop: () => void) => void) {
let stop = false;
const stopFunc = () => stop = true;
const nodeCallback = (node: Node) => {
if (stop) return true;
cbNode(node, stopFunc);
if (stop) return true;
node.forEachChild(nodeCallback, arrayCallback);
return stop;
};
const arrayCallback = cbNodeArray == null ? undefined : (nodes: Node[]) => {
if (stop) return true;
cbNodeArray(nodes, stopFunc);
if (stop) return true;

for (const node of nodes) {
node.forEachChild(nodeCallback, arrayCallback);
if (stop) return true;
}

return stop;
};

this.forEachChild(nodeCallback, arrayCallback);
}

/**
* Gets the node's descendants.
*/
Expand Down

0 comments on commit 9eabe57

Please sign in to comment.