Skip to content

Commit 46c965c

Browse files
committed
feat: add utilities for typescript ast
'ast-utils.ts' provides typescript ast utility functions
1 parent 00e111a commit 46c965c

File tree

3 files changed

+287
-1
lines changed

3 files changed

+287
-1
lines changed

addon/ng2/utilities/ast-utils.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import * as ts from 'typescript';
2+
import { InsertChange } from './change';
3+
/**
4+
* Find all nodes from the AST in the subtree of node of SyntaxKind kind.
5+
* @param node
6+
* @param kind (a valid index of ts.SyntaxKind enum, eg ts.SyntaxKind.ImportDeclaration)
7+
* @return all nodes of kind kind, or [] if none is found
8+
*/
9+
export function findNodes (node: ts.Node, kind: number, arr: ts.Node[] = []): ts.Node[] {
10+
if (node) {
11+
if (node.kind === kind) {
12+
arr.push(node);
13+
}
14+
node.getChildren().forEach(child => findNodes(child, kind, arr));
15+
}
16+
return arr;
17+
}
18+
19+
/**
20+
* @param nodes (nodes to sort)
21+
* @return (nodes sorted by their position from the source file
22+
* or [] if nodes is empty)
23+
*/
24+
export function sortNodesByPosition(nodes: ts.Node[]): ts.Node[] {
25+
return nodes.sort((first, second) => {return first.pos - second.pos;});
26+
}
27+
28+
/**
29+
* Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]`
30+
* or after the last of occurence of `syntaxKind` if the last occurence is a sub child
31+
* of ts.SyntaxKind[nodes[i].kind] and save the changes in file.
32+
*
33+
* @param nodes (insert after the last occurence of nodes)
34+
* @param toInsert (string to insert)
35+
* @param file (file to insert changes into)
36+
* @param fallbackPos (position to insert if toInsert happens to be the first occurence)
37+
* @param syntaxKind (the ts.SyntaxKind of the subchildren to insert after)
38+
* @return Change instance
39+
* @throw Error if toInsert is first occurence but fall back is not set
40+
*/
41+
export function insertAfterLastOccurence(nodes: ts.Node[], toInsert: string, file: string,
42+
fallbackPos?: number, syntaxKind?: ts.SyntaxKind): Change {
43+
var lastItem = sortNodesByPosition(nodes).pop();
44+
if (syntaxKind) {
45+
lastItem = sortNodesByPosition(findNodes(lastItem, syntaxKind)).pop();
46+
}
47+
if (!lastItem && fallbackPos == undefined) {
48+
throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`);
49+
}
50+
let lastItemPosition: number = lastItem ? lastItem.end : fallbackPos;
51+
return new InsertChange(file, lastItemPosition, toInsert);
52+
}
53+

addon/ng2/utilities/dynamic-path-parser.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,5 @@ module.exports = function dynamicPathParser(project, entityName) {
5555
parsedPath.appRoot = appRoot
5656

5757
return parsedPath;
58-
};
58+
};
59+

tests/acceptance/ast-utils.spec.ts

+232
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import * as mockFs from 'mock-fs';
2+
import { expect } from 'chai';
3+
import * as ts from 'typescript';
4+
import * as fs from 'fs';
5+
import { InsertChange, RemoveChange } from '../../addon/ng2/utilities/change';
6+
import {findNodes,
7+
sortNodesByPosition,
8+
insertAfterLastOccurence} from '../../addon/ng2/utilities/ast-utils';
9+
import * as Promise from 'ember-cli/lib/ext/promise';
10+
const readFile = Promise.denodeify(fs.readFile);
11+
12+
describe('ast-utils: findNodes', () => {
13+
const sourceFile = 'tmp/tmp.ts';
14+
15+
beforeEach(() => {
16+
let mockDrive = {
17+
'tmp': {
18+
'tmp.ts': `import * as myTest from 'tests' \n` +
19+
'hello.'
20+
}
21+
};
22+
mockFs(mockDrive);
23+
});
24+
25+
afterEach(() => {
26+
mockFs.restore();
27+
});
28+
29+
it('finds no imports', () => {
30+
let editedFile = new RemoveChange(sourceFile, 0, `import * as myTest from 'tests' \n`);
31+
return editedFile
32+
.apply()
33+
.then(() => {
34+
let rootNode = getRootNode(sourceFile);
35+
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
36+
expect(nodes).to.be.empty;
37+
});
38+
});
39+
it('finds one import', () => {
40+
let rootNode = getRootNode(sourceFile);
41+
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
42+
expect(nodes.length).to.equal(1);
43+
});
44+
it('finds two imports from inline declarations', () => {
45+
// remove new line and add an inline import
46+
let editedFile = new RemoveChange(sourceFile, 32, '\n');
47+
return editedFile
48+
.apply()
49+
.then(() => {
50+
let insert = new InsertChange(sourceFile, 32, `import {Routes} from '@angular/routes'`);
51+
return insert.apply();
52+
})
53+
.then(() => {
54+
let rootNode = getRootNode(sourceFile);
55+
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
56+
expect(nodes.length).to.equal(2);
57+
});
58+
});
59+
it('finds two imports from new line separated declarations', () => {
60+
let editedFile = new InsertChange(sourceFile, 33, `import {Routes} from '@angular/routes'`);
61+
return editedFile
62+
.apply()
63+
.then(() => {
64+
let rootNode = getRootNode(sourceFile);
65+
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
66+
expect(nodes.length).to.equal(2);
67+
});
68+
});
69+
});
70+
71+
describe('ast-utils: sortNodesByPosition', () => {
72+
const sourceFile = 'tmp/tmp.ts';
73+
beforeEach(() => {
74+
let mockDrive = {
75+
'tmp': {
76+
'tmp.ts': ''
77+
}
78+
};
79+
mockFs(mockDrive);
80+
});
81+
82+
afterEach(() => {
83+
mockFs.restore();
84+
});
85+
86+
it('gives an empty array', () => {
87+
let nodes = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
88+
let sortedNodes = sortNodesByPosition(nodes);
89+
expect(sortedNodes).to.be.empty;
90+
});
91+
92+
it('returns unity array', () => {
93+
let editedFile = new InsertChange(sourceFile, 0, `import * as ts from 'ts'`);
94+
return editedFile
95+
.apply()
96+
.then(() => {
97+
let nodes = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
98+
let sortedNodes = sortNodesByPosition(nodes);
99+
expect(sortedNodes.length).to.equal(1);
100+
expect(sortedNodes[0].pos).to.equal(0);
101+
});
102+
});
103+
it('returns a sorted array of three components', () => {
104+
let content = `import {Router} from '@angular/router'\n` +
105+
`import * as fs from 'fs'` +
106+
`import { Component} from '@angular/core'\n`;
107+
let editedFile = new InsertChange(sourceFile, 0, content);
108+
return editedFile
109+
.apply()
110+
.then(() => {
111+
let nodes = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
112+
// shuffle up nodes
113+
let shuffledNodes = [nodes[1], nodes[2], nodes[0]];
114+
expect(shuffledNodes[0].pos).to.equal(38);
115+
expect(shuffledNodes[1].pos).to.equal(63);
116+
expect(shuffledNodes[2].pos).to.equal(0);
117+
118+
let sortedNodes = sortNodesByPosition(shuffledNodes);
119+
expect(sortedNodes.length).to.equal(3);
120+
expect(sortedNodes[0].pos).to.equal(0);
121+
expect(sortedNodes[1].pos).to.equal(38);
122+
expect(sortedNodes[2].pos).to.equal(63);
123+
});
124+
});
125+
});
126+
127+
describe('ast-utils: insertAfterLastOccurence', () => {
128+
const sourceFile = 'tmp/tmp.ts';
129+
beforeEach(() => {
130+
let mockDrive = {
131+
'tmp': {
132+
'tmp.ts': ''
133+
}
134+
};
135+
mockFs(mockDrive);
136+
});
137+
138+
afterEach(() => {
139+
mockFs.restore();
140+
});
141+
142+
it('inserts at beginning of file', () => {
143+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
144+
return insertAfterLastOccurence(imports, `\nimport { Router } from '@angular/router';`,
145+
sourceFile, 0)
146+
.apply()
147+
.then(() => {
148+
return readFile(sourceFile, 'utf8');
149+
}).then((content) => {
150+
let expected = '\nimport { Router } from \'@angular/router\';';
151+
expect(content).to.equal(expected);
152+
});
153+
});
154+
it('throws an error if first occurence with no fallback position', () => {
155+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
156+
expect(() => insertAfterLastOccurence(imports, `import { Router } from '@angular/router';`,
157+
sourceFile)).to.throw(Error);
158+
});
159+
it('inserts after last import', () => {
160+
let content = `import { foo, bar } from 'fizz';`;
161+
let editedFile = new InsertChange(sourceFile, 0, content);
162+
return editedFile
163+
.apply()
164+
.then(() => {
165+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
166+
return insertAfterLastOccurence(imports, ', baz', sourceFile,
167+
0, ts.SyntaxKind.Identifier)
168+
.apply();
169+
}).then(() => {
170+
return readFile(sourceFile, 'utf8');
171+
}).then(newContent => expect(newContent).to.equal(`import { foo, bar, baz } from 'fizz';`));
172+
});
173+
it('inserts after last import declaration', () => {
174+
let content = `import * from 'foo' \n import { bar } from 'baz'`;
175+
let editedFile = new InsertChange(sourceFile, 0, content);
176+
return editedFile
177+
.apply()
178+
.then(() => {
179+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
180+
return insertAfterLastOccurence(imports, `\nimport Router from '@angular/router'`,
181+
sourceFile)
182+
.apply();
183+
}).then(() => {
184+
return readFile(sourceFile, 'utf8');
185+
}).then(newContent => {
186+
let expected = `import * from 'foo' \n import { bar } from 'baz'` +
187+
`\nimport Router from '@angular/router'`;
188+
expect(newContent).to.equal(expected);
189+
});
190+
});
191+
it('inserts correctly if no imports', () => {
192+
let content = `import {} from 'foo'`;
193+
let editedFile = new InsertChange(sourceFile, 0, content);
194+
return editedFile
195+
.apply()
196+
.then(() => {
197+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
198+
return insertAfterLastOccurence(imports, ', bar', sourceFile, undefined,
199+
ts.SyntaxKind.Identifier)
200+
.apply();
201+
}).catch(() => {
202+
return readFile(sourceFile, 'utf8');
203+
})
204+
.then(newContent => {
205+
expect(newContent).to.equal(content);
206+
// use a fallback position for safety
207+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
208+
let pos = findNodes(sortNodesByPosition(imports).pop(),
209+
ts.SyntaxKind.CloseBraceToken).pop().pos;
210+
return insertAfterLastOccurence(imports, ' bar ',
211+
sourceFile, pos, ts.SyntaxKind.Identifier)
212+
.apply();
213+
}).then(() => {
214+
return readFile(sourceFile, 'utf8');
215+
}).then(newContent => {
216+
expect(newContent).to.equal(`import { bar } from 'foo'`);
217+
});
218+
});
219+
});
220+
221+
/**
222+
* Gets node of kind kind from sourceFile
223+
*/
224+
function getNodesOfKind(kind: ts.SyntaxKind, sourceFile: string) {
225+
return findNodes(getRootNode(sourceFile), kind);
226+
}
227+
228+
function getRootNode(sourceFile: string) {
229+
return ts.createSourceFile(sourceFile, fs.readFileSync(sourceFile).toString(),
230+
ts.ScriptTarget.ES6, true);
231+
}
232+

0 commit comments

Comments
 (0)