as2dts
Version:
A command line tool that converts ActionScript 3 classes and interfaces to TypeScript definitions (d.ts files)
811 lines (729 loc) • 22.4 kB
text/typescript
import Token = require('./token');
import NodeKind = require('./nodeKind');
import Operators = require('./operators');
import KeyWords = require('./keywords');
import Node = require('./node');
//Utils
function assign(target: any, ...items: any[]): any {
return items.reduce(function (target: any, source: any) {
return Object.keys(source).reduce((target: any, key: string) => {
target[key] = source[key];
return target;
}, target);
}, target);
}
var keywordsCheck: { [index: string]: boolean }
= Object.keys(KeyWords).reduce(
(result: { [index: string]: boolean}, key: string) => {
var keyword: string = (<any>KeyWords)[key];
result[keyword] = true;
return result;
}, <{ [index: string]: boolean}>{}
);
function isKeyWord(text: string): boolean {
return !!keywordsCheck[text];
}
function transformAST(node:Node, parentNode: Node): Node {
//we don't care about comment
var newNode= new Node(
node.content(),
node.kind,
node.start,
node.end,
node.text,
[],
parentNode
);
newNode.children = node.children.filter(
child => !!child &&
child.kind !== NodeKind.AS_DOC &&
child.kind !== NodeKind.MULTI_LINE_COMMENT
).map(child => transformAST(child, newNode))
return newNode;
}
interface Scope {
parent: Scope;
declarations: Declaration[];
isTopLevel: boolean;
}
var globVars = [
'undefined', 'NaN', 'Infinity',
'Array', 'Boolean', 'decodeURI', 'decodeURIComponent', 'encodeURI', 'encodeURIComponent', 'escape',
'int', 'isFinite', 'isNaN', 'isXMLName', 'Number', 'Object',
'parseFloat', 'parseInt', 'String', 'trace', 'uint', 'unescape', 'Vector', 'XML', 'XMLList',
'ArgumentError', 'arguments', 'Class', 'Date', 'DefinitionError', 'Error', 'EvalError', 'Function', 'Math', 'Namespace', 'QName', 'RangeError', 'ReferenceError', 'RegExp', 'SecurityError', 'SyntaxError', 'TypeError', 'URIError', 'VerifyError'
];
interface Declaration {
bound?: string;
name: string;
}
export interface EmitterOptions {
defsOnly?: boolean;
}
var defaultEmitterOptions = {
defsOnly: false
}
var data: {
source: string;
options: EmitterOptions;
}
var state: {
index: number;
currentClassName: string;
inConstructor: boolean;
scope: Scope;
isNew: boolean;
emitThisForNextIdent: boolean;
parentNode?: Node
};
var output: string;
export function emit(ast: Node, source: string, options?: EmitterOptions) {
data = {
source: source,
options: assign(defaultEmitterOptions, options || {})
};
Object.freeze(data);
state = {
index: 0,
currentClassName: '',
inConstructor: false,
scope: null,
isNew: false,
emitThisForNextIdent: true
}
output = '';
enterScope([]);
visitNode(transformAST(ast, null));
catchup(data.source.length -1);
exitScope();
return output;
}
function visitNodes(nodes: Node[]) {
if (!nodes) {
return;
}
nodes.forEach(node => visitNode(node));
}
var visitors: {[kind: string]: (node: Node) => void } = {};
visitors[NodeKind.PACKAGE] = emitPackage;
visitors[NodeKind.META] = emitMeta;
visitors[NodeKind.IMPORT] = emitImport;
visitors[NodeKind.INCLUDE] = visitors[NodeKind.USE] = emitInclude;
visitors[NodeKind.FUNCTION] = emitFunction;
visitors[NodeKind.LAMBDA] = emitFunction;
visitors[NodeKind.INTERFACE] = emitInterface;
visitors[NodeKind.CLASS] = emitClass;
visitors[NodeKind.VECTOR] = emitVector;
visitors[NodeKind.SHORT_VECTOR] = emitShortVector;
visitors[NodeKind.TYPE] = emitNodeText;
visitors[NodeKind.NAME] = emitNodeText;
visitors[NodeKind.EXTENDS] = emitNodeText;
visitors[NodeKind.IMPLEMENTS] = emitNodeText;
visitors[NodeKind.CALL] = emitCall;
visitors[NodeKind.NEW] = emitNew;
visitors[NodeKind.RELATION] = emitRelation;
visitors[NodeKind.OP] = emitOp;
visitors[NodeKind.IDENTIFIER] = emitIdent;
visitors[NodeKind.XML_LITERAL] = emitXMLLiteral;
visitors[NodeKind.CONST_LIST] = emitConstList;
visitors[NodeKind.REST] = emitRest;
function visitNode(node: Node)
{
if (!node)
return;
var defsOnly = data.options.defsOnly;
if (defsOnly && node.kind === NodeKind.CONTENT && node.parent.kind !== NodeKind.PACKAGE)
{
// skip anything outside a package block
catchup(node.start);
skipTo(node.subtreeEnd);
}
else if (defsOnly && node.kind === NodeKind.NAME && node.findParent(NodeKind.PARAMETER) && (node.parent.findChild(NodeKind.INIT) || state.inConstructor))
{
// denote optional parameters
emitNodeText(node);
insert('?');
}
else if (defsOnly && node.kind === NodeKind.INIT && node.parent.kind === NodeKind.NAME_TYPE_INIT)
{
// skip default values
skipTo(node.subtreeEnd);
}
else if (defsOnly && node.kind === NodeKind.BLOCK
&& (node.parent.kind === NodeKind.FUNCTION || node.parent.kind === NodeKind.GET || node.parent.kind === NodeKind.SET))
{
// skip children of function block
skipTo(node.subtreeEnd);
if (state.inConstructor)
{
if (node.findParent(NodeKind.CLASS).findChild(NodeKind.EXTENDS))
insert(' { super(); }');
else
insert(' { }');
}
else if (node.parent.kind === NodeKind.SET)
{
insert(' { }');
}
else
{
insert(' { return null as any; }');
}
}
else if (visitors.hasOwnProperty(node.kind))
{
visitors[node.kind](node);
}
else
{
catchup(node.start);
visitNodes(node.children);
}
}
var GLOBAL_MODULE:string = '__global__';
var globalExports:string[] = [];
function emitRest(node:Node) {
// emit "...rest"
catchup(node.end);
insert(':');
// emit type
visitNodes(node.children);
}
function emitPackage(node: Node) {
catchup(node.start);
skip(NodeKind.PACKAGE.length);
insert('module');
if (!node.findChild(NodeKind.NAME).text)
insert(' ' + GLOBAL_MODULE);
visitNodes(node.children);
catchup(node.subtreeEnd);
var name:string;
while (name = globalExports.shift())
insert('\nimport ' + name + ' = ' + GLOBAL_MODULE + '.' + name + ';');
}
function emitMeta(node: Node) {
catchup(node.start);
commentNode(node, false);
}
function emitInclude(node: Node) {
catchup(node.start);
commentNode(node, true);
}
function emitImport(node: Node) {
catchup(node.start + NodeKind.IMPORT.length + 1);
var split = node.text.split('.');
var name = split[split.length -1];
insert(name + ' = ');
catchup(node.end);
state.scope.declarations.push({ name: name });
}
function emitInterface(node: Node) {
var exported = emitDeclaration(node);
//we'll catchup the other part
var name:string = node.findChild(NodeKind.NAME).text;
state.scope.declarations.push({ name: name });
var content = node.findChild(NodeKind.CONTENT)
var contentsNode = content && content.children;
var foundVariables: { [name: string]: boolean } = {};
if (contentsNode) {
contentsNode.forEach(node => {
visitNode(node.findChild(NodeKind.META_LIST));
catchup(node.start);
if (node.kind === NodeKind.FUNCTION) {
skip(8);
visitNodes(node.children);
} else if (node.kind === NodeKind.GET || node.kind === NodeKind.SET) {
var name = node.findChild(NodeKind.NAME),
paramerterList = node.findChild(NodeKind.PARAMETER_LIST);
if (!foundVariables[name.text]) {
skipTo(name.start);
catchup(name.end);
foundVariables[name.text] = true;
if (node.kind === NodeKind.GET) {
skipTo(paramerterList.end);
var type = node.findChild(NodeKind.TYPE);
if (type) {
emitNodeText(type);
}
} else if (node.kind === NodeKind.SET) {
var setParam = paramerterList
.findChild(NodeKind.PARAMETER).children[0];
skipTo(setParam.findChild(NodeKind.NAME).end)
var type = setParam.findChild(NodeKind.TYPE);
if (type) {
emitNodeText(type);
}
skipTo(node.end);
}
} else {
commentNode(node, true);
}
} else {
//include or import in interface content not supported
commentNode(node, true);
}
})
}
catchup(node.subtreeEnd);
if (exported)
insert('\nexport var ' + name + ':new(..._:any[])=>' + name + ';');
}
function emitFunction(node: Node) {
emitDeclaration(node);
enterFunctionScope(node);
var rest = node.getChildrenStartingFrom(NodeKind.MOD_LIST);
exitScope();
visitNodes(rest);
}
function emitClass(node: Node) {
emitDeclaration(node);
var name = node.findChild(NodeKind.NAME);
state.currentClassName = name.text.split('<')[0];
visitNode(name);
var content = node.findChild(NodeKind.CONTENT)
var contentsNode = content && content.children;
if (contentsNode) {
//collects declarations
enterClassScope(contentsNode);
contentsNode.forEach(node => {
visitNode(node.findChild(NodeKind.META_LIST));
catchup(node.start);
switch (node.kind) {
case NodeKind.SET:
emitSet(node);
break;
case NodeKind.GET:
case NodeKind.FUNCTION:
emitMethod(node);
break;
case NodeKind.VAR_LIST:
emitPropertyDecl(node);
break;
case NodeKind.CONST_LIST:
emitPropertyDecl(node, true);
break;
default:
visitNode(node);
}
});
exitScope();
}
state.currentClassName = null;
}
function emitSet(node: Node) {
if (!emitClassField(node))
{
skipTo(node.subtreeEnd);
return;
}
var name = node.findChild(NodeKind.NAME);
consume('function', name.start);
var params = node.findChild(NodeKind.PARAMETER_LIST)
visitNode(params);
catchup(params.end)
var type = node.findChild(NodeKind.TYPE);
if (type) {
skipTo(type.end);
}
enterFunctionScope(node);
visitNodes(node.getChildrenStartingFrom(NodeKind.TYPE));
exitScope();
}
function emitConstList(node: Node) {
catchup(node.start);
var nameTypeInit = node.findChild(NodeKind.NAME_TYPE_INIT);
skipTo(nameTypeInit.start);
insert('var ');
visitNode(nameTypeInit);
}
function emitMethod(node: Node) {
var name:Node = node.findChild(NodeKind.NAME);
state.inConstructor = node.kind === NodeKind.FUNCTION && name.text === state.currentClassName;
if (state.inConstructor)
{
var mods = node.findChild(NodeKind.MOD_LIST);
if (mods) {
catchup(mods.start);
}
insert('constructor');
skipTo(name.end)
}
else if (emitClassField(node))
{
consume('function', name.start);
emitNodeText(name);
}
else
{
skipTo(node.subtreeEnd);
return;
}
enterFunctionScope(node);
visitNodes(node.getChildrenStartingFrom(NodeKind.NAME));
exitScope();
state.inConstructor = false;
}
function emitPropertyDecl(node: Node, isConst = false) {
var names = node.findChildren(NodeKind.NAME_TYPE_INIT);
if (!emitClassField(node))
{
skipTo(node.subtreeEnd);
return;
}
for (var i:number = 0; i < names.length; i++)
{
var name:Node = names[i];
if (i == 0)
{
consume(isConst ? 'const' : 'var', name.start);
visitNode(name);
}
else
{
insert('; ');
emitClassField(node, true);
skipTo(name.start);
visitNode(name);
}
}
}
function emitClassField(node:Node, again:boolean = false):boolean {
var mods = node.findChild(NodeKind.MOD_LIST);
if (mods) {
catchup(mods.start);
if (data.options.defsOnly
&& (mods.findChild(NodeKind.MODIFIER, 'private')
|| mods.findChild(NodeKind.MODIFIER, 'internal')))
{
return false;
}
mods.children.forEach(node => {
catchup(node.start);
if (node.text === 'private' || node.text === 'public' || node.text === 'protected' || node.text === 'static')
{
if (again)
insert(node.text + ' ');
}
else
{
commentNode(node, false);
}
catchup(node.end);
});
}
return true;
}
function emitDeclaration(node: Node):boolean {
catchup(node.start);
visitNode(node.findChild(NodeKind.META_LIST));
var mods = node.findChild(NodeKind.MOD_LIST);
if (mods && mods.children.length) {
catchup(mods.start);
var insertExport = false;
mods.children.forEach(node => {
if (node.text !== 'private') {
insertExport = true;
}
skipTo(node.end);
});
if (insertExport)
{
insert('export');
var pkg = node.findParent(NodeKind.PACKAGE);
if (pkg && !pkg.findChild(NodeKind.NAME).text)
globalExports.push(node.findChild(NodeKind.NAME).text);
}
}
return insertExport;
}
var typeMapping:{[type:string]:string} = {
'Array': 'any[]',
'Boolean': 'boolean',
'Class': 'new(..._:any[])=>any',
'int': 'number',
'Number': 'number',
'String': 'string',
'uint': 'number',
'*': 'any'
};
function emitNodeText(node: Node) {
catchup(node.start);
skipTo(node.end);
insert(' ');
insert(typeMapping.hasOwnProperty(node.text) ? typeMapping[node.text] : node.text);
insert(' ');
}
function emitVector(node: Node) {
catchup(node.start);
var type = node.findChild(NodeKind.TYPE);
if (type) {
skipTo(type.start);
emitNodeText(type);
insert('[]');
} else {
insert('any[]');
}
skipTo(node.end);
}
function emitShortVector(node: Node) {
catchup(node.start);
var vector = node.findChild(NodeKind.VECTOR);
insert('Array');
var type = vector.findChild(NodeKind.TYPE);
if(type) {
emitNodeText(type);
} else {
insert('any');
}
catchup(vector.end);
insert('(');
var arrayLiteral = node.findChild(NodeKind.ARRAY)
if (arrayLiteral.children && arrayLiteral.children.length) {
skipTo(arrayLiteral.children[0].start)
visitNodes(arrayLiteral.children)
catchup(arrayLiteral.lastChild.end);
}
insert(')');
skipTo(node.end);
}
function emitNew(node:Node) {
catchup(node.start);
state.isNew = true;
state.emitThisForNextIdent = false;
visitNodes(node.children);
state.isNew = false;
}
function emitCall(node: Node) {
catchup(node.start);
var isNew = state.isNew;
state.isNew = false;
if (node.children[0].kind === NodeKind.VECTOR) {
if (isNew) {
var vector = node.children[0];
catchup(vector.start);
insert('Array');
insert('<')
var type = vector.findChild(NodeKind.TYPE);
if (type) {
skipTo(type.start);
emitNodeText(type);
} else {
insert('any');
}
skipTo(vector.end);
insert('>');
visitNodes(node.getChildrenStartingFrom(NodeKind.VECTOR));
return;
}
var args = node.findChild(NodeKind.ARGUMENTS);
//vector conversion lets just cast to array
if (args.children.length === 1) {
insert('(<');
emitVector(node.children[0]);
insert('>');
skipTo(args.children[0].start);
visitNode(args.children[0]);
catchup(node.end);
return;
}
}
visitNodes(node.children);
}
function emitRelation(node: Node) {
catchup(node.start);
var as = node.findChild(NodeKind.AS);
if (as) {
if (node.lastChild.kind === NodeKind.IDENTIFIER) {
insert('<');
insert(node.lastChild.text);
insert('>');
visitNodes(node.getChildrenUntil(NodeKind.AS));
catchup(as.start);
skipTo(node.end);
} else {
commentNode(node, false);
}
return;
}
visitNodes(node.children)
}
function emitOp(node: Node) {
catchup(node.start);
if (node.text === "is") {
insert('instanceof');
skipTo(node.end);
return;
}
catchup(node.end);
}
function emitIdent(node: Node) {
catchup(node.start);
if (node.parent && node.parent.kind === NodeKind.DOT) {
//in case of dot just check the first
if(node.parent.children[0] !== node) {
return;
}
}
if (isKeyWord(node.text)) {
return;
}
var def = findDefInScope(node.text);
if (def && def.bound) {
insert(def.bound + '.');
}
if (!def && state.currentClassName && globVars.indexOf(node.text) === -1 && state.emitThisForNextIdent && node.text !== state.currentClassName) {
insert('this.');
}
state.emitThisForNextIdent = true;
}
function emitXMLLiteral(node: Node) {
catchup(node.start);
insert(JSON.stringify(node.text))
skipTo(node.end);
}
function enterClassScope(contentsNode: Node[]) {
var found: {[name: string]: boolean } = {};
var declarations = contentsNode.map(node => {
var nameNode: Node;
var isStatic: boolean;
switch (node.kind) {
case NodeKind.SET:
case NodeKind.GET:
case NodeKind.FUNCTION:
nameNode = node.findChild(NodeKind.NAME);
break;
case NodeKind.VAR_LIST:
case NodeKind.CONST_LIST:
nameNode = node.findChild(NodeKind.NAME_TYPE_INIT).findChild(NodeKind.NAME);
break;
}
if (!nameNode || found[nameNode.text]) {
return null;
}
found[nameNode.text] = true;
if (nameNode.text === state.currentClassName) {
return;
}
var modList = node.findChild(NodeKind.MOD_LIST);
var isStatic = modList &&
modList.children.some(mod => mod.text === 'static');
return {
name: nameNode.text,
bound: isStatic ? state.currentClassName : 'this'
};
}).filter(el => !!el);
enterScope(declarations);
}
function enterFunctionScope(node: Node) {
var decls: Declaration[] = [];
var params = node.findChild(NodeKind.PARAMETER_LIST);
if (params && params.children.length) {
decls = params.children.map(param => {
var nameTypeInit = param.findChild(NodeKind.NAME_TYPE_INIT);
if (nameTypeInit) {
return {name: nameTypeInit.findChild(NodeKind.NAME).text}
}
var rest = param.findChild(NodeKind.REST)
return { name: rest.text };
});
}
var block = node.findChild(NodeKind.BLOCK);
if (block) {
function traverse(node: Node): Declaration[] {
var result = new Array<Declaration>();
if (node.kind === NodeKind.VAR_LIST || node.kind === NodeKind.CONST_LIST ||
node.kind === NodeKind.VAR || node.kind === NodeKind.CONST) {
result = result.concat(
node
.findChildren(NodeKind.NAME_TYPE_INIT)
.map(node => ({ name: node.findChild(NodeKind.NAME).text }))
);
}
if (node.kind !== NodeKind.FUNCTION && node.children && node.children.length) {
result = Array.prototype.concat.apply(result, node.children.map(traverse));
}
return result.filter(decl => !!decl);
}
decls = decls.concat(traverse(block));
}
enterScope(decls);
}
function enterScope(decls: Declaration[]) {
state.scope = {
parent: state.scope,
declarations: decls,
get isTopLevel() {
return !state.scope;
}
};
}
function exitScope() {
state.scope = state.scope && state.scope.parent;
}
function findDefInScope(text: string) {
var scope = state.scope;
while(scope) {
for (var i = 0; i< scope.declarations.length; i++) {
if (scope.declarations[i].name === text) {
return scope.declarations[i];
}
}
scope = scope.parent;
}
return null;
}
function commentNode(node: Node, catchSemi:boolean) {
insert('/*');
var start = output.length;
catchup(node.end);
var index = state.index;
if (catchSemi) {
while(true) {
if (index >= data.source.length) {
break;
}
if (data.source[index] === '\n') {
catchup(index);
break;
}
if (data.source[index] === ';') {
catchup(index + 1);
break;
}
index++;
}
}
// disable nested block comments
output = output.substr(0, start) + output.substr(start).split('*').join(' * ');
insert('*/');
}
function catchup(index: number) {
if (state.index > index) {
return;
}
while (state.index !== index) {
output += data.source[state.index];
state.index++;
}
}
function skipTo(index: number) {
if (index < 0)
state.index = data.source.length;
else
state.index = index;
}
function skip(number: number) {
state.index += number;
}
function insert(string: string) {
output += string;
}
function consume(string: string, limit: number) {
var index = data.source.indexOf(string, state.index) + string.length;
if (index > limit || index < state.index) {
console.log("started searching at: " + data.source.substr(state.index));
console.log('output:',output);
throw new Error('invalid consume ' + JSON.stringify({string: string, index: index, limit: limit, "state": state}, null, 3));
}
state.index = index;
}