From f74b7f74019e796c95c228dc6b0348f9db6e709f Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 9 May 2020 02:58:03 +0100 Subject: [PATCH] implement AST validation (#3863) --- bin/uglifyjs | 2 + lib/ast.js | 302 +++++++++++++++++++++++++++++++++++++++------ lib/minify.js | 4 + lib/mozilla-ast.js | 14 ++- lib/utils.js | 3 + test/benchmark.js | 4 +- test/compress.js | 1 + test/jetstream.js | 4 +- 8 files changed, 290 insertions(+), 44 deletions(-) diff --git a/bin/uglifyjs b/bin/uglifyjs index e5124b06..500fe6d8 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -68,6 +68,7 @@ program.option("--self", "Build UglifyJS as a library (implies --wrap UglifyJS)" program.option("--source-map [options]", "Enable source map/specify source map options.", parse_js()); program.option("--timings", "Display operations run time on STDERR."); program.option("--toplevel", "Compress and/or mangle variables in toplevel scope."); +program.option("--validate", "Perform validation during AST manipulations."); program.option("--verbose", "Print diagnostic messages."); program.option("--warn", "Print warning messages."); program.option("--wrap ", "Embed everything as a function with “exports” corresponding to “name” globally."); @@ -91,6 +92,7 @@ if (!program.output && program.sourceMap && program.sourceMap.url != "inline") { "mangle", "sourceMap", "toplevel", + "validate", "wrap" ].forEach(function(name) { if (name in program) { diff --git a/lib/ast.js b/lib/ast.js index e8b7c56e..2cbb69d6 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -112,7 +112,14 @@ var AST_Node = DEFNODE("Node", "start end", { }, walk: function(visitor) { visitor.visit(this); - } + }, + _validate: noop, + validate: function() { + var ctor = this.CTOR; + do { + ctor.prototype._validate.call(this); + } while (ctor = ctor.BASE); + }, }, null); (AST_Node.log_function = function(fn, verbose) { @@ -135,6 +142,32 @@ var AST_Node = DEFNODE("Node", "start end", { } })(); +var restore_transforms = []; +AST_Node.enable_validation = function() { + AST_Node.disable_validation(); + (function validate_transform(ctor) { + var transform = ctor.prototype.transform; + ctor.prototype.transform = function(tw, in_list) { + var node = transform.call(this, tw, in_list); + if (node instanceof AST_Node) { + node.validate(); + } else if (!(node === null || in_list && List.is_op(node))) { + throw new Error("invalid transformed value: " + node); + } + return node; + }; + restore_transforms.push(function() { + ctor.prototype.transform = transform; + }); + ctor.SUBCLASSES.forEach(validate_transform); + })(this); +}; + +AST_Node.disable_validation = function() { + var restore; + while (restore = restore_transforms.pop()) restore(); +}; + /* -----[ statements ]----- */ var AST_Statement = DEFNODE("Statement", null, { @@ -151,8 +184,18 @@ var AST_Directive = DEFNODE("Directive", "value quote", { value: "[string] The value of this directive as a plain string (it's not an AST_String!)", quote: "[string] the original quote character" }, + _validate: function() { + if (typeof this.value != "string") throw new Error("value must be string"); + }, }, AST_Statement); +function must_be_expression(node, prop) { + if (!(node[prop] instanceof AST_Node)) throw new Error(prop + " must be AST_Node"); + if (node[prop] instanceof AST_Statement && !(node[prop] instanceof AST_Function)) { + throw new Error(prop + " cannot be AST_Statement"); + } +} + var AST_SimpleStatement = DEFNODE("SimpleStatement", "body", { $documentation: "A statement consisting of an expression, i.e. a = 1 + 2", $propdoc: { @@ -163,7 +206,10 @@ var AST_SimpleStatement = DEFNODE("SimpleStatement", "body", { visitor.visit(node, function() { node.body.walk(visitor); }); - } + }, + _validate: function() { + must_be_expression(this, "body"); + }, }, AST_Statement); function walk_body(node, visitor) { @@ -182,7 +228,13 @@ var AST_Block = DEFNODE("Block", "body", { visitor.visit(node, function() { walk_body(node, visitor); }); - } + }, + _validate: function() { + this.body.forEach(function(node) { + if (!(node instanceof AST_Statement)) throw new Error("body must be AST_Statement[]"); + if (node instanceof AST_Function) throw new Error("body cannot contain AST_Function"); + }); + }, }, AST_Statement); var AST_BlockStatement = DEFNODE("BlockStatement", null, { @@ -197,7 +249,11 @@ var AST_StatementWithBody = DEFNODE("StatementWithBody", "body", { $documentation: "Base class for all statements that contain one nested body: `For`, `ForIn`, `Do`, `While`, `With`", $propdoc: { body: "[AST_Statement] the body; this should always be present, even if it's an AST_EmptyStatement" - } + }, + _validate: function() { + if (!(this.body instanceof AST_Statement)) throw new Error("body must be AST_Statement"); + if (this.body instanceof AST_Function) throw new Error("body cannot be AST_Function"); + }, }, AST_Statement); var AST_LabeledStatement = DEFNODE("LabeledStatement", "label", { @@ -225,7 +281,10 @@ var AST_LabeledStatement = DEFNODE("LabeledStatement", "label", { })); } return node; - } + }, + _validate: function() { + if (!(this.label instanceof AST_Label)) throw new Error("label must be AST_Label"); + }, }, AST_StatementWithBody); var AST_IterationStatement = DEFNODE("IterationStatement", null, { @@ -236,7 +295,10 @@ var AST_DWLoop = DEFNODE("DWLoop", "condition", { $documentation: "Base class for do/while statements", $propdoc: { condition: "[AST_Node] the loop condition. Should not be instanceof AST_Statement" - } + }, + _validate: function() { + must_be_expression(this, "condition"); + }, }, AST_IterationStatement); var AST_Do = DEFNODE("Do", null, { @@ -276,7 +338,18 @@ var AST_For = DEFNODE("For", "init condition step", { if (node.step) node.step.walk(visitor); node.body.walk(visitor); }); - } + }, + _validate: function() { + if (this.init != null) { + if (!(this.init instanceof AST_Node)) throw new Error("init must be AST_Node"); + if (this.init instanceof AST_Statement + && !(this.init instanceof AST_Definitions || this.init instanceof AST_Function)) { + throw new Error("init cannot be AST_Statement"); + } + } + if (this.condition != null) must_be_expression(this, "condition"); + if (this.step != null) must_be_expression(this, "step"); + }, }, AST_IterationStatement); var AST_ForIn = DEFNODE("ForIn", "init object", { @@ -292,7 +365,15 @@ var AST_ForIn = DEFNODE("ForIn", "init object", { node.object.walk(visitor); node.body.walk(visitor); }); - } + }, + _validate: function() { + if (this.init instanceof AST_Definitions) { + if (this.init.definitions.length != 1) throw new Error("init must have single declaration"); + } else if (!(this.init instanceof AST_PropAccess || this.init instanceof AST_SymbolRef)) { + throw new Error("init must be assignable"); + } + must_be_expression(this, "object"); + }, }, AST_IterationStatement); var AST_With = DEFNODE("With", "expression", { @@ -306,7 +387,10 @@ var AST_With = DEFNODE("With", "expression", { node.expression.walk(visitor); node.body.walk(visitor); }); - } + }, + _validate: function() { + must_be_expression(this, "expression"); + }, }, AST_StatementWithBody); /* -----[ scope and functions ]----- */ @@ -331,7 +415,12 @@ var AST_Scope = DEFNODE("Scope", "variables functions uses_with uses_eval parent }, pinned: function() { return this.uses_eval || this.uses_with; - } + }, + _validate: function() { + if (this.parent_scope != null) { + if (!(this.parent_scope instanceof AST_Scope)) throw new Error("parent_scope must be AST_Scope"); + } + }, }, AST_Block); var AST_Toplevel = DEFNODE("Toplevel", "globals", { @@ -394,19 +483,35 @@ var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments length_read", { }); walk_body(node, visitor); }); - } + }, + _validate: function() { + this.argnames.forEach(function(node) { + if (!(node instanceof AST_SymbolFunarg)) throw new Error("argnames must be AST_SymbolFunarg[]"); + }); + }, }, AST_Scope); var AST_Accessor = DEFNODE("Accessor", null, { - $documentation: "A setter/getter function. The `name` property is always null." + $documentation: "A setter/getter function. The `name` property is always null.", + _validate: function() { + if (this.name != null) throw new Error("name must be null"); + }, }, AST_Lambda); var AST_Function = DEFNODE("Function", "inlined", { - $documentation: "A function expression" + $documentation: "A function expression", + _validate: function() { + if (this.name != null) { + if (!(this.name instanceof AST_SymbolLambda)) throw new Error("name must be AST_SymbolLambda"); + } + }, }, AST_Lambda); var AST_Defun = DEFNODE("Defun", "inlined", { - $documentation: "A function definition" + $documentation: "A function definition", + _validate: function() { + if (!(this.name instanceof AST_SymbolDefun)) throw new Error("name must be AST_SymbolDefun"); + }, }, AST_Lambda); /* -----[ JUMPS ]----- */ @@ -429,11 +534,17 @@ var AST_Exit = DEFNODE("Exit", "value", { }, AST_Jump); var AST_Return = DEFNODE("Return", null, { - $documentation: "A `return` statement" + $documentation: "A `return` statement", + _validate: function() { + if (this.value != null) must_be_expression(this, "value"); + }, }, AST_Exit); var AST_Throw = DEFNODE("Throw", null, { - $documentation: "A `throw` statement" + $documentation: "A `throw` statement", + _validate: function() { + must_be_expression(this, "value"); + }, }, AST_Exit); var AST_LoopControl = DEFNODE("LoopControl", "label", { @@ -446,7 +557,12 @@ var AST_LoopControl = DEFNODE("LoopControl", "label", { visitor.visit(node, function() { if (node.label) node.label.walk(visitor); }); - } + }, + _validate: function() { + if (this.label != null) { + if (!(this.label instanceof AST_LabelRef)) throw new Error("label must be AST_LabelRef"); + } + }, }, AST_Jump); var AST_Break = DEFNODE("Break", null, { @@ -472,7 +588,14 @@ var AST_If = DEFNODE("If", "condition alternative", { node.body.walk(visitor); if (node.alternative) node.alternative.walk(visitor); }); - } + }, + _validate: function() { + must_be_expression(this, "condition"); + if (this.alternative != null) { + if (!(this.alternative instanceof AST_Statement)) throw new Error("alternative must be AST_Statement"); + if (this.alternative instanceof AST_Function) throw new error("alternative cannot be AST_Function"); + } + }, }, AST_StatementWithBody); /* -----[ SWITCH ]----- */ @@ -488,7 +611,10 @@ var AST_Switch = DEFNODE("Switch", "expression", { node.expression.walk(visitor); walk_body(node, visitor); }); - } + }, + _validate: function() { + must_be_expression(this, "expression"); + }, }, AST_Block); var AST_SwitchBranch = DEFNODE("SwitchBranch", null, { @@ -510,7 +636,10 @@ var AST_Case = DEFNODE("Case", "expression", { node.expression.walk(visitor); walk_body(node, visitor); }); - } + }, + _validate: function() { + must_be_expression(this, "expression"); + }, }, AST_SwitchBranch); /* -----[ EXCEPTIONS ]----- */ @@ -528,7 +657,15 @@ var AST_Try = DEFNODE("Try", "bcatch bfinally", { if (node.bcatch) node.bcatch.walk(visitor); if (node.bfinally) node.bfinally.walk(visitor); }); - } + }, + _validate: function() { + if (this.bcatch != null) { + if (!(this.bcatch instanceof AST_Catch)) throw new Error("bcatch must be AST_Catch"); + } + if (this.bfinally != null) { + if (!(this.bfinally instanceof AST_Finally)) throw new Error("bfinally must be AST_Finally"); + } + }, }, AST_Block); var AST_Catch = DEFNODE("Catch", "argname", { @@ -542,7 +679,10 @@ var AST_Catch = DEFNODE("Catch", "argname", { node.argname.walk(visitor); walk_body(node, visitor); }); - } + }, + _validate: function() { + if (!(this.argname instanceof AST_SymbolCatch)) throw new Error("argname must be AST_SymbolCatch"); + }, }, AST_Block); var AST_Finally = DEFNODE("Finally", null, { @@ -567,7 +707,12 @@ var AST_Definitions = DEFNODE("Definitions", "definitions", { }, AST_Statement); var AST_Var = DEFNODE("Var", null, { - $documentation: "A `var` statement" + $documentation: "A `var` statement", + _validate: function() { + this.definitions.forEach(function(node) { + if (!(node instanceof AST_VarDef)) throw new Error("definitions must be AST_VarDef[]"); + }); + }, }, AST_Definitions); var AST_VarDef = DEFNODE("VarDef", "name value", { @@ -582,11 +727,24 @@ var AST_VarDef = DEFNODE("VarDef", "name value", { node.name.walk(visitor); if (node.value) node.value.walk(visitor); }); - } + }, + _validate: function() { + if (!(this.name instanceof AST_SymbolVar)) throw new Error("name must be AST_SymbolVar"); + if (this.value != null) must_be_expression(this, "value"); + }, }); /* -----[ OTHER ]----- */ +function must_be_expressions(node, prop) { + node[prop].forEach(function(node) { + if (!(node instanceof AST_Node)) throw new Error(prop + " must be AST_Node[]"); + if (node instanceof AST_Statement && !(node instanceof AST_Function)) { + throw new Error(prop + " cannot contain AST_Statement"); + } + }); +} + var AST_Call = DEFNODE("Call", "expression args", { $documentation: "A function call expression", $propdoc: { @@ -601,7 +759,11 @@ var AST_Call = DEFNODE("Call", "expression args", { arg.walk(visitor); }); }); - } + }, + _validate: function() { + must_be_expression(this, "expression"); + must_be_expressions(this, "args"); + }, }); var AST_New = DEFNODE("New", null, { @@ -620,7 +782,10 @@ var AST_Sequence = DEFNODE("Sequence", "expressions", { expr.walk(visitor); }); }); - } + }, + _validate: function() { + must_be_expressions(this, "expressions"); + }, }); var AST_PropAccess = DEFNODE("PropAccess", "expression property", { @@ -640,7 +805,10 @@ var AST_PropAccess = DEFNODE("PropAccess", "expression property", { return; } return p; - } + }, + _validate: function() { + must_be_expression(this, "expression"); + }, }); var AST_Dot = DEFNODE("Dot", null, { @@ -650,7 +818,10 @@ var AST_Dot = DEFNODE("Dot", null, { visitor.visit(node, function() { node.expression.walk(visitor); }); - } + }, + _validate: function() { + if (typeof this.property != "string") throw new Error("property must be string"); + }, }, AST_PropAccess); var AST_Sub = DEFNODE("Sub", null, { @@ -661,7 +832,10 @@ var AST_Sub = DEFNODE("Sub", null, { node.expression.walk(visitor); node.property.walk(visitor); }); - } + }, + _validate: function() { + must_be_expression(this, "property"); + }, }, AST_PropAccess); var AST_Unary = DEFNODE("Unary", "operator expression", { @@ -675,7 +849,11 @@ var AST_Unary = DEFNODE("Unary", "operator expression", { visitor.visit(node, function() { node.expression.walk(visitor); }); - } + }, + _validate: function() { + if (typeof this.operator != "string") throw new Error("operator must be string"); + must_be_expression(this, "expression"); + }, }); var AST_UnaryPrefix = DEFNODE("UnaryPrefix", null, { @@ -699,7 +877,12 @@ var AST_Binary = DEFNODE("Binary", "operator left right", { node.left.walk(visitor); node.right.walk(visitor); }); - } + }, + _validate: function() { + must_be_expression(this, "left"); + if (typeof this.operator != "string") throw new Error("operator must be string"); + must_be_expression(this, "right"); + }, }); var AST_Conditional = DEFNODE("Conditional", "condition consequent alternative", { @@ -716,11 +899,19 @@ var AST_Conditional = DEFNODE("Conditional", "condition consequent alternative", node.consequent.walk(visitor); node.alternative.walk(visitor); }); - } + }, + _validate: function() { + must_be_expression(this, "condition"); + must_be_expression(this, "consequent"); + must_be_expression(this, "alternative"); + }, }); var AST_Assign = DEFNODE("Assign", null, { $documentation: "An assignment expression — `a = b + 5`", + _validate: function() { + if (this.operator.indexOf("=") < 0) throw new Error('operator must contain "="'); + }, }, AST_Binary); /* -----[ LITERALS ]----- */ @@ -737,7 +928,10 @@ var AST_Array = DEFNODE("Array", "elements", { element.walk(visitor); }); }); - } + }, + _validate: function() { + must_be_expressions(this, "elements"); + }, }); var AST_Object = DEFNODE("Object", "properties", { @@ -752,7 +946,12 @@ var AST_Object = DEFNODE("Object", "properties", { prop.walk(visitor); }); }); - } + }, + _validate: function() { + this.properties.forEach(function(node) { + if (!(node instanceof AST_ObjectProperty)) throw new Error("properties must be AST_ObjectProperty[]"); + }); + }, }); var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { @@ -773,15 +972,27 @@ var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote", { $documentation: "A key: value object property", $propdoc: { quote: "[string] the original quote character" - } + }, + _validate: function() { + if (typeof this.key != "string") throw new Error("key must be string"); + must_be_expression(this, "value"); + }, }, AST_ObjectProperty); var AST_ObjectSetter = DEFNODE("ObjectSetter", null, { $documentation: "An object setter property", + _validate: function() { + if (!(this.key instanceof AST_SymbolAccessor)) throw new Error("key must be AST_SymbolAccessor"); + if (!(this.value instanceof AST_Accessor)) throw new Error("value must be AST_Accessor"); + }, }, AST_ObjectProperty); var AST_ObjectGetter = DEFNODE("ObjectGetter", null, { $documentation: "An object getter property", + _validate: function() { + if (!(this.key instanceof AST_SymbolAccessor)) throw new Error("key must be AST_SymbolAccessor"); + if (!(this.value instanceof AST_Accessor)) throw new Error("value must be AST_Accessor"); + }, }, AST_ObjectProperty); var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { @@ -791,6 +1002,9 @@ var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { thedef: "[SymbolDef/S] the definition of this symbol" }, $documentation: "Base class for all symbols", + _validate: function() { + if (typeof this.name != "string") throw new Error("name must be string"); + }, }); var AST_SymbolAccessor = DEFNODE("SymbolAccessor", null, { @@ -842,6 +1056,9 @@ var AST_LabelRef = DEFNODE("LabelRef", null, { var AST_This = DEFNODE("This", null, { $documentation: "The `this` symbol", + _validate: function() { + if (this.name !== "this") throw new Error('name must be "this"'); + }, }, AST_Symbol); var AST_Constant = DEFNODE("Constant", null, { @@ -853,21 +1070,30 @@ var AST_String = DEFNODE("String", "value quote", { $propdoc: { value: "[string] the contents of this string", quote: "[string] the original quote character" - } + }, + _validate: function() { + if (typeof this.value != "string") throw new Error("value must be string"); + }, }, AST_Constant); var AST_Number = DEFNODE("Number", "value", { $documentation: "A number literal", $propdoc: { value: "[number] the numeric value", - } + }, + _validate: function() { + if (typeof this.value != "number") throw new Error("value must be number"); + }, }, AST_Constant); var AST_RegExp = DEFNODE("RegExp", "value", { $documentation: "A regexp literal", $propdoc: { value: "[RegExp] the actual regexp" - } + }, + _validate: function() { + if (!(this.value instanceof RegExp)) throw new Error("value must be RegExp"); + }, }, AST_Constant); var AST_Atom = DEFNODE("Atom", null, { diff --git a/lib/minify.js b/lib/minify.js index 2182d653..13d36de0 100644 --- a/lib/minify.js +++ b/lib/minify.js @@ -85,9 +85,11 @@ function minify(files, options) { sourceMap: false, timings: false, toplevel: false, + validate: false, warnings: false, wrap: false, }, true); + if (options.validate) AST_Node.enable_validation(); var timings = options.timings && { start: Date.now() }; @@ -253,5 +255,7 @@ function minify(files, options) { return result; } catch (ex) { return { error: ex }; + } finally { + AST_Node.disable_validation(); } } diff --git a/lib/mozilla-ast.js b/lib/mozilla-ast.js index e538d6e8..272e3e2a 100644 --- a/lib/mozilla-ast.js +++ b/lib/mozilla-ast.js @@ -212,7 +212,14 @@ end : my_end_token(M), name : M.name }); - } + }, + ThisExpression: function(M) { + return new AST_This({ + start : my_start_token(M), + end : my_end_token(M), + name : "this", + }); + }, }; MOZ_TO_ME.UpdateExpression = @@ -245,7 +252,6 @@ map("VariableDeclarator", AST_VarDef, "id>name, init>value"); map("CatchClause", AST_Catch, "param>argname, body%body"); - map("ThisExpression", AST_This); map("BinaryExpression", AST_Binary, "operator=operator, left>left, right>right"); map("LogicalExpression", AST_Binary, "operator=operator, left>left, right>right"); map("AssignmentExpression", AST_Assign, "operator=operator, left>left, right>right"); @@ -407,6 +413,10 @@ }; }); + def_to_moz(AST_This, function To_Moz_ThisExpression() { + return { type: "ThisExpression" }; + }); + def_to_moz(AST_RegExp, function To_Moz_RegExpLiteral(M) { var flags = M.value.toString().match(/[gimuy]*$/)[0]; var value = "/" + M.value.raw_source + "/" + flags; diff --git a/lib/utils.js b/lib/utils.js index 2451c536..8ae17304 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -147,6 +147,9 @@ var List = (function() { } return top.concat(ret); } + List.is_op = function(val) { + return val === skip || val instanceof AtTop || val instanceof Last || val instanceof Splice; + }; List.at_top = function(val) { return new AtTop(val); }; List.splice = function(val) { return new Splice(val); }; List.last = function(val) { return new Last(val); }; diff --git a/test/benchmark.js b/test/benchmark.js index ea044b26..8c2e819c 100644 --- a/test/benchmark.js +++ b/test/benchmark.js @@ -8,9 +8,9 @@ var fetch = require("./fetch"); var spawn = require("child_process").spawn; var zlib = require("zlib"); var args = process.argv.slice(2); -args.unshift("bin/uglifyjs"); if (!args.length) args.push("-mc"); -args.push("--timings"); +args.unshift("bin/uglifyjs"); +args.push("--validate", "--timings"); var urls = [ "https://code.jquery.com/jquery-3.4.1.js", "https://code.angularjs.org/1.7.8/angular.js", diff --git a/test/compress.js b/test/compress.js index cd2284c4..8a6dd228 100644 --- a/test/compress.js +++ b/test/compress.js @@ -14,6 +14,7 @@ var file = process.argv[2]; var dir = path.resolve(path.dirname(module.filename), "compress"); if (file) { var minify_options = require("./ufuzz/options.json").map(JSON.stringify); + U.AST_Node.enable_validation(); log("--- {file}", { file: file }); var tests = parse_test(path.resolve(dir, file)); process.exit(Object.keys(tests).filter(function(name) { diff --git a/test/jetstream.js b/test/jetstream.js index 3ad9677f..23c49b05 100644 --- a/test/jetstream.js +++ b/test/jetstream.js @@ -14,9 +14,9 @@ if (typeof phantom == "undefined") { args.splice(debug, 1); debug = true; } - args.unshift("bin/uglifyjs"); if (!args.length) args.push("-mcb", "beautify=false,webkit"); - args.push("--timings"); + args.unshift("bin/uglifyjs"); + args.push("--validate", "--timings"); var child_process = require("child_process"); var fetch = require("./fetch"); var http = require("http"); -- 2.34.1