Last active
January 2, 2016 11:09
-
-
Save mwheeler/8294225 to your computer and use it in GitHub Desktop.
A quick example for walking the raw AST in typescript 0.9.5.
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
var fs = require('fs'); | |
var path = require('path'); | |
// Load the typescript module | |
var TypeScript = (function(){ | |
var sandbox = { | |
__filename: __filename, | |
__dirname: __dirname, | |
global: global, | |
process: process, | |
require: require, | |
console: console, | |
exports: null, | |
setInterval: setInterval, | |
setTimeout: setTimeout | |
}; | |
var vm = require('vm'); | |
var typescript_filename = require.resolve('typescript'); | |
var source = fs.readFileSync(typescript_filename, 'utf8'); | |
var script = vm.createScript(source.concat('\n\nexports = TypeScript;'), typescript_filename); | |
script.runInNewContext(sandbox); | |
return sandbox.exports; | |
})(); | |
// Setup compiler settings | |
var compiler_settings = TypeScript.ImmutableCompilationSettings.fromCompilationSettings({ | |
noImplicitAny: true, | |
useCaseSensitiveFileResolution: true, | |
moduleGenTarget: TypeScript.ModuleGenTarget.Synchronous, | |
codeGenTarget: TypeScript.LanguageVersion.EcmaScript5 | |
}); | |
// Create a quick and dirty compiler 'host' to resolve native paths | |
var host = { | |
getScriptSnapshot: function(filename) | |
{ | |
var contents = fs.readFileSync(filename).toString(); | |
return TypeScript.ScriptSnapshot.fromString(contents); | |
}, | |
resolveRelativePath: function(pathname, dirname) | |
{ | |
return host.resolvePath(pathname, dirname); | |
//return path.relative(dirname, pathname); | |
}, | |
writeFile: function(fileName, contents, writeByteOrderMark) | |
{ | |
fs.writeFileSync(fileName, contents); | |
}, | |
getParentDirectory: function(filepath) | |
{ | |
return path.dirname(filepath); | |
}, | |
fileExists: function(path) | |
{ | |
return fs.existsSync(path); | |
}, | |
directoryExists: function(path) | |
{ | |
return fs.existsSync(path); | |
}, | |
resolvePath: function(filepath, search_paths) | |
{ | |
var search = []; | |
if(filepath[0] !== '/' && filepath[0] !== '\\') | |
{ | |
search = search.concat(search_paths).map(function(x) | |
{ | |
if(x[0] !== '/' && x[0] !== '\\') | |
{ | |
return path.join(process.cwd(), x); | |
} | |
return x; | |
}); | |
// TODO: 'built in' / task-driven search paths | |
} | |
var exts = ['', '.ts', '.js']; | |
for(var ext in exts) | |
{ | |
var args = search.concat(filepath + exts[ext]); | |
var result = path.resolve.apply(path, args); | |
if(result && fs.existsSync(result)) return result; | |
} | |
return filepath; | |
} | |
}; | |
function parse_module(module_path) | |
{ | |
// TODO: Figure out if it's appropriate to re-use the existing compiler | |
// - note: the existing compiler has already evaluated these modules when type checking the imports | |
var module_compiler = new TypeScript.TypeScriptCompiler(new TypeScript.NullLogger(), compiler_settings); | |
var resolved_module = TypeScript.ReferenceResolver.resolve([module_path], host, compiler_settings).resolvedFiles; | |
if(!resolved_module || resolved_module.length <= 0) | |
{ | |
console.log('Failed to resolve external module: ' + ast.stringLiteral.text()); | |
return undefined; | |
} | |
resolved_module.forEach(function(resolvedFile) | |
{ | |
var input = resolvedFile.path; | |
if(input[0] != '/') input = path.relative(process.cwd(), input); | |
var sourceFile = { | |
scriptSnapshot: host.getScriptSnapshot(resolvedFile.path), | |
byteOderMark: 1/*ByteOrderMark.Utf8*/ | |
}; | |
// 1.0 = compiler.addSourceUnit(resolvedFile.path, sourceFile.scriptSnapshot, sourceFile.byteOrderMark, 0, false, resolvedFile.referencedFiles); | |
module_compiler.addFile(resolvedFile.path, sourceFile.scriptSnapshot, sourceFile.byteOrderMark, 0, false, resolvedFile.referencedFiles); | |
}); | |
var decl = module_compiler.topLevelDeclaration(path); | |
return gen_schema(decl.ast()); | |
} | |
function resolve(id, namespace) | |
{ | |
if(id in namespace) return namespace[id]; | |
if(namespace._parent) return resolve(id, namespace._parent); | |
return undefined; | |
} | |
function gen_schema(ast, namespace) | |
{ | |
if(!ast) return undefined; | |
var result = {}; | |
// TODO: Look into using an AST walker, could be running sntaxTree().sourceUnit().accept(new MyWalker())??? | |
var is_exported = false; | |
var is_public = false; | |
var is_static = false; | |
var is_optional = false; | |
if(ast.modifiers) | |
{ | |
if(ast.modifiers.indexOf(TypeScript.PullElementFlags.Exported) !== -1) | |
{ | |
is_exported = true; | |
} | |
if(ast.modifiers.indexOf(TypeScript.PullElementFlags.Public) !== -1) | |
{ | |
is_public = true; | |
} | |
if(ast.modifiers.indexOf(TypeScript.PullElementFlags.Static) !== -1) | |
{ | |
is_static = true; | |
} | |
if(ast.modifiers.indexOf(TypeScript.PullElementFlags.Optional) !== -1) | |
{ | |
is_optional = true; | |
} | |
if(!is_public && !is_exported) return undefined; | |
} | |
// TODO: Generate proper data from this, currently just testing | |
switch (ast.kind()/*nodeType() in 1.0?*/) | |
{ | |
case TypeScript.SyntaxKind.Identifier: | |
return ast.text(); | |
case TypeScript.SyntaxKind.List: | |
for(var i = 0; i < ast.childCount(); ++i) | |
{ | |
var child = gen_schema(ast.childAt(i), namespace); | |
if(!child) continue; | |
result[child.name] = child; | |
} | |
break; | |
case TypeScript.SyntaxKind.SeparatedList: | |
var last_untyped = null; | |
for(var i = 0; i < ast.nonSeparatorCount(); ++i) | |
{ | |
var child = gen_schema(ast.nonSeparatorAt(i), namespace); | |
if(last_untyped) | |
{ | |
// Typed identifier | |
if(typeof child !== 'string') | |
{ | |
result[last_untyped] = child; | |
last_untyped = null; | |
} | |
else | |
{ | |
result[last_untyped] = { value: 'type', type: 'object', members: {} }; | |
last_untyped = child; | |
} | |
} | |
else last_untyped = child; | |
} | |
break; | |
case TypeScript.SyntaxKind.Script: | |
ast = ast.topLevelMod; | |
case TypeScript.SyntaxKind.ModuleDeclaration: | |
var module_name = path.basename(ast.name.text()); | |
result.name = module_name; | |
case TypeScript.SyntaxKind.SourceUnit: | |
result.type = 'module'; | |
result.value = gen_schema(ast.moduleElements, { _parent: namespace, _exports: {} }); | |
if(namespace) namespace[result.name] = result; | |
break; | |
case TypeScript.SyntaxKind.VariableStatement: | |
result = gen_schema(ast.declaration, namespace); | |
break; | |
case TypeScript.SyntaxKind.VariableDeclaration: | |
result = gen_schema(ast.declarators, namespace); | |
break; | |
case TypeScript.SyntaxKind.VariableDeclarator: | |
result.name = ast.propertyName.text(); | |
result.type = 'property'; | |
// Take type annotation if one exists | |
if(ast.typeAnnotation) | |
{ | |
result.value = gen_schema(ast.typeAnnotation, namespace); | |
} | |
// Else, infer the type from any assignment clause | |
else if(ast.equalsValueClause) | |
{ | |
var default_value = gen_schema(ast.equalsValueClause.value, namespace); | |
if(default_value != null) | |
{ | |
result.value = default_value.type? default_value : { type: default_value }; | |
} | |
else | |
{ | |
result.value = { type: 'object', value: {} }; | |
} | |
} | |
// Finally, assign unit type | |
else | |
{ | |
result.value = { type: 'object', value: {} }; | |
} | |
namespace[result.name] = result; | |
break; | |
// Function invocation | |
case TypeScript.SyntaxKind.InvocationExpression: | |
console.log("TypeScript.SyntaxKind.InvocationExpression"); | |
var args = gen_schema(ast.argumentList.arguments, namespace); | |
// TODO: qualified invocation should resolve the namespace of the qualified type, instead of using namespace | |
return resolve(ast.expression.text(), args, namespace); | |
case TypeScript.SyntaxKind.MemberAccessExpression: | |
console.log("TypeScript.SyntaxKind.MemberAccessExpression"); | |
console.log(ast.name.text()); | |
console.log(gen_schema(ast.expression, namespace)); | |
break; | |
case TypeScript.SyntaxKind.StringLiteral: | |
return ast.text().replace(/^'|^"|'$|"$/gm, ''); | |
case TypeScript.SyntaxKind.IdentifierName: | |
// TODO: should we resolve this id to a type? | |
return ast.text(); | |
case TypeScript.SyntaxKind.TypeAnnotation: | |
return gen_schema(ast.type, namespace); | |
case TypeScript.SyntaxKind.FunctionType: | |
// ast.modifiers | |
result.name = ast.identifier.text(); | |
result.value = 'function'; | |
result.type = gen_schema(ast.callSignature, namespace); | |
break; | |
case TypeScript.SyntaxKind.CallSignature: | |
result.type_params = gen_schema(ast.typeParameterList, namespace); | |
result.params = gen_schema(ast.parameterList, namespace); | |
result.returns = gen_schema(ast.typeAnnotation, namespace); | |
break; | |
case TypeScript.SyntaxKind.ParameterList: | |
return gen_schema(ast.parameters, namespace); | |
break; | |
case TypeScript.SyntaxKind.TypeParameterList: | |
return gen_schema(ast.typeParameters, namespace); | |
break; | |
case TypeScript.SyntaxKind.ObjectType: | |
result.value = 'type'; | |
result.type = 'object'; | |
result.members = gen_schema(ast.typeMembers, namespace); | |
break; | |
case TypeScript.SyntaxKind.ArrayType: | |
result.value = 'type'; | |
result.type = 'array'; | |
result.element_type = gen_schema(ast.type, namespace); | |
break; | |
case TypeScript.SyntaxKind.GenericType: | |
//result.value = 'type'; | |
//result.type = 'object'; // todo: lookup for ast.name.text() | |
// todo: replace parametric types with matches from ast.typeArgumentList | |
break; | |
case TypeScript.SyntaxKind.TypeQuery: | |
//result.value = 'type'; | |
//result.type = 'object'; todo: lookup for ast.name.text() | |
break; | |
case TypeScript.SyntaxKind.BuiltInType: | |
result.value = 'type'; | |
result.type = ast.type.text(); | |
break; | |
case TypeScript.SyntaxKind.AnyKeyword: | |
result.value = 'type'; | |
result.type = 'any'; | |
break; | |
case TypeScript.SyntaxKind.StringKeyword: | |
result.value = 'type'; | |
result.type = 'string'; | |
break; | |
case TypeScript.SyntaxKind.VoidKeyword: | |
result.value = 'type'; | |
result.type = 'void'; | |
break; | |
case TypeScript.SyntaxKind.NumberKeyword: | |
result.value = 'type'; | |
result.type = 'number'; | |
break; | |
case TypeScript.SyntaxKind.FunctionDeclaration: | |
// todo: modifiers | |
result.name = ast.identifier.text(); | |
result.type = 'method'; | |
result.value = gen_schema(ast.callSignature, namespace); | |
result['static'] = is_static; | |
break; | |
case TypeScript.SyntaxKind.MemberFunctionDeclaration: | |
result.name = ast.propertyName.text(); | |
result.value = 'method'; | |
result.type = gen_schema(ast.callSignature, namespace); | |
result['static'] = is_static; | |
break; | |
case TypeScript.SyntaxKind.MemberVariableDeclaration: | |
result = gen_schema(ast.variableDeclarator, namespace); | |
if(result) | |
{ | |
result.optional = is_optional; | |
result['static'] = is_static; | |
} | |
break; | |
// getter | |
case TypeScript.SyntaxKind.GetAccessor: | |
// ast.modifiers | |
// ast.propertyName | |
// ast.parameterList | |
// ast.typeAnnotation | |
break; | |
// setter | |
case TypeScript.SyntaxKind.SetAccessor: | |
// ast.modifiers | |
// ast.propertyName | |
// ast.parameterList | |
break; | |
case TypeScript.SyntaxKind.ClassDeclaration: | |
console.log('== [CLASS] ' + ast.identifier.text() + ' =='); | |
// ast.modifiers | |
result.name = ast.identifier.text(); // <ast.typeParameterList> | |
result.value = 'class'; | |
result.members = gen_schema(ast.classElements, namespace); | |
namespace[result.name] = result; | |
break; | |
case TypeScript.SyntaxKind.Parameter: | |
result.name = ast.identifier .text; | |
result.value = 'parameter'; | |
result.type = gen_schema(ast.typeAnnotation, namespace); | |
result.optional = is_optional; | |
result["default"] = ast.equalsValueClause? gen_schema(ast.equalsValueClause.value, namespace) : undefined; | |
break; | |
// Override all exports w/ identified value | |
case TypeScript.SyntaxKind.ExportAssignment: | |
result.name = ast.id.text; | |
result.value = namespace[result.name]; | |
namespace._exports[result.name] = result.value; | |
console.log('== [EXPORT]: ' + result.name + ' =='); | |
// TODO: Determine type (property/method/module/event), and add to result | |
// export_class/property/method/event/module functions which handle each type appropriately) | |
break; | |
// ??? | |
case TypeScript.SyntaxKind.ExternalModuleReference: | |
var path = ast.stringLiteral.text().replace(/^'|^"|'$|"$/gm, ''); | |
path = host.resolvePath(id, path.dirname(ast.fileName())) | |
result = parse_module(path); | |
break; | |
// import LocalName = require("ModuleName") | |
case TypeScript.SyntaxKind.ImportDeclaration: | |
if(!is_exported) return undefined; | |
result.type = 'module'; | |
result.name = ast.identifier.text(); | |
result.value = gen_schema(ast.moduleReference); | |
break; | |
default: | |
console.log(ast.kind()); | |
return undefined; | |
} | |
return result; | |
} | |
var input = 'fill_me_in.ts'; | |
// Create a compiler | |
var compiler = new TypeScript.TypeScriptCompiler(new TypeScript.NullLogger(), compiler_settings); | |
// Resolve input file requires | |
var resolved_files = TypeScript.ReferenceResolver.resolve([input], host, compiler_settings).resolvedFiles; | |
// Note: only used the 'last' resolved file (which is the input), everything before that are resolved require()s | |
[resolved_files[resolved_files.length-1]].forEach(function(resolvedFile) | |
{ | |
// Add file to compiler | |
var sourceFile = { | |
scriptSnapshot: host.getScriptSnapshot(resolvedFile.path), | |
byteOderMark: 1/*ByteOrderMark.Utf8*/ | |
}; | |
// 1.0 = compiler.addSourceUnit(filename, sourceFile.scriptSnapshot, sourceFile.byteOrderMark, 0, false, resolvedFile.referencedFiles); | |
compiler.addFile(resolvedFile.path, sourceFile.scriptSnapshot, sourceFile.byteOrderMark, 0, false, resolvedFile.referencedFiles); | |
// Get file's top level declaration | |
var decl = compiler.topLevelDeclaration(resolvedFile.path); // file's top level PullDecl object | |
// Raw AST: | |
var file_ast = decl.ast(); | |
console.log(gen_schema(file_ast)); | |
// Typed AST: | |
//var tr = new TypeScript.PullTypeResolver(compiler_settings, decl.semanticInfoChain()); | |
//var context = new TypeScript.PullTypeResolutionContext(tr, true, resolvedFile.path); | |
//var module_symbol = tr.resolveAST(decl.ast(), false, context); // PullSymbol | |
//console.log(different_parser(module_symbol)); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment