diff --git a/addon/ng2/utilities/ast-utils.ts b/addon/ng2/utilities/ast-utils.ts new file mode 100644 index 000000000000..7b9f1104f33f --- /dev/null +++ b/addon/ng2/utilities/ast-utils.ts @@ -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); +} diff --git a/addon/ng2/utilities/dynamic-path-parser.js b/addon/ng2/utilities/dynamic-path-parser.js index 281f9ea17f42..a00300596b70 100644 --- a/addon/ng2/utilities/dynamic-path-parser.js +++ b/addon/ng2/utilities/dynamic-path-parser.js @@ -55,4 +55,5 @@ module.exports = function dynamicPathParser(project, entityName) { parsedPath.appRoot = appRoot return parsedPath; -}; \ No newline at end of file +}; + diff --git a/tests/acceptance/ast-utils.spec.ts b/tests/acceptance/ast-utils.spec.ts new file mode 100644 index 000000000000..cfc2cf42f346 --- /dev/null +++ b/tests/acceptance/ast-utils.spec.ts @@ -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); +}