diff --git a/esprima-to-ast.js b/esprima-to-ast.js index 6f0301330ab26bbd36643841994004723d76eb54..4ea5a79ec37a5c5ee3cf2a786ef4f410eaaab020 100644 --- a/esprima-to-ast.js +++ b/esprima-to-ast.js @@ -508,26 +508,31 @@ function esprimaToAST(prog) { /* Custom Error handler with JSON pretty-printing */ -function ASTErrorJSONReplacer(key, value, depth) { - if (depth === undefined) { depth = 3; } - if (typeof value === "object") { - if (depth > 0) { - for (var nestKey in value) { - value[nestKey] = ASTErrorJSONReplacer(nestKey, value[nestKey], depth - 1); +function toString(ast, maxDepth) { + if (maxDepth === undefined) { maxDepth = 3; } + + return JSON.stringify(ast, function ASTErrorJSONReplacer(key, value, depth) { + if (depth === undefined) { depth = maxDepth; } + + if (typeof value === "object") { + if (depth > 0) { + for (var nestKey in value) { + value[nestKey] = ASTErrorJSONReplacer(nestKey, value[nestKey], depth - 1); + } + } else { + value = "RECURSION TRUNCATED"; } - } else { - value = "RECURSION TRUNCATED"; } - } - return value; + return value; + }, 2); } function NewASTErrorType(name, parentError) { var error = function (message, expr) { this.name = name; - this.message = message + "\n" + JSON.stringify(expr, ASTErrorJSONReplacer, 2); + this.message = message + "\n" + toString(expr); if (Error.captureStackTrace) { Error.captureStackTrace(this, error); } } error.prototype = Object.create(parentError.prototype); @@ -541,3 +546,4 @@ var UnsupportedSyntaxError = NewASTErrorType("UnsupportedSyntaxError", EsprimaTo module.exports.esprimaToAST = esprimaToAST; module.exports.EsprimaToASTError = EsprimaToASTError; module.exports.UnsupportedSyntaxError = UnsupportedSyntaxError; +module.exports.toString = toString; diff --git a/package.json b/package.json index 8d1d5d9898e718d8159097fac14a9b547ceb5208..3b51e6802512e1aa74888ed940dea9533a716326 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "esprima": "^1.2.5" }, "devDependencies": { + "chai": "^3.5.0", "klaw": "^1.1.3", "mocha": "^2.4.5", "mz": "^2.3.1", diff --git a/test/parser.js b/test/parser.js index 2e083e19c03c6f54b2ff77f9617204af512f68ce..8a33c8ed60d5c5b2e35c82f0cc43f953f1b4146f 100644 --- a/test/parser.js +++ b/test/parser.js @@ -1,7 +1,10 @@ +"use strict"; + var fs = require('mz/fs'); var walk = require('klaw'); var filter = require('through2-filter'); fs.readlinkSync = require('readlink').sync; // a non-broken readlink... +var assert = require('chai').assert; var esprima = require('esprima'); var esprimaToAST = require('../esprima-to-ast.js'); @@ -31,6 +34,169 @@ function isParserNegativeTest(str) { return /(?:SyntaxError|\?!NotEarlyError)/.test(negative); } +function typecheckAST(ast) { + /* typename -> ((tagname -> fieldname -> type)|type|type => (tagname -> fieldname -> type)) */ + const types = { + list: function (x) {return {"[]": {}, "::": {head: x, tail: x + " list"}}}, + option: function (x) {return {None: {}, Some: {value: x}}}, + unary_op: { + Coq_unary_op_delete: {}, Coq_unary_op_void: {}, Coq_unary_op_typeof: {}, Coq_unary_op_post_incr: {}, + Coq_unary_op_post_decr: {}, Coq_unary_op_pre_incr: {}, Coq_unary_op_pre_decr: {}, Coq_unary_op_add: {}, + Coq_unary_op_neg: {}, Coq_unary_op_bitwise_not: {}, Coq_unary_op_not: {}, + }, + binary_op: { + Coq_binary_op_mult: {}, Coq_binary_op_div: {}, Coq_binary_op_mod: {}, Coq_binary_op_add: {}, + Coq_binary_op_sub: {}, Coq_binary_op_left_shift: {}, Coq_binary_op_right_shift: {}, + Coq_binary_op_unsigned_right_shift: {}, Coq_binary_op_lt: {}, Coq_binary_op_gt: {}, Coq_binary_op_le: {}, + Coq_binary_op_ge: {}, Coq_binary_op_instanceof: {}, Coq_binary_op_in: {}, Coq_binary_op_equal: {}, + Coq_binary_op_disequal: {}, Coq_binary_op_strict_equal: {}, Coq_binary_op_strict_disequal: {}, + Coq_binary_op_bitwise_and: {}, Coq_binary_op_bitwise_or: {}, Coq_binary_op_bitwise_xor: {}, Coq_binary_op_and: {}, + Coq_binary_op_or: {}, Coq_binary_op_coma: {}, + }, + literal: { + Coq_literal_null: {}, + Coq_literal_bool : {value: "boolean"}, + Coq_literal_number : {value: "number"}, + Coq_literal_string : {value: "string"}, + }, + label: { + Coq_label_empty: {}, + Coq_label_string : {value: "string"}, + }, + label_set: "label list", + strictness_flag: "boolean", + propname: { + Coq_propname_identifier : {value: "string"}, + Coq_propname_string : {value: "string"}, + Coq_propname_number : {value: "number"}, + }, + expr: { + Coq_expr_this: {}, + Coq_expr_identifier : {name: "string"}, + Coq_expr_literal : {value: "literal"}, + Coq_expr_object : {fields: "(propname * propbody) list"}, + Coq_expr_array : {elements: "expr option list"}, + Coq_expr_function : {func_name_opt: "string option", arg_names: "string list", body: "funcbody"}, + Coq_expr_access : {obj: "expr", field: "expr"}, + Coq_expr_member : {obj: "expr", field_name: "string"}, + Coq_expr_new : {func: "expr", args: "expr list"}, + Coq_expr_call : {func: "expr", args: "expr list"}, + Coq_expr_unary_op : {op: "unary_op", arg: "expr"}, + Coq_expr_binary_op : {arg1: "expr", op: "binary_op", arg2: "expr"}, + Coq_expr_conditional : {cond: "expr", then_branch: "expr", else_branch: "expr"}, + Coq_expr_assign : {left_expr: "expr", op_opt: "binary_op option", right_expr: "expr"}, + }, + propbody: { + Coq_propbody_val : {expr: "expr"}, + Coq_propbody_get : {body: "funcbody"}, + Coq_propbody_set : {names: "string list", body: "funcbody"}, + }, + funcbody: { + Coq_funcbody_intro : {prog: "prog", source: "string"}, + }, + stat: { + Coq_stat_expr : {expr: "expr"}, + Coq_stat_label : {label: "string", stat: "stat"}, + Coq_stat_block : {stats: "stat list"}, + Coq_stat_var_decl : {decls: "(string * expr option) list"}, + Coq_stat_if : {cond: "expr", then_branch: "stat", else_branch: "stat option"}, + Coq_stat_do_while : {labels: "label_set", body: "stat", cond: "expr"}, + Coq_stat_while : {labels: "label_set", cond: "expr", body: "stat"}, + Coq_stat_with : {obj: "expr", stat: "stat"}, + Coq_stat_throw : {arg: "expr"}, + Coq_stat_return : {arg_opt: "expr option"}, + Coq_stat_break : {label: "label"}, + Coq_stat_continue : {label: "label"}, + Coq_stat_try : {body: "stat", catch_stats_opt: "(string * stat) option", finally_opt: "stat option"}, + Coq_stat_for : {labels: "label_set", init: "expr option", cond: "expr option", step: "expr option", body: "stat"}, + Coq_stat_for_var : {labels: "label_set", init: "(string * expr option) list", cond: "expr option", step: "expr option", body: "stat"}, + Coq_stat_for_in : {labels: "label_set", id: "expr", obj: "expr", body: "stat"}, + Coq_stat_for_in_var : {labels: "label_set", id: "string", init: "expr option", obj: "expr", body: "stat"}, + Coq_stat_debugger: {}, + Coq_stat_switch : {labels: "label_set", arg: "expr", body: "switchbody"}, + }, + switchbody: { + Coq_switchbody_nodefault : {clauses: "switchclause list"}, + Coq_switchbody_withdefault : {clauses_before: "switchclause list", clause_default: "stat list", clauses_after: "switchclause list"}, + }, + switchclause: { + Coq_switchclause_intro : {arg: "expr", stats: "stat list"}, + }, + prog: { + Coq_prog_intro : {strictness: "strictness_flag", elements: "element list"}, + }, + element: { + Coq_element_stat : {stat: "stat"}, + Coq_element_func_decl : {func_name: "string", arg_names: "string list", body: "funcbody"}, + }, + propdefs: "(propname * propbody) list", + elements: "element list", + /* funcdecl: { { funcdecl_name : string; + funcdecl_parameters : string list; + funcdecl_body : funcbody } */ + }; + + var isBaseType = function(type) { + return type === "string" || + type === "number" || + type === "boolean"; + }; + + // Returns a base type name, or an object containing the type's constructors + var getType = function(type) { + if (isBaseType(type)) { + return type; + } + + // Test for poly type + var i = type.lastIndexOf(' '); + if (i >= 0) { + var polyTypeName = type.substring(i+1); + var polyTypeConstr = getType(polyTypeName); + assert.isFunction(polyTypeConstr); + var instance = polyTypeConstr(type.substring(0, i)); + instance._typeName = polyTypeName; + return instance; + } + + assert(types.hasOwnProperty(type), "Type " + type + " not present in type environment"); + var t = types[type]; + if (typeof t === "string") { + // type alias, recursively lookup + return getType(t); + } else { + t._typeName = type; + return t; + } + }; + + var errorMsg = function (value, msg) { + return _ => esprimaToAST.toString(value, 1) + " " + msg; + }; + + var typecheck = function(type, value) { + var t = getType(type); + if (isBaseType(t)) { + assert(t === typeof value, errorMsg(value, "was expected to have type of "+t)); + } else { + assert(value.hasOwnProperty("type"), errorMsg(value, "doesn't have a type property")); + assert.strictEqual(t._typeName, value.type); + assert(value.hasOwnProperty("tag"), errorMsg(value, "doesn't have a tag property")); + assert.notStrictEqual(value.tag, "_typeName"); + assert(t.hasOwnProperty(value.tag), value.tag + " is a not a valid constructor of " + t._typeName); + + // Test each field defined in the type constructor + var constructor = t[value.tag]; + Object.keys(constructor).forEach(field => { + assert(value.hasOwnProperty(field), errorMsg(value, "doesn't have a " + field + " property")); + typecheck(constructor[field], value[field]); + }); + } + }; + + return typecheck("prog", ast); +} + walk(test262path) .pipe(filter.obj(file => file.stats.isFile() && file.path.endsWith(".js"))) .on('readable', function() { @@ -65,13 +231,13 @@ walk(test262path) }); it('converts', function() { - var prog; try { - prog = esprima.parse(source, {loc: true}); + var prog = esprima.parse(source, {loc: true}); } catch(e) { return; } try { - esprimaToAST.esprimaToAST(prog); + var ast = esprimaToAST.esprimaToAST(prog); + typecheckAST(ast); } catch (e) { if (e instanceof esprimaToAST.UnsupportedSyntaxError) { } else {