8000 feat: add utilities for typescript ast · angular/angular-cli@f20747a · GitHub
[go: up one dir, main page]

Skip to content

Commit f20747a

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

File tree

3 files changed

+289
-1
lines changed

3 files changed

+289
-1
lines changed

addon/ng2/utilities/ast-utils.ts

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

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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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+
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: sortNodesByPosition', () => {
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('gives an empty array', () => {
88+
let nodes = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
89+
let sortedNodes = sortNodesByPosition(nodes);
90+
expect(sortedNodes).to.be.empty;
91+
});
92+
93+
it('returns unity array', () => {
94+
let editedFile = new InsertChange(sourceFile, 0, `import * as ts from 'ts'`);
95+
return editedFile
96+
.apply()
97+
.then(() => {
98+
let nodes = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
99+
let sortedNodes = sortNodesByPosition(nodes);
100+
expect(sortedNodes.length).to.equal(1);
101+
expect(sortedNodes[0].pos).to.equal(0);
102+
});
103+
});
104+
it('returns a sorted array of three components', () => {
105+
let content = `import {Router} from '@angular/router'\n` +
106+
`import * as fs from 'fs'` +
107+
`import { Component} from '@angular/core'\n`;
108+
let editedFile = new InsertChange(sourceFile, 0, content);
109+
return editedFile
110+
.apply()
111+
.then(() => {
112+
let nodes = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
113+
// shuffle up nodes
114+
let shuffledNodes = [nodes[1], nodes[2], nodes[0]];
115+
expect(shuffledNodes[0].pos).to.equal(38);
116+
expect(shuffledNodes[1].pos).to.equal(63);
117+
expect(shuffledNodes[2].pos).to.equal(0);
118+
119+
let sortedNodes = sortNodesByPosition(shuffledNodes);
120+
expect(sortedNodes.length).to.equal(3);
121+
expect(sortedNodes[0].pos).to.equal(0);
122+
expect(sortedNodes[1].pos).to.equal(38);
123+
expect(sortedNodes[2].pos).to.equal(63);
124+
});
125+
});
126+
});
127+
128+
describe('ast-utils: insertAfterLastOccurence', () => {
129+
const sourceFile = 'tmp/tmp.ts';
130+
beforeEach(() => {
131+
let mockDrive = {
132+
'tmp': {
133+
'tmp.ts': ''
134+
}
135+
};
136+
mockFs(mockDrive);
137+
});
138+
139+
afterEach(() => {
140+
mockFs.restore();
141+
});
142+
143+
it('inserts at beginning of file', () => {
144+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
145+
return insertAfterLastOccurence(imports, `\nimport { Router } from '@angular/router';`,
146+
sourceFile, 0)
147+
.apply()
148+
.then(() => {
149+
return readFile(sourceFile, 'utf8');
150+
}).then((content) => {
151+
let expected = '\nimport { Router } from \'@angular/router\';';
152+
expect(content).to.equal(expected);
153+
});
154+
});
155+
it('throws an error if first occurence with no fallback position', () => {
156+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
157+
expect(() => insertAfterLastOccurence(imports, `import { Router } from '@angular/router';`,
158+
sourceFile)).to.throw(Error);
159+
});
160+
it('inserts after last import', () => {
161+
let content = `import { foo, bar } from 'fizz';`;
162+
let editedFile = new InsertChange(sourceFile, 0, content);
163+
return editedFile
164+
.apply()
165+
.then(() => {
166+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
167+
return insertAfterLastOccurence(imports, ', baz', sourceFile,
168+
0, ts.SyntaxKind.Identifier)
169+
.apply();
170+
}).then(() => {
171+
return readFile(sourceFile, 'utf8');
172+
}).then(newContent => expect(newContent).to.equal(`import { foo, bar, baz } from 'fizz';`));
173+
});
174+
it('inserts after last import declaration', () => {
175+
let content = `import * from 'foo' \n import { bar } from 'baz'`;
176+
let editedFile = new InsertChange(sourceFile, 0, content);
177+
return editedFile
178+
.apply()
179+
.then(() => {
180+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
181+
return insertAfterLastOccurence(imports, `\nimport Router from '@angular/router'`,
182+
sourceFile)
183+
.apply();
184+
}).then(() => {
185+
return readFile(sourceFile, 'utf8');
186+
}).then(newContent => {
187+
let expected = `import * from 'foo' \n import { bar } from 'baz'` +
188+
`\nimport Router from '@angular/router'`;
189+
expect(newContent).to.equal(expected);
190+
});
191+
});
192+
it('inserts correctly if no imports', () => {
193+
let content = `import {} from 'foo'`;
194+
let editedFile = new InsertChange(sourceFile, 0, content);
195+
return editedFile
196+
.apply()
197+
.then(() => {
198+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
199+
return insertAfterLastOccurence(imports, ', bar', sourceFile, undefined,
200+
ts.SyntaxKind.Identifier)
201+
.apply();
202+
}).catch(() => {
203+
return readFile(sourceFile, 'utf8');
204+
})
205+
.then(newContent => {
206+
expect(newContent).to.equal(content);
207+
// use a fallback position for safety
208+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
209+
let pos = findNodes(sortNodesByPosition(imports).pop(),
210+
ts.SyntaxKind.CloseBraceToken).pop().pos;
211+
return insertAfterLastOccurence(imports, ' bar ',
212+
sourceFile, pos, ts.SyntaxKind.Identifier)
213+
.apply();
214+
}).then(() => {
215+
return readFile(sourceFile, 'utf8');
216+
}).then(newContent => {
217+
expect(newContent).to.equal(`import { bar } from 'foo'`);
218+
});
219+
});
220+
});
221+
222+
/**
223+
* Gets node of kind kind from sourceFile
224+
*/
225+
function getNodesOfKind(kind: ts.SyntaxKind, sourceFile: string) {
226+
return findNodes(getRootNode(sourceFile), kind);
227+
}
228+
229+
function getRootNode(sourceFile: string) {
230+
return ts.createSourceFile(sourceFile, fs.readFileSync(sourceFile).toString(),
231+
ts.ScriptTarget.ES6, true);
232+
}
233+

0 commit comments

Comments
 (0)
0