Skip to content

Commit 131e58d

Browse files
committed
feat: add utilities for typescript ast
'ast-utils.ts' provides typescript ast utility functions
1 parent c85b14f commit 131e58d

File tree

3 files changed

+234
-1
lines changed

3 files changed

+234
-1
lines changed

addon/ng2/utilities/ast-utils.ts

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

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

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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 * as Promise from 'ember-cli/lib/ext/promise';
7+
import {
8+
findNodes,
9+
insertAfterLastOccurrence} from '../../addon/ng2/utilities/ast-utils';
10+
11+
const readFile = Promise.denodeify(fs.readFile);
12+
13+
describe('ast-utils: findNodes', () => {
14+
const sourceFile = 'tmp/tmp.ts';
15+
16+
beforeEach(() => {
17+
let mockDrive = {
18+
'tmp': {
19+
'tmp.ts': `import * as myTest from 'tests' \n` +
20+
'hello.'
21+
}
22+
};
23+
mockFs(mockDrive);
24+
});
25+
26+
afterEach(() => {
27+
mockFs.restore();
28+
});
29+
30+
it('finds no imports', () => {
31+
let editedFile = new RemoveChange(sourceFile, 0, `import * as myTest from 'tests' \n`);
32+
return editedFile
33+
.apply()
34+
.then(() => {
35+
let rootNode = getRootNode(sourceFile);
36+
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
37+
expect(nodes).to.be.empty;
38+
});
39+
});
40+
it('finds one import', () => {
41+
let rootNode = getRootNode(sourceFile);
42+
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
43+
expect(nodes.length).to.equal(1);
44+
});
45+
it('finds two imports from inline declarations', () => {
46+
// remove new line and add an inline import
47+
let editedFile = new RemoveChange(sourceFile, 32, '\n');
48+
return editedFile
49+
.apply()
50+
.then(() => {
51+
let insert = new InsertChange(sourceFile, 32, `import {Routes} from '@angular/routes'`);
52+
return insert.apply();
53+
})
54+
.then(() => {
55+
let rootNode = getRootNode(sourceFile);
56+
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
57+
expect(nodes.length).to.equal(2);
58+
});
59+
});
60+
it('finds two imports from new line separated declarations', () => {
61+
let editedFile = new InsertChange(sourceFile, 33, `import {Routes} from '@angular/routes'`);
62+
return editedFile
63+
.apply()
64+
.then(() => {
65+
let rootNode = getRootNode(sourceFile);
66+
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
67+
expect(nodes.length).to.equal(2);
68+
});
69+
});
70+
});
71+
72+
describe('ast-utils: insertAfterLastOccurrence', () => {
73+
const sourceFile = 'tmp/tmp.ts';
74+
beforeEach(() => {
75+
let mockDrive = {
76+
'tmp': {
77+
'tmp.ts': ''
78+
}
79+
};
80+
mockFs(mockDrive);
81+
});
82+
83+
afterEach(() => {
84+
mockFs.restore();
85+
});
86+
87+
it('inserts at beginning of file', () => {
88+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
89+
return insertAfterLastOccurrence(imports, `\nimport { Router } from '@angular/router';`,
90+
sourceFile, 0)
91+
.apply()
92+
.then(() => {
93+
return readFile(sourceFile, 'utf8');
94+
}).then((content) => {
95+
let expected = '\nimport { Router } from \'@angular/router\';';
96+
expect(content).to.equal(expected);
97+
});
98+
});
99+
it('throws an error if first occurence with no fallback position', () => {
100+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
101+
expect(() => insertAfterLastOccurrence(imports, `import { Router } from '@angular/router';`,
102+
sourceFile)).to.throw(Error);
103+
});
104+
it('inserts after last import', () => {
105+
let content = `import { foo, bar } from 'fizz';`;
106+
let editedFile = new InsertChange(sourceFile, 0, content);
107+
return editedFile
108+
.apply()
109+
.then(() => {
110+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
111+
return insertAfterLastOccurrence(imports, ', baz', sourceFile,
112+
0, ts.SyntaxKind.Identifier)
113+
.apply();
114+
}).then(() => {
115+
return readFile(sourceFile, 'utf8');
116+
}).then(newContent => expect(newContent).to.equal(`import { foo, bar, baz } from 'fizz';`));
117+
});
118+
it('inserts after last import declaration', () => {
119+
let content = `import * from 'foo' \n import { bar } from 'baz'`;
120+
let editedFile = new InsertChange(sourceFile, 0, content);
121+
return editedFile
122+
.apply()
123+
.then(() => {
124+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
125+
return insertAfterLastOccurrence(imports, `\nimport Router from '@angular/router'`,
126+
sourceFile)
127+
.apply();
128+
}).then(() => {
129+
return readFile(sourceFile, 'utf8');
130+
}).then(newContent => {
131+
let expected = `import * from 'foo' \n import { bar } from 'baz'` +
132+
`\nimport Router from '@angular/router'`;
133+
expect(newContent).to.equal(expected);
134+
});
135+
});
136+
it('inserts correctly if no imports', () => {
137+
let content = `import {} from 'foo'`;
138+
let editedFile = new InsertChange(sourceFile, 0, content);
139+
return editedFile
140+
.apply()
141+
.then(() => {
142+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
143+
return insertAfterLastOccurrence(imports, ', bar', sourceFile, undefined,
144+
ts.SyntaxKind.Identifier)
145+
.apply();
146+
}).catch(() => {
147+
return readFile(sourceFile, 'utf8');
148+
})
149+
.then(newContent => {
150+
expect(newContent).to.equal(content);
151+
// use a fallback position for safety
152+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
153+
let pos = findNodes(imports.sort((a, b) => a.pos - b.pos).pop(),
154+
ts.SyntaxKind.CloseBraceToken).pop().pos;
155+
return insertAfterLastOccurrence(imports, ' bar ',
156+
sourceFile, pos, ts.SyntaxKind.Identifier)
157+
.apply();
158+
}).then(() => {
159+
return readFile(sourceFile, 'utf8');
160+
}).then(newContent => {
161+
expect(newContent).to.equal(`import { bar } from 'foo'`);
162+
});
163+
});
164+
});
165+
166+
/**
167+
* Gets node of kind kind from sourceFile
168+
*/
169+
function getNodesOfKind(kind: ts.SyntaxKind, sourceFile: string) {
170+
return findNodes(getRootNode(sourceFile), kind);
171+
}
172+
173+
function getRootNode(sourceFile: string) {
174+
return ts.createSourceFile(sourceFile, fs.readFileSync(sourceFile).toString(),
175+
ts.ScriptTarget.ES6, true);
176+
}
177+

0 commit comments

Comments
 (0)