Skip to content

feat: add utilities for typescript ast #1159

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 22, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';

/**
Copy link
Contributor

@hansl hansl Jul 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: empty line before to separate.

* 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: space before to separate imports from rest of the code.


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);
}