Skip to content

Instantly share code, notes, and snippets.

@mwheeler
Last active January 2, 2016 11:09
Show Gist options
  • Save mwheeler/8294225 to your computer and use it in GitHub Desktop.
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.
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