Skip to content

Commit 0cfc2bf

Browse files
emma-menshansl
authored andcommitted
feat: add utilities for typescript ast (#1159)
'ast-utils.ts' provides typescript ast utility functions
1 parent dcaf9ee commit 0cfc2bf

File tree

3 files changed

+233
-1
lines changed

3 files changed

+233
-1
lines changed

addon/ng2/utilities/ast-utils.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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: ts.Node[] = [];
15+
if (node.kind === kind) {
16+
arr.push(node);
17+
}
18+
return node.getChildren().reduce((foundNodes, child) =>
19+
foundNodes.concat(findNodes(child, kind)), 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,
44+
file: string, 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+
}

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

0 commit comments

Comments
 (0)