Skip to content

Commit

Permalink
feat: add utilities for typescript ast (#1159)
Browse files Browse the repository at this point in the history
'ast-utils.ts' provides typescript ast utility functions
  • Loading branch information
emma-mens authored and hansl committed Jul 22, 2016
1 parent dcaf9ee commit 0cfc2bf
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 1 deletion.
54 changes: 54 additions & 0 deletions addon/ng2/utilities/ast-utils.ts
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);
}
3 changes: 2 additions & 1 deletion addon/ng2/utilities/dynamic-path-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ module.exports = function dynamicPathParser(project, entityName) {
parsedPath.appRoot = appRoot

return parsedPath;
};
};

177 changes: 177 additions & 0 deletions tests/acceptance/ast-utils.spec.ts
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);
}

0 comments on commit 0cfc2bf

Please sign in to comment.