-
Notifications
You must be signed in to change notification settings - Fork 12k
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.