-
Notifications
You must be signed in to change notification settings - Fork 12k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add utilities for typescript ast (#1159)
'ast-utils.ts' provides typescript ast utility functions
- Loading branch information
Showing
3 changed files
with
233 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import * as ts from 'typescript'; | ||
import { InsertChange } from './change'; | ||
|
||
/** | ||
* Find all nodes from the AST in the subtree of node of SyntaxKind kind. | ||
* @param node | ||
* @param kind | ||
* @return all nodes of kind kind, or [] if none is found | ||
*/ | ||
export function findNodes(node: ts.Node, kind: ts.SyntaxKind): ts.Node[] { | ||
if (!node) { | ||
return []; | ||
} | ||
let arr: ts.Node[] = []; | ||
if (node.kind === kind) { | ||
arr.push(node); | ||
} | ||
return node.getChildren().reduce((foundNodes, child) => | ||
foundNodes.concat(findNodes(child, kind)), arr); | ||
} | ||
|
||
/** | ||
* Helper for sorting nodes. | ||
* @return function to sort nodes in increasing order of position in sourceFile | ||
*/ | ||
function nodesByPosition(first: ts.Node, second: ts.Node): number { | ||
return first.pos - second.pos; | ||
} | ||
|
||
/** | ||
* Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]` | ||
* or after the last of occurence of `syntaxKind` if the last occurence is a sub child | ||
* of ts.SyntaxKind[nodes[i].kind] and save the changes in file. | ||
* | ||
* @param nodes insert after the last occurence of nodes | ||
* @param toInsert string to insert | ||
* @param file file to insert changes into | ||
* @param fallbackPos position to insert if toInsert happens to be the first occurence | ||
* @param syntaxKind the ts.SyntaxKind of the subchildren to insert after | ||
* @return Change instance | ||
* @throw Error if toInsert is first occurence but fall back is not set | ||
*/ | ||
export function insertAfterLastOccurrence(nodes: ts.Node[], toInsert: string, | ||
file: string, fallbackPos?: number, syntaxKind?: ts.SyntaxKind): Change { | ||
var lastItem = nodes.sort(nodesByPosition).pop(); | ||
if (syntaxKind) { | ||
lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop(); | ||
} | ||
if (!lastItem && fallbackPos == undefined) { | ||
throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`); | ||
} | ||
let lastItemPosition: number = lastItem ? lastItem.end : fallbackPos; | ||
return new InsertChange(file, lastItemPosition, toInsert); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import * as mockFs from 'mock-fs'; | ||
import { expect } from 'chai'; | ||
import * as ts from 'typescript'; | ||
import * as fs from 'fs'; | ||
import { InsertChange, RemoveChange } from '../../addon/ng2/utilities/change'; | ||
import * as Promise from 'ember-cli/lib/ext/promise'; | ||
import { | ||
findNodes, | ||
insertAfterLastOccurrence | ||
} from '../../addon/ng2/utilities/ast-utils'; | ||
|
||
const readFile = Promise.denodeify(fs.readFile); | ||
|
||
describe('ast-utils: findNodes', () => { | ||
const sourceFile = 'tmp/tmp.ts'; | ||
|
||
beforeEach(() => { | ||
let mockDrive = { | ||
'tmp': { | ||
'tmp.ts': `import * as myTest from 'tests' \n` + | ||
'hello.' | ||
} | ||
}; | ||
mockFs(mockDrive); | ||
}); | ||
|
||
afterEach(() => { | ||
mockFs.restore(); | ||
}); | ||
|
||
it('finds no imports', () => { | ||
let editedFile = new RemoveChange(sourceFile, 0, `import * as myTest from 'tests' \n`); | ||
return editedFile | ||
.apply() | ||
.then(() => { | ||
let rootNode = getRootNode(sourceFile); | ||
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); | ||
expect(nodes).to.be.empty; | ||
}); | ||
}); | ||
it('finds one import', () => { | ||
let rootNode = getRootNode(sourceFile); | ||
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); | ||
expect(nodes.length).to.equal(1); | ||
}); | ||
it('finds two imports from inline declarations', () => { | ||
// remove new line and add an inline import | ||
let editedFile = new RemoveChange(sourceFile, 32, '\n'); | ||
return editedFile | ||
.apply() | ||
.then(() => { | ||
let insert = new InsertChange(sourceFile, 32, `import {Routes} from '@angular/routes'`); | ||
return insert.apply(); | ||
}) | ||
.then(() => { | ||
let rootNode = getRootNode(sourceFile); | ||
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); | ||
expect(nodes.length).to.equal(2); | ||
}); | ||
}); | ||
it('finds two imports from new line separated declarations', () => { | ||
let editedFile = new InsertChange(sourceFile, 33, `import {Routes} from '@angular/routes'`); | ||
return editedFile | ||
.apply() | ||
.then(() => { | ||
let rootNode = getRootNode(sourceFile); | ||
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); | ||
expect(nodes.length).to.equal(2); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('ast-utils: insertAfterLastOccurrence', () => { | ||
const sourceFile = 'tmp/tmp.ts'; | ||
beforeEach(() => { | ||
let mockDrive = { | ||
'tmp': { | ||
'tmp.ts': '' | ||
} | ||
}; | ||
mockFs(mockDrive); | ||
}); | ||
|
||
afterEach(() => { | ||
mockFs.restore(); | ||
}); | ||
|
||
it('inserts at beginning of file', () => { | ||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile); | ||
return insertAfterLastOccurrence(imports, `\nimport { Router } from '@angular/router';`, | ||
sourceFile, 0) | ||
.apply() | ||
.then(() => { | ||
return readFile(sourceFile, 'utf8'); | ||
}).then((content) => { | ||
let expected = '\nimport { Router } from \'@angular/router\';'; | ||
expect(content).to.equal(expected); | ||
}); | ||
}); | ||
it('throws an error if first occurence with no fallback position', () => { | ||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile); | ||
expect(() => insertAfterLastOccurrence(imports, `import { Router } from '@angular/router';`, | ||
sourceFile)).to.throw(Error); | ||
}); | ||
it('inserts after last import', () => { | ||
let content = `import { foo, bar } from 'fizz';`; | ||
let editedFile = new InsertChange(sourceFile, 0, content); | ||
return editedFile | ||
.apply() | ||
.then(() => { | ||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile); | ||
return insertAfterLastOccurrence(imports, ', baz', sourceFile, | ||
0, ts.SyntaxKind.Identifier) | ||
.apply(); | ||
}).then(() => { | ||
return readFile(sourceFile, 'utf8'); | ||
}).then(newContent => expect(newContent).to.equal(`import { foo, bar, baz } from 'fizz';`)); | ||
}); | ||
it('inserts after last import declaration', () => { | ||
let content = `import * from 'foo' \n import { bar } from 'baz'`; | ||
let editedFile = new InsertChange(sourceFile, 0, content); | ||
return editedFile | ||
.apply() | ||
.then(() => { | ||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile); | ||
return insertAfterLastOccurrence(imports, `\nimport Router from '@angular/router'`, | ||
sourceFile) | ||
.apply(); | ||
}).then(() => { | ||
return readFile(sourceFile, 'utf8'); | ||
}).then(newContent => { | ||
let expected = `import * from 'foo' \n import { bar } from 'baz'` + | ||
`\nimport Router from '@angular/router'`; | ||
expect(newContent).to.equal(expected); | ||
}); | ||
}); | ||
it('inserts correctly if no imports', () => { | ||
let content = `import {} from 'foo'`; | ||
let editedFile = new InsertChange(sourceFile, 0, content); | ||
return editedFile | ||
.apply() | ||
.then(() => { | ||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile); | ||
return insertAfterLastOccurrence(imports, ', bar', sourceFile, undefined, | ||
ts.SyntaxKind.Identifier) | ||
.apply(); | ||
}).catch(() => { | ||
return readFile(sourceFile, 'utf8'); | ||
}) | ||
.then(newContent => { | ||
expect(newContent).to.equal(content); | ||
// use a fallback position for safety | ||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile); | ||
let pos = findNodes(imports.sort((a, b) => a.pos - b.pos).pop(), | ||
ts.SyntaxKind.CloseBraceToken).pop().pos; | ||
return insertAfterLastOccurrence(imports, ' bar ', | ||
sourceFile, pos, ts.SyntaxKind.Identifier) | ||
.apply(); | ||
}).then(() => { | ||
return readFile(sourceFile, 'utf8'); | ||
}).then(newContent => { | ||
expect(newContent).to.equal(`import { bar } from 'foo'`); | ||
}); | ||
}); | ||
}); | ||
|
||
/** | ||
* Gets node of kind kind from sourceFile | ||
*/ | ||
function getNodesOfKind(kind: ts.SyntaxKind, sourceFile: string) { | ||
return findNodes(getRootNode(sourceFile), kind); | ||
} | ||
|
||
function getRootNode(sourceFile: string) { | ||
return ts.createSourceFile(sourceFile, fs.readFileSync(sourceFile).toString(), | ||
ts.ScriptTarget.ES6, true); | ||
} |