diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..3d9a761d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: node_js +node_js: + - "0.4" + - "0.8" + - "0.10" + - "0.11" diff --git a/README.md b/README.md index ce55dc30..27d06cd6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ UglifyJS 2 ========== +[![Build Status](https://travis-ci.org/mishoo/UglifyJS2.png)](https://travis-ci.org/mishoo/UglifyJS2) UglifyJS is a JavaScript parser, minifier, compressor or beautifier toolkit. @@ -45,55 +46,72 @@ files. The available options are: - --source-map Specify an output file where to generate source map. - [string] - --source-map-root The path to the original source to be included in the - source map. [string] - --source-map-url The path to the source map to be added in //@ - sourceMappingURL. Defaults to the value passed with - --source-map. [string] - --in-source-map Input source map, useful if you're compressing JS that was - generated from some other original code. - --screw-ie8 Pass this flag if you don't care about full compliance with - Internet Explorer 6-8 quirks (by default UglifyJS will try - to be IE-proof). - -p, --prefix Skip prefix for original filenames that appear in source - maps. For example -p 3 will drop 3 directories from file - names and ensure they are relative paths. - -o, --output Output file (default STDOUT). - -b, --beautify Beautify output/specify output options. [string] - -m, --mangle Mangle names/pass mangler options. [string] - -r, --reserved Reserved names to exclude from mangling. - -c, --compress Enable compressor/pass compressor options. Pass options - like -c hoist_vars=false,if_return=false. Use -c with no - argument to use the default compression options. [string] - -d, --define Global definitions [string] - --comments Preserve copyright comments in the output. By default this - works like Google Closure, keeping JSDoc-style comments - that contain "@license" or "@preserve". You can optionally - pass one of the following arguments to this flag: - - "all" to keep all comments - - a valid JS regexp (needs to start with a slash) to keep - only comments that match. - Note that currently not *all* comments can be kept when - compression is on, because of dead code removal or - cascading statements into sequences. [string] - --stats Display operations run time on STDERR. [boolean] - --acorn Use Acorn for parsing. [boolean] - --spidermonkey Assume input fles are SpiderMonkey AST format (as JSON). - [boolean] - --self Build itself (UglifyJS2) as a library (implies - --wrap=UglifyJS --export-all) [boolean] - --wrap Embed everything in a big function, making the “exports” - and “global” variables available. You need to pass an - argument to this option to specify the name that your - module will take when included in, say, a browser. - [string] - --export-all Only used when --wrap, this tells UglifyJS to add code to - automatically export all globals. [boolean] - --lint Display some scope warnings [boolean] - -v, --verbose Verbose [boolean] - -V, --version Print version number and exit. [boolean] +``` + --source-map Specify an output file where to generate source map. + [string] + --source-map-root The path to the original source to be included in the + source map. [string] + --source-map-url The path to the source map to be added in //# + sourceMappingURL. Defaults to the value passed with + --source-map. [string] + --source-map-include-sources + Pass this flag if you want to include the content of + source files in the source map as sourcesContent + property. [boolean] + --in-source-map Input source map, useful if you're compressing JS that was + generated from some other original code. + --screw-ie8 Pass this flag if you don't care about full compliance + with Internet Explorer 6-8 quirks (by default UglifyJS + will try to be IE-proof). [boolean] + --expr Parse a single expression, rather than a program (for + parsing JSON) [boolean] + -p, --prefix Skip prefix for original filenames that appear in source + maps. For example -p 3 will drop 3 directories from file + names and ensure they are relative paths. You can also + specify -p relative, which will make UglifyJS figure out + itself the relative paths between original sources, the + source map and the output file. [string] + -o, --output Output file (default STDOUT). + -b, --beautify Beautify output/specify output options. [string] + -m, --mangle Mangle names/pass mangler options. [string] + -r, --reserved Reserved names to exclude from mangling. + -c, --compress Enable compressor/pass compressor options. Pass options + like -c hoist_vars=false,if_return=false. Use -c with no + argument to use the default compression options. [string] + -d, --define Global definitions [string] + -e, --enclose Embed everything in a big function, with a configurable + parameter/argument list. [string] + --comments Preserve copyright comments in the output. By default this + works like Google Closure, keeping JSDoc-style comments + that contain "@license" or "@preserve". You can optionally + pass one of the following arguments to this flag: + - "all" to keep all comments + - a valid JS regexp (needs to start with a slash) to keep + only comments that match. + Note that currently not *all* comments can be kept when + compression is on, because of dead code removal or + cascading statements into sequences. [string] + --preamble Preamble to prepend to the output. You can use this to + insert a comment, for example for licensing information. + This will not be parsed, but the source map will adjust + for its presence. + --stats Display operations run time on STDERR. [boolean] + --acorn Use Acorn for parsing. [boolean] + --spidermonkey Assume input files are SpiderMonkey AST format (as JSON). + [boolean] + --self Build itself (UglifyJS2) as a library (implies + --wrap=UglifyJS --export-all) [boolean] + --wrap Embed everything in a big function, making the “exports” + and “global” variables available. You need to pass an + argument to this option to specify the name that your + module will take when included in, say, a browser. + [string] + --export-all Only used when --wrap, this tells UglifyJS to add code to + automatically export all globals. [boolean] + --lint Display some scope warnings [boolean] + -v, --verbose Verbose [boolean] + -V, --version Print version number and exit. [boolean] +``` Specify `--output` (`-o`) to declare the output file. Otherwise the output goes to STDOUT. @@ -155,7 +173,7 @@ To enable the mangler you need to pass `--mangle` (`-m`). The following - `toplevel` — mangle names declared in the toplevel scope (disabled by default). -- `eval` — mangle names visible in scopes where `eval` or `when` are used +- `eval` — mangle names visible in scopes where `eval` or `with` are used (disabled by default). When mangling is enabled but you want to prevent certain names from being @@ -174,32 +192,70 @@ you can pass a comma-separated list of options. Options are in the form to set `true`; it's effectively a shortcut for `foo=true`). - `sequences` -- join consecutive simple statements using the comma operator + - `properties` -- rewrite property access using the dot notation, for example `foo["bar"] → foo.bar` + - `dead_code` -- remove unreachable code + - `drop_debugger` -- remove `debugger;` statements + - `unsafe` (default: false) -- apply "unsafe" transformations (discussion below) + - `conditionals` -- apply optimizations for `if`-s and conditional expressions + - `comparisons` -- apply certain optimizations to binary nodes, for example: `!(a <= b) → a > b` (only when `unsafe`), attempts to negate binary nodes, e.g. `a = !b && !c && !d && !e → a=!(b||c||d||e)` etc. + - `evaluate` -- attempt to evaluate constant expressions + - `booleans` -- various optimizations for boolean context, for example `!!a ? b : c → a ? b : c` + - `loops` -- optimizations for `do`, `while` and `for` loops when we can statically determine the condition + - `unused` -- drop unreferenced functions and variables + - `hoist_funs` -- hoist function declarations + - `hoist_vars` (default: false) -- hoist `var` declarations (this is `false` by default because it seems to increase the size of the output in general) + - `if_return` -- optimizations for if/return and if/continue + - `join_vars` -- join consecutive `var` statements + - `cascade` -- small optimization for sequences, transform `x, x` into `x` and `x = something(), x` into `x = something()` + - `warnings` -- display warnings when dropping unreachable code or unused declarations etc. +- `negate_iife` -- negate "Immediately-Called Function Expressions" + where the return value is discarded, to avoid the parens that the + code generator would insert. + +- `pure_getters` -- the default is `false`. If you pass `true` for + this, UglifyJS will assume that object property access + (e.g. `foo.bar` or `foo["bar"]`) doesn't have any side effects. + +- `pure_funcs` -- default `null`. You can pass an array of names and + UglifyJS will assume that those functions do not produce side + effects. DANGER: will not check if the name is redefined in scope. + An example case here, for instance `var q = Math.floor(a/b)`. If + variable `q` is not used elsewhere, UglifyJS will drop it, but will + still keep the `Math.floor(a/b)`, not knowing what it does. You can + pass `pure_funcs: [ 'Math.floor' ]` to let it know that this + function won't produce any side effect, in which case the whole + statement would get discarded. The current implementation adds some + overhead (compression will be slower). + +- `drop_console` -- default `false`. Pass `true` to discard calls to + `console.*` functions. + ### The `unsafe` option It enables some transformations that *might* break code logic in certain @@ -212,7 +268,7 @@ when this flag is on: - `String(exp)` or `exp.toString()` → `"" + exp` - `new Object/RegExp/Function/Error/Array (...)` → we discard the `new` - `typeof foo == "undefined"` → `foo === void 0` -- `void 0` → `"undefined"` (if there is a variable named "undefined" in +- `void 0` → `undefined` (if there is a variable named "undefined" in scope; we do it because the variable name will be mangled, typically reduced to a single character). @@ -222,10 +278,11 @@ You can use the `--define` (`-d`) switch in order to declare global variables that UglifyJS will assume to be constants (unless defined in scope). For example if you pass `--define DEBUG=false` then, coupled with dead code removal UglifyJS will discard the following from the output: - - if (DEBUG) { - console.log("debug stuff"); - } +```javascript +if (DEBUG) { + console.log("debug stuff"); +} +``` UglifyJS will warn about the condition being always false and about dropping unreachable code; for now there is no option to turn off only this specific @@ -234,10 +291,11 @@ warning, you can pass `warnings=false` to turn off *all* warnings. Another way of doing that is to declare your globals as constants in a separate file and include it into the build. For example you can have a `build/defines.js` file with the following: - - const DEBUG = false; - const PRODUCTION = true; - // etc. +```javascript +const DEBUG = false; +const PRODUCTION = true; +// etc. +``` and build your code like this: @@ -274,9 +332,6 @@ can pass additional arguments that control the code output: It doesn't work very well currently, but it does make the code generated by UglifyJS more readable. - `max-line-len` (default 32000) -- maximum line length (for uglified code) -- `ie-proof` (default `true`) -- generate “IE-proof” code (for now this - means add brackets around the do/while in code like this: `if (foo) do - something(); while (bar); else ...`. - `bracketize` (default `false`) -- always insert brackets in `if`, `for`, `do`, `while` or `with` statements, even if their body is a single statement. @@ -284,6 +339,10 @@ can pass additional arguments that control the code output: you pass `false` then whenever possible we will use a newline instead of a semicolon, leading to more readable output of uglified code (size before gzip could be smaller; size after gzip insignificantly larger). +- `preamble` (default `null`) -- when passed it must be a string and + it will be prepended to the output literally. The source map will + adjust for this text. Can be used to insert a comment containing + licensing information, for example. ### Keeping copyright notices or other comments @@ -296,14 +355,15 @@ keep only comments that match this regexp. For example `--comments Note, however, that there might be situations where comments are lost. For example: - - function f() { - /** @preserve Foo Bar */ - function g() { - // this function is never called - } - return something(); - } +```javascript +function f() { + /** @preserve Foo Bar */ + function g() { + // this function is never called + } + return something(); +} +``` Even though it has "@preserve", the comment will be lost because the inner function `g` (which is the AST node to which the comment is attached to) is @@ -345,8 +405,9 @@ API Reference Assuming installation via NPM, you can load UglifyJS in your application like this: - - var UglifyJS = require("uglify-js"); +```javascript +var UglifyJS = require("uglify-js"); +``` It exports a lot of names, but I'll discuss here the basics that are needed for parsing, mangling and compressing a piece of code. The sequence is (1) @@ -357,45 +418,49 @@ parse, (2) compress, (3) mangle, (4) generate output code. There's a single toplevel function which combines all the steps. If you don't need additional customization, you might want to go with `minify`. Example: - - var result = UglifyJS.minify("/path/to/file.js"); - console.log(result.code); // minified output - // if you need to pass code instead of file name - var result = UglifyJS.minify("var b = function () {};", {fromString: true}); +```javascript +var result = UglifyJS.minify("/path/to/file.js"); +console.log(result.code); // minified output +// if you need to pass code instead of file name +var result = UglifyJS.minify("var b = function () {};", {fromString: true}); +``` You can also compress multiple files: - - var result = UglifyJS.minify([ "file1.js", "file2.js", "file3.js" ]); - console.log(result.code); +```javascript +var result = UglifyJS.minify([ "file1.js", "file2.js", "file3.js" ]); +console.log(result.code); +``` To generate a source map: - - var result = UglifyJS.minify([ "file1.js", "file2.js", "file3.js" ], { - outSourceMap: "out.js.map" - }); - console.log(result.code); // minified output - console.log(result.map); +```javascript +var result = UglifyJS.minify([ "file1.js", "file2.js", "file3.js" ], { + outSourceMap: "out.js.map" +}); +console.log(result.code); // minified output +console.log(result.map); +``` Note that the source map is not saved in a file, it's just returned in `result.map`. The value passed for `outSourceMap` is only used to set the `file` attribute in the source map (see [the spec][sm-spec]). You can also specify sourceRoot property to be included in source map: - - var result = UglifyJS.minify([ "file1.js", "file2.js", "file3.js" ], { - outSourceMap: "out.js.map", - sourceRoot: "http://example.com/src" - }); - +```javascript +var result = UglifyJS.minify([ "file1.js", "file2.js", "file3.js" ], { + outSourceMap: "out.js.map", + sourceRoot: "http://example.com/src" +}); +``` If you're compressing compiled JavaScript and have a source map for it, you can use the `inSourceMap` argument: - - var result = UglifyJS.minify("compiled.js", { - inSourceMap: "compiled.js.map", - outSourceMap: "minified.js.map" - }); - // same as before, it returns `code` and `map` +```javascript +var result = UglifyJS.minify("compiled.js", { + inSourceMap: "compiled.js.map", + outSourceMap: "minified.js.map" +}); +// same as before, it returns `code` and `map` +``` The `inSourceMap` is only used if you also request `outSourceMap` (it makes no sense otherwise). @@ -425,8 +490,9 @@ Following there's more detailed API info, in case the `minify` function is too simple for your needs. #### The parser - - var toplevel_ast = UglifyJS.parse(code, options); +```javascript +var toplevel_ast = UglifyJS.parse(code, options); +``` `options` is optional and if present it must be an object. The following properties are available: @@ -440,15 +506,16 @@ properties are available: The last two options are useful when you'd like to minify multiple files and get a single file as the output and a proper source map. Our CLI tool does something like this: - - var toplevel = null; - files.forEach(function(file){ - var code = fs.readFileSync(file); - toplevel = UglifyJS.parse(code, { - filename: file, - toplevel: toplevel - }); - }); +```javascript +var toplevel = null; +files.forEach(function(file){ + var code = fs.readFileSync(file, "utf8"); + toplevel = UglifyJS.parse(code, { + filename: file, + toplevel: toplevel + }); +}); +``` After this, we have in `toplevel` a big AST containing all our files, with each token having proper information about where it came from. @@ -462,15 +529,17 @@ referenced, if it is a global or not, if a function is using `eval` or the `with` statement etc. I will discuss this some place else, for now what's important to know is that you need to call the following before doing anything with the tree: - - toplevel.figure_out_scope() +```javascript +toplevel.figure_out_scope() +``` #### Compression Like this: - - var compressor = UglifyJS.Compressor(options); - var compressed_ast = toplevel.transform(compressor); +```javascript +var compressor = UglifyJS.Compressor(options); +var compressed_ast = toplevel.transform(compressor); +``` The `options` can be missing. Available options are discussed above in “Compressor options”. Defaults should lead to best compression in most @@ -486,23 +555,26 @@ the compressor might drop unused variables / unreachable code and this might change the number of identifiers or their position). Optionally, you can call a trick that helps after Gzip (counting character frequency in non-mangleable words). Example: - - compressed_ast.figure_out_scope(); - compressed_ast.compute_char_frequency(); - compressed_ast.mangle_names(); +```javascript +compressed_ast.figure_out_scope(); +compressed_ast.compute_char_frequency(); +compressed_ast.mangle_names(); +``` #### Generating output AST nodes have a `print` method that takes an output stream. Essentially, to generate code you do this: - - var stream = UglifyJS.OutputStream(options); - compressed_ast.print(stream); - var code = stream.toString(); // this is your minified code +```javascript +var stream = UglifyJS.OutputStream(options); +compressed_ast.print(stream); +var code = stream.toString(); // this is your minified code +``` or, for a shortcut you can do: - - var code = compressed_ast.print_to_string(options); +```javascript +var code = compressed_ast.print_to_string(options); +``` As usual, `options` is optional. The output stream accepts a lot of otions, most of them documented above in section “Beautifier options”. The two @@ -540,16 +612,17 @@ to be a `SourceMap` object (which is a thin wrapper on top of the [source-map][source-map] library). Example: +```javascript +var source_map = UglifyJS.SourceMap(source_map_options); +var stream = UglifyJS.OutputStream({ + ... + source_map: source_map +}); +compressed_ast.print(stream); - var source_map = UglifyJS.SourceMap(source_map_options); - var stream = UglifyJS.OutputStream({ - ... - source_map: source_map - }); - compressed_ast.print(stream); - - var code = stream.toString(); - var map = source_map.toString(); // json output for your source map +var code = stream.toString(); +var map = source_map.toString(); // json output for your source map +``` The `source_map_options` (optional) can contain the following properties: diff --git a/bin/uglifyjs b/bin/uglifyjs index ee32fdf1..3a3318b2 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -7,6 +7,7 @@ var UglifyJS = require("../tools/node"); var sys = require("util"); var optimist = require("optimist"); var fs = require("fs"); +var path = require("path"); var async = require("async"); var acorn; var ARGS = optimist @@ -20,11 +21,15 @@ mangling you need to use `-c` and `-m`.\ ") .describe("source-map", "Specify an output file where to generate source map.") .describe("source-map-root", "The path to the original source to be included in the source map.") - .describe("source-map-url", "The path to the source map to be added in //@ sourceMappingURL. Defaults to the value passed with --source-map.") + .describe("source-map-url", "The path to the source map to be added in //# sourceMappingURL. Defaults to the value passed with --source-map.") + .describe("source-map-include-sources", "Pass this flag if you want to include the content of source files in the source map as sourcesContent property.") .describe("in-source-map", "Input source map, useful if you're compressing JS that was generated from some other original code.") .describe("screw-ie8", "Pass this flag if you don't care about full compliance with Internet Explorer 6-8 quirks (by default UglifyJS will try to be IE-proof).") + .describe("expr", "Parse a single expression, rather than a program (for parsing JSON)") .describe("p", "Skip prefix for original filenames that appear in source maps. \ -For example -p 3 will drop 3 directories from file names and ensure they are relative paths.") +For example -p 3 will drop 3 directories from file names and ensure they are relative paths. \ +You can also specify -p relative, which will make UglifyJS figure out itself the relative paths between original sources, \ +the source map and the output file.") .describe("o", "Output file (default STDOUT).") .describe("b", "Beautify output/specify output options.") .describe("m", "Mangle names/pass mangler options.") @@ -44,9 +49,13 @@ You can optionally pass one of the following arguments to this flag:\n\ Note that currently not *all* comments can be kept when compression is on, \ because of dead code removal or cascading statements into sequences.") + .describe("preamble", "Preamble to prepend to the output. You can use this to insert a \ +comment, for example for licensing information. This will not be \ +parsed, but the source map will adjust for its presence.") + .describe("stats", "Display operations run time on STDERR.") .describe("acorn", "Use Acorn for parsing.") - .describe("spidermonkey", "Assume input fles are SpiderMonkey AST format (as JSON).") + .describe("spidermonkey", "Assume input files are SpiderMonkey AST format (as JSON).") .describe("self", "Build itself (UglifyJS2) as a library (implies --wrap=UglifyJS --export-all)") .describe("wrap", "Embed everything in a big function, making the “exports” and “global” variables available. \ You need to pass an argument to this option to specify the name that your module will take when included in, say, a browser.") @@ -54,6 +63,7 @@ You need to pass an argument to this option to specify the name that your module .describe("lint", "Display some scope warnings") .describe("v", "Verbose") .describe("V", "Print version number and exit.") + .describe("noerr", "Don't throw an error for unknown options in -c, -b or -m.") .alias("p", "prefix") .alias("o", "output") @@ -76,6 +86,10 @@ You need to pass an argument to this option to specify the name that your module .string("e") .string("comments") .string("wrap") + .string("p") + + .boolean("expr") + .boolean("source-map-include-sources") .boolean("screw-ie8") .boolean("export-all") .boolean("self") @@ -85,6 +99,7 @@ You need to pass an argument to this option to specify the name that your module .boolean("spidermonkey") .boolean("lint") .boolean("V") + .boolean("noerr") .wrap(80) @@ -93,6 +108,12 @@ You need to pass an argument to this option to specify the name that your module normalize(ARGS); +if (ARGS.noerr) { + UglifyJS.DefaultsError.croak = function(msg, defs) { + sys.error("WARN: " + msg); + }; +} + if (ARGS.version || ARGS.V) { var json = require("../package.json"); sys.puts(json.name + ' ' + json.version); @@ -122,19 +143,21 @@ if (ARGS.d) { if (COMPRESS) COMPRESS.global_defs = getOptions("d"); } -if (ARGS.screw_ie8) { - if (COMPRESS) COMPRESS.screw_ie8 = true; - if (MANGLE) MANGLE.screw_ie8 = true; -} - if (ARGS.r) { if (MANGLE) MANGLE.except = ARGS.r.replace(/^\s+|\s+$/g).split(/\s*,+\s*/); } var OUTPUT_OPTIONS = { - beautify: BEAUTIFY ? true : false + beautify: BEAUTIFY ? true : false, + preamble: ARGS.preamble || null, }; +if (ARGS.screw_ie8) { + if (COMPRESS) COMPRESS.screw_ie8 = true; + if (MANGLE) MANGLE.screw_ie8 = true; + OUTPUT_OPTIONS.screw_ie8 = true; +} + if (BEAUTIFY) UglifyJS.merge(OUTPUT_OPTIONS, BEAUTIFY); @@ -196,9 +219,11 @@ if (files.filter(function(el){ return el == "-" }).length > 1) { var STATS = {}; var OUTPUT_FILE = ARGS.o; var TOPLEVEL = null; +var P_RELATIVE = ARGS.p && ARGS.p == "relative"; +var SOURCES_CONTENT = {}; var SOURCE_MAP = ARGS.source_map ? UglifyJS.SourceMap({ - file: OUTPUT_FILE, + file: P_RELATIVE ? path.relative(path.dirname(ARGS.source_map), OUTPUT_FILE) : OUTPUT_FILE, root: ARGS.source_map_root, orig: ORIG_MAP, }) : null; @@ -220,12 +245,20 @@ try { async.eachLimit(files, 1, function (file, cb) { read_whole_file(file, function (err, code) { if (err) { - sys.error("ERROR: can't read file: " + filename); + sys.error("ERROR: can't read file: " + file); process.exit(1); } if (ARGS.p != null) { - file = file.replace(/^\/+/, "").split(/\/+/).slice(ARGS.p).join("/"); + if (P_RELATIVE) { + file = path.relative(path.dirname(ARGS.source_map), file); + } else { + var p = parseInt(ARGS.p, 10); + if (!isNaN(p)) { + file = file.replace(/^\/+/, "").split(/\/+/).slice(ARGS.p).join("/"); + } + } } + SOURCES_CONTENT[file] = code; time_it("parse", function(){ if (ARGS.spidermonkey) { var program = JSON.parse(code); @@ -235,16 +268,26 @@ async.eachLimit(files, 1, function (file, cb) { else if (ARGS.acorn) { TOPLEVEL = acorn.parse(code, { locations : true, - trackComments : true, sourceFile : file, program : TOPLEVEL }); } else { - TOPLEVEL = UglifyJS.parse(code, { - filename: file, - toplevel: TOPLEVEL - }); + try { + TOPLEVEL = UglifyJS.parse(code, { + filename : file, + toplevel : TOPLEVEL, + expression : ARGS.expr, + }); + } catch(ex) { + if (ex instanceof UglifyJS.JS_Parse_Error) { + sys.error("Parse error at " + file + ":" + ex.line + "," + ex.col); + sys.error(ex.message); + sys.error(ex.stack); + process.exit(1); + } + throw ex; + } }; }); cb(); @@ -260,11 +303,12 @@ async.eachLimit(files, 1, function (file, cb) { if (ARGS.enclose) { var arg_parameter_list = ARGS.enclose; - - if (!(arg_parameter_list instanceof Array)) { + if (arg_parameter_list === true) { + arg_parameter_list = []; + } + else if (!(arg_parameter_list instanceof Array)) { arg_parameter_list = [arg_parameter_list]; } - TOPLEVEL = TOPLEVEL.wrap_enclose(arg_parameter_list); } @@ -297,6 +341,15 @@ async.eachLimit(files, 1, function (file, cb) { if (MANGLE) time_it("mangle", function(){ TOPLEVEL.mangle_names(MANGLE); }); + + if (ARGS.source_map_include_sources) { + for (var file in SOURCES_CONTENT) { + if (SOURCES_CONTENT.hasOwnProperty(file)) { + SOURCE_MAP.get().setSourceContent(file, SOURCES_CONTENT[file]); + } + } + } + time_it("generate", function(){ TOPLEVEL.print(output); }); @@ -305,14 +358,18 @@ async.eachLimit(files, 1, function (file, cb) { if (SOURCE_MAP) { fs.writeFileSync(ARGS.source_map, SOURCE_MAP, "utf8"); - output += "\n/*\n//@ sourceMappingURL=" + (ARGS.source_map_url || ARGS.source_map) + "\n*/"; + var source_map_url = ARGS.source_map_url || ( + P_RELATIVE + ? path.relative(path.dirname(OUTPUT_FILE), ARGS.source_map) + : ARGS.source_map + ); + output += "\n//# sourceMappingURL=" + source_map_url; } if (OUTPUT_FILE) { fs.writeFileSync(OUTPUT_FILE, output, "utf8"); } else { sys.print(output); - sys.error("\n"); } if (ARGS.stats) { @@ -344,7 +401,7 @@ function getOptions(x, constants) { if (x !== true) { var ast; try { - ast = UglifyJS.parse(x); + ast = UglifyJS.parse(x, { expression: true }); } catch(ex) { if (ex instanceof UglifyJS.JS_Parse_Error) { sys.error("Error parsing arguments in: " + x); @@ -352,8 +409,6 @@ function getOptions(x, constants) { } } ast.walk(new UglifyJS.TreeWalker(function(node){ - if (node instanceof UglifyJS.AST_Toplevel) return; // descend - if (node instanceof UglifyJS.AST_SimpleStatement) return; // descend if (node instanceof UglifyJS.AST_Seq) return; // descend if (node instanceof UglifyJS.AST_Assign) { var name = node.left.print_to_string({ beautify: false }).replace(/-/g, "_"); @@ -363,6 +418,11 @@ function getOptions(x, constants) { ret[name] = value; return true; // no descend } + if (node instanceof UglifyJS.AST_Symbol || node instanceof UglifyJS.AST_Binary) { + var name = node.print_to_string({ beautify: false }).replace(/-/g, "_"); + ret[name] = true; + return true; // no descend + } sys.error(node.TYPE) sys.error("Error parsing arguments in: " + x); process.exit(1); diff --git a/lib/ast.js b/lib/ast.js index a1301da8..051cd2fb 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -197,6 +197,10 @@ var AST_LabeledStatement = DEFNODE("LabeledStatement", "label", { } }, AST_StatementWithBody); +var AST_IterationStatement = DEFNODE("IterationStatement", null, { + $documentation: "Internal class. All loops inherit from it." +}, AST_StatementWithBody); + var AST_DWLoop = DEFNODE("DWLoop", "condition", { $documentation: "Base class for do/while statements", $propdoc: { @@ -208,7 +212,7 @@ var AST_DWLoop = DEFNODE("DWLoop", "condition", { this.body._walk(visitor); }); } -}, AST_StatementWithBody); +}, AST_IterationStatement); var AST_Do = DEFNODE("Do", null, { $documentation: "A `do` statement", @@ -233,7 +237,7 @@ var AST_For = DEFNODE("For", "init condition step", { this.body._walk(visitor); }); } -}, AST_StatementWithBody); +}, AST_IterationStatement); var AST_ForIn = DEFNODE("ForIn", "init name object", { $documentation: "A `for ... in` statement", @@ -249,7 +253,7 @@ var AST_ForIn = DEFNODE("ForIn", "init name object", { this.body._walk(visitor); }); } -}, AST_StatementWithBody); +}, AST_IterationStatement); var AST_With = DEFNODE("With", "expression", { $documentation: "A `with` statement", @@ -291,10 +295,10 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", { var parameters = []; arg_parameter_pairs.forEach(function(pair) { - var split = pair.split(":"); + var splitAt = pair.lastIndexOf(":"); - args.push(split[0]); - parameters.push(split[1]); + args.push(pair.substr(0, splitAt)); + parameters.push(pair.substr(splitAt + 1)); }); var wrapped_tl = "(function(" + parameters.join(",") + "){ '$ORIG'; })(" + args.join(",") + ")"; @@ -367,7 +371,7 @@ var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments", { }, AST_Scope); var AST_Accessor = DEFNODE("Accessor", null, { - $documentation: "A setter/getter function" + $documentation: "A setter/getter function. The `name` property is always null." }, AST_Lambda); var AST_Function = DEFNODE("Function", null, { @@ -494,12 +498,6 @@ var AST_Try = DEFNODE("Try", "bcatch bfinally", { } }, AST_Block); -// XXX: this is wrong according to ECMA-262 (12.4). the catch block -// should introduce another scope, as the argname should be visible -// only inside the catch block. However, doing it this way because of -// IE which simply introduces the name in the surrounding scope. If -// we ever want to fix this then AST_Catch should inherit from -// AST_Scope. var AST_Catch = DEFNODE("Catch", "argname", { $documentation: "A `catch` node; only makes sense as part of a `try` statement", $propdoc: { @@ -752,7 +750,7 @@ var AST_Object = DEFNODE("Object", "properties", { var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { $documentation: "Base class for literal object properties", $propdoc: { - key: "[string] the property name; it's always a plain string in our AST, no matter if it was a string, number or identifier in original code", + key: "[string] the property name converted to a string for ObjectKeyVal. For setters and getters this is an arbitrary AST_Node.", value: "[AST_Node] property value. For setters and getters this is an AST_Function." }, _walk: function(visitor) { @@ -821,7 +819,11 @@ var AST_SymbolCatch = DEFNODE("SymbolCatch", null, { var AST_Label = DEFNODE("Label", "references", { $documentation: "Symbol naming a label (declaration)", $propdoc: { - references: "[AST_LabelRef*] a list of nodes referring to this label" + references: "[AST_LoopControl*] a list of nodes referring to this label" + }, + initialize: function() { + this.references = []; + this.thedef = this; } }, AST_Symbol); @@ -945,6 +947,9 @@ TreeWalker.prototype = { if (x instanceof type) return x; } }, + has_directive: function(type) { + return this.find_parent(AST_Scope).has_directive(type); + }, in_boolean_context: function() { var stack = this.stack; var i = stack.length, self = stack[--i]; @@ -965,21 +970,15 @@ TreeWalker.prototype = { }, loopcontrol_target: function(label) { var stack = this.stack; - if (label) { - for (var i = stack.length; --i >= 0;) { - var x = stack[i]; - if (x instanceof AST_LabeledStatement && x.label.name == label.name) { - return x.body; - } - } - } else { - for (var i = stack.length; --i >= 0;) { - var x = stack[i]; - if (x instanceof AST_Switch - || x instanceof AST_For - || x instanceof AST_ForIn - || x instanceof AST_DWLoop) return x; + if (label) for (var i = stack.length; --i >= 0;) { + var x = stack[i]; + if (x instanceof AST_LabeledStatement && x.label.name == label.name) { + return x.body; } + } else for (var i = stack.length; --i >= 0;) { + var x = stack[i]; + if (x instanceof AST_Switch || x instanceof AST_IterationStatement) + return x; } } }; diff --git a/lib/compress.js b/lib/compress.js index ebd3dd7a..b589aca5 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -61,12 +61,18 @@ function Compressor(options, false_by_default) { loops : !false_by_default, unused : !false_by_default, hoist_funs : !false_by_default, + keep_fargs : false, hoist_vars : false, if_return : !false_by_default, join_vars : !false_by_default, cascade : !false_by_default, side_effects : !false_by_default, + pure_getters : false, + pure_funcs : null, + negate_iife : !false_by_default, screw_ie8 : false, + drop_console : false, + angular : false, warnings : true, global_defs : {} @@ -82,22 +88,16 @@ merge(Compressor.prototype, { }, before: function(node, descend, in_list) { if (node._squeezed) return node; + var was_scope = false; if (node instanceof AST_Scope) { - node.drop_unused(this); node = node.hoist_declarations(this); + was_scope = true; } descend(node, this); node = node.optimize(this); - if (node instanceof AST_Scope) { - // dead code removal might leave further unused declarations. - // this'll usually save very few bytes, but the performance - // hit seems negligible so I'll just drop it here. - - // no point to repeat warnings. - var save_warnings = this.options.warnings; - this.options.warnings = false; + if (was_scope && node instanceof AST_Scope) { node.drop_unused(this); - this.options.warnings = save_warnings; + descend(node, this); } node._squeezed = true; return node; @@ -200,6 +200,9 @@ merge(Compressor.prototype, { var CHANGED; do { CHANGED = false; + if (compressor.option("angular")) { + statements = process_for_angular(statements); + } statements = eliminate_spurious_blocks(statements); if (compressor.option("dead_code")) { statements = eliminate_dead_code(statements, compressor); @@ -214,8 +217,57 @@ merge(Compressor.prototype, { statements = join_consecutive_vars(statements, compressor); } } while (CHANGED); + + if (compressor.option("negate_iife")) { + negate_iifes(statements, compressor); + } + return statements; + function process_for_angular(statements) { + function make_injector(func, name) { + return make_node(AST_SimpleStatement, func, { + body: make_node(AST_Assign, func, { + operator: "=", + left: make_node(AST_Dot, name, { + expression: make_node(AST_SymbolRef, name, name), + property: "$inject" + }), + right: make_node(AST_Array, func, { + elements: func.argnames.map(function(sym){ + return make_node(AST_String, sym, { value: sym.name }); + }) + }) + }) + }); + } + return statements.reduce(function(a, stat){ + a.push(stat); + var token = stat.start; + var comments = token.comments_before; + if (comments && comments.length > 0) { + var last = comments.pop(); + if (/@ngInject/.test(last.value)) { + // case 1: defun + if (stat instanceof AST_Defun) { + a.push(make_injector(stat, stat.name)); + } + else if (stat instanceof AST_Definitions) { + stat.definitions.forEach(function(def){ + if (def.value && def.value instanceof AST_Lambda) { + a.push(make_injector(def.value, def.name)); + } + }); + } + else { + compressor.warn("Unknown statement marked with @ngInject [{file}:{line},{col}]", token); + } + } + } + return a; + }, []); + } + function eliminate_spurious_blocks(statements) { var seen_dirs = []; return statements.reduce(function(a, stat){ @@ -318,14 +370,14 @@ merge(Compressor.prototype, { || (ab instanceof AST_Continue && self === loop_body(lct)) || (ab instanceof AST_Break && lct instanceof AST_BlockStatement && self === lct))) { if (ab.label) { - remove(ab.label.thedef.references, ab.label); + remove(ab.label.thedef.references, ab); } CHANGED = true; var body = as_statement_array(stat.body).slice(0, -1); stat = stat.clone(); stat.condition = stat.condition.negate(compressor); stat.body = make_node(AST_BlockStatement, stat, { - body: ret + body: as_statement_array(stat.alternative).concat(ret) }); stat.alternative = make_node(AST_BlockStatement, stat, { body: body @@ -340,7 +392,7 @@ merge(Compressor.prototype, { || (ab instanceof AST_Continue && self === loop_body(lct)) || (ab instanceof AST_Break && lct instanceof AST_BlockStatement && self === lct))) { if (ab.label) { - remove(ab.label.thedef.references, ab.label); + remove(ab.label.thedef.references, ab); } CHANGED = true; stat = stat.clone(); @@ -379,7 +431,7 @@ merge(Compressor.prototype, { && loop_body(lct) === self) || (stat instanceof AST_Continue && loop_body(lct) === self)) { if (stat.label) { - remove(stat.label.thedef.references, stat.label); + remove(stat.label.thedef.references, stat); } } else { a.push(stat); @@ -497,6 +549,40 @@ merge(Compressor.prototype, { }, []); }; + function negate_iifes(statements, compressor) { + statements.forEach(function(stat){ + if (stat instanceof AST_SimpleStatement) { + stat.body = (function transform(thing) { + return thing.transform(new TreeTransformer(function(node){ + if (node instanceof AST_Call && node.expression instanceof AST_Function) { + return make_node(AST_UnaryPrefix, node, { + operator: "!", + expression: node + }); + } + else if (node instanceof AST_Call) { + node.expression = transform(node.expression); + } + else if (node instanceof AST_Seq) { + node.car = transform(node.car); + } + else if (node instanceof AST_Conditional) { + var expr = transform(node.condition); + if (expr !== node.condition) { + // it has been negated, reverse + node.condition = expr; + var tmp = node.consequent; + node.consequent = node.alternative; + node.alternative = tmp; + } + } + return node; + })); + })(stat.body); + } + }); + }; + }; function extract_declarations_from_unreachable_code(compressor, stat, target) { @@ -590,14 +676,14 @@ merge(Compressor.prototype, { // elements. If the node has been successfully reduced to a // constant, then the second element tells us the value; // otherwise the second element is missing. The first element - // of the array is always an AST_Node descendant; when + // of the array is always an AST_Node descendant; if // evaluation was successful it's a node that represents the - // constant; otherwise it's the original node. + // constant; otherwise it's the original or a replacement node. AST_Node.DEFMETHOD("evaluate", function(compressor){ if (!compressor.option("evaluate")) return [ this ]; try { - var val = this._eval(), ast = make_node_from_constant(compressor, val, this); - return [ best_of(ast, this), val ]; + var val = this._eval(compressor); + return [ best_of(make_node_from_constant(compressor, val, this), this), val ]; } catch(ex) { if (ex !== def) throw ex; return [ this ]; @@ -611,10 +697,12 @@ merge(Compressor.prototype, { // inherits from AST_Statement; however, an AST_Function // isn't really a statement. This could byte in other // places too. :-( Wish JS had multiple inheritance. - return [ this ]; + throw def; }); - function ev(node) { - return node._eval(); + function ev(node, compressor) { + if (!compressor) throw new Error("Compressor must be passed"); + + return node._eval(compressor); }; def(AST_Node, function(){ throw def; // not constant @@ -622,69 +710,69 @@ merge(Compressor.prototype, { def(AST_Constant, function(){ return this.getValue(); }); - def(AST_UnaryPrefix, function(){ + def(AST_UnaryPrefix, function(compressor){ var e = this.expression; switch (this.operator) { - case "!": return !ev(e); + case "!": return !ev(e, compressor); case "typeof": // Function would be evaluated to an array and so typeof would // incorrectly return 'object'. Hence making is a special case. if (e instanceof AST_Function) return typeof function(){}; - e = ev(e); + e = ev(e, compressor); // typeof returns "object" or "function" on different platforms // so cannot evaluate reliably if (e instanceof RegExp) throw def; return typeof e; - case "void": return void ev(e); - case "~": return ~ev(e); + case "void": return void ev(e, compressor); + case "~": return ~ev(e, compressor); case "-": - e = ev(e); + e = ev(e, compressor); if (e === 0) throw def; return -e; - case "+": return +ev(e); + case "+": return +ev(e, compressor); } throw def; }); - def(AST_Binary, function(){ + def(AST_Binary, function(c){ var left = this.left, right = this.right; switch (this.operator) { - case "&&" : return ev(left) && ev(right); - case "||" : return ev(left) || ev(right); - case "|" : return ev(left) | ev(right); - case "&" : return ev(left) & ev(right); - case "^" : return ev(left) ^ ev(right); - case "+" : return ev(left) + ev(right); - case "*" : return ev(left) * ev(right); - case "/" : return ev(left) / ev(right); - case "%" : return ev(left) % ev(right); - case "-" : return ev(left) - ev(right); - case "<<" : return ev(left) << ev(right); - case ">>" : return ev(left) >> ev(right); - case ">>>" : return ev(left) >>> ev(right); - case "==" : return ev(left) == ev(right); - case "===" : return ev(left) === ev(right); - case "!=" : return ev(left) != ev(right); - case "!==" : return ev(left) !== ev(right); - case "<" : return ev(left) < ev(right); - case "<=" : return ev(left) <= ev(right); - case ">" : return ev(left) > ev(right); - case ">=" : return ev(left) >= ev(right); - case "in" : return ev(left) in ev(right); - case "instanceof" : return ev(left) instanceof ev(right); + case "&&" : return ev(left, c) && ev(right, c); + case "||" : return ev(left, c) || ev(right, c); + case "|" : return ev(left, c) | ev(right, c); + case "&" : return ev(left, c) & ev(right, c); + case "^" : return ev(left, c) ^ ev(right, c); + case "+" : return ev(left, c) + ev(right, c); + case "*" : return ev(left, c) * ev(right, c); + case "/" : return ev(left, c) / ev(right, c); + case "%" : return ev(left, c) % ev(right, c); + case "-" : return ev(left, c) - ev(right, c); + case "<<" : return ev(left, c) << ev(right, c); + case ">>" : return ev(left, c) >> ev(right, c); + case ">>>" : return ev(left, c) >>> ev(right, c); + case "==" : return ev(left, c) == ev(right, c); + case "===" : return ev(left, c) === ev(right, c); + case "!=" : return ev(left, c) != ev(right, c); + case "!==" : return ev(left, c) !== ev(right, c); + case "<" : return ev(left, c) < ev(right, c); + case "<=" : return ev(left, c) <= ev(right, c); + case ">" : return ev(left, c) > ev(right, c); + case ">=" : return ev(left, c) >= ev(right, c); + case "in" : return ev(left, c) in ev(right, c); + case "instanceof" : return ev(left, c) instanceof ev(right, c); } throw def; }); - def(AST_Conditional, function(){ - return ev(this.condition) - ? ev(this.consequent) - : ev(this.alternative); + def(AST_Conditional, function(compressor){ + return ev(this.condition, compressor) + ? ev(this.consequent, compressor) + : ev(this.alternative, compressor); }); - def(AST_SymbolRef, function(){ + def(AST_SymbolRef, function(compressor){ var d = this.definition(); - if (d && d.constant && d.init) return ev(d.init); + if (d && d.constant && d.init) return ev(d.init, compressor); throw def; }); })(function(node, func){ @@ -760,70 +848,78 @@ merge(Compressor.prototype, { // determine if expression has side effects (function(def){ - def(AST_Node, function(){ return true }); + def(AST_Node, function(compressor){ return true }); - def(AST_EmptyStatement, function(){ return false }); - def(AST_Constant, function(){ return false }); - def(AST_This, function(){ return false }); + def(AST_EmptyStatement, function(compressor){ return false }); + def(AST_Constant, function(compressor){ return false }); + def(AST_This, function(compressor){ return false }); - def(AST_Block, function(){ + def(AST_Call, function(compressor){ + var pure = compressor.option("pure_funcs"); + if (!pure) return true; + return pure.indexOf(this.expression.print_to_string()) < 0; + }); + + def(AST_Block, function(compressor){ for (var i = this.body.length; --i >= 0;) { - if (this.body[i].has_side_effects()) + if (this.body[i].has_side_effects(compressor)) return true; } return false; }); - def(AST_SimpleStatement, function(){ - return this.body.has_side_effects(); + def(AST_SimpleStatement, function(compressor){ + return this.body.has_side_effects(compressor); }); - def(AST_Defun, function(){ return true }); - def(AST_Function, function(){ return false }); - def(AST_Binary, function(){ - return this.left.has_side_effects() - || this.right.has_side_effects(); + def(AST_Defun, function(compressor){ return true }); + def(AST_Function, function(compressor){ return false }); + def(AST_Binary, function(compressor){ + return this.left.has_side_effects(compressor) + || this.right.has_side_effects(compressor); }); - def(AST_Assign, function(){ return true }); - def(AST_Conditional, function(){ - return this.condition.has_side_effects() - || this.consequent.has_side_effects() - || this.alternative.has_side_effects(); + def(AST_Assign, function(compressor){ return true }); + def(AST_Conditional, function(compressor){ + return this.condition.has_side_effects(compressor) + || this.consequent.has_side_effects(compressor) + || this.alternative.has_side_effects(compressor); }); - def(AST_Unary, function(){ + def(AST_Unary, function(compressor){ return this.operator == "delete" || this.operator == "++" || this.operator == "--" - || this.expression.has_side_effects(); + || this.expression.has_side_effects(compressor); }); - def(AST_SymbolRef, function(){ return false }); - def(AST_Object, function(){ + def(AST_SymbolRef, function(compressor){ return false }); + def(AST_Object, function(compressor){ for (var i = this.properties.length; --i >= 0;) - if (this.properties[i].has_side_effects()) + if (this.properties[i].has_side_effects(compressor)) return true; return false; }); - def(AST_ObjectProperty, function(){ - return this.value.has_side_effects(); + def(AST_ObjectProperty, function(compressor){ + return this.value.has_side_effects(compressor); }); - def(AST_Array, function(){ + def(AST_Array, function(compressor){ for (var i = this.elements.length; --i >= 0;) - if (this.elements[i].has_side_effects()) + if (this.elements[i].has_side_effects(compressor)) return true; return false; }); - // def(AST_Dot, function(){ - // return this.expression.has_side_effects(); - // }); - // def(AST_Sub, function(){ - // return this.expression.has_side_effects() - // || this.property.has_side_effects(); - // }); - def(AST_PropAccess, function(){ - return true; + def(AST_Dot, function(compressor){ + if (!compressor.option("pure_getters")) return true; + return this.expression.has_side_effects(compressor); }); - def(AST_Seq, function(){ - return this.car.has_side_effects() - || this.cdr.has_side_effects(); + def(AST_Sub, function(compressor){ + if (!compressor.option("pure_getters")) return true; + return this.expression.has_side_effects(compressor) + || this.property.has_side_effects(compressor); + }); + def(AST_PropAccess, function(compressor){ + return !compressor.option("pure_getters"); + }); + def(AST_Seq, function(compressor){ + return this.car.has_side_effects(compressor) + || this.cdr.has_side_effects(compressor); }); })(function(node, func){ node.DEFMETHOD("has_side_effects", func); @@ -907,7 +1003,7 @@ merge(Compressor.prototype, { node.definitions.forEach(function(def){ if (def.value) { initializations.add(def.name.name, def.value); - if (def.value.has_side_effects()) { + if (def.value.has_side_effects(compressor)) { def.value.walk(tw); } } @@ -948,19 +1044,21 @@ merge(Compressor.prototype, { // pass 3: we should drop declarations not in_use var tt = new TreeTransformer( function before(node, descend, in_list) { - if (node instanceof AST_Lambda) { - for (var a = node.argnames, i = a.length; --i >= 0;) { - var sym = a[i]; - if (sym.unreferenced()) { - a.pop(); - compressor.warn("Dropping unused function argument {name} [{file}:{line},{col}]", { - name : sym.name, - file : sym.start.file, - line : sym.start.line, - col : sym.start.col - }); + if (node instanceof AST_Lambda && !(node instanceof AST_Accessor)) { + if (!compressor.option("keep_fargs")) { + for (var a = node.argnames, i = a.length; --i >= 0;) { + var sym = a[i]; + if (sym.unreferenced()) { + a.pop(); + compressor.warn("Dropping unused function argument {name} [{file}:{line},{col}]", { + name : sym.name, + file : sym.start.file, + line : sym.start.line, + col : sym.start.col + }); + } + else break; } - else break; } } if (node instanceof AST_Defun && node !== self) { @@ -984,7 +1082,7 @@ merge(Compressor.prototype, { line : def.name.start.line, col : def.name.start.col }; - if (def.value && def.value.has_side_effects()) { + if (def.value && def.value.has_side_effects(compressor)) { def._unused_side_effects = true; compressor.warn("Side effects in initialization of unused variable {name} [{file}:{line},{col}]", w); return true; @@ -1038,18 +1136,23 @@ merge(Compressor.prototype, { } return node; } - if (node instanceof AST_For && node.init instanceof AST_BlockStatement) { + if (node instanceof AST_For) { descend(node, this); - // certain combination of unused name + side effect leads to: - // https://github.com/mishoo/UglifyJS2/issues/44 - // that's an invalid AST. - // We fix it at this stage by moving the `var` outside the `for`. - var body = node.init.body.slice(0, -1); - node.init = node.init.body.slice(-1)[0].body; - body.push(node); - return in_list ? MAP.splice(body) : make_node(AST_BlockStatement, node, { - body: body - }); + + if (node.init instanceof AST_BlockStatement) { + // certain combination of unused name + side effect leads to: + // https://github.com/mishoo/UglifyJS2/issues/44 + // that's an invalid AST. + // We fix it at this stage by moving the `var` outside the `for`. + + var body = node.init.body.slice(0, -1); + node.init = node.init.body.slice(-1)[0].body; + body.push(node); + + return in_list ? MAP.splice(body) : make_node(AST_BlockStatement, node, { + body: body + }); + } } if (node instanceof AST_Scope && node !== self) return node; @@ -1186,7 +1289,7 @@ merge(Compressor.prototype, { OPT(AST_SimpleStatement, function(self, compressor){ if (compressor.option("side_effects")) { - if (!self.body.has_side_effects()) { + if (!self.body.has_side_effects(compressor)) { compressor.warn("Dropping side-effect-free statement [{file}:{line},{col}]", self.start); return make_node(AST_EmptyStatement, self); } @@ -1570,7 +1673,7 @@ merge(Compressor.prototype, { if (self.args.length != 1) { return make_node(AST_Array, self, { elements: self.args - }); + }).transform(compressor); } break; case "Object": @@ -1584,11 +1687,80 @@ merge(Compressor.prototype, { if (self.args.length == 0) return make_node(AST_String, self, { value: "" }); - return make_node(AST_Binary, self, { + if (self.args.length <= 1) return make_node(AST_Binary, self, { left: self.args[0], operator: "+", right: make_node(AST_String, self, { value: "" }) + }).transform(compressor); + break; + case "Number": + if (self.args.length == 0) return make_node(AST_Number, self, { + value: 0 }); + if (self.args.length == 1) return make_node(AST_UnaryPrefix, self, { + expression: self.args[0], + operator: "+" + }).transform(compressor); + case "Boolean": + if (self.args.length == 0) return make_node(AST_False, self); + if (self.args.length == 1) return make_node(AST_UnaryPrefix, self, { + expression: make_node(AST_UnaryPrefix, null, { + expression: self.args[0], + operator: "!" + }), + operator: "!" + }).transform(compressor); + break; + case "Function": + if (all(self.args, function(x){ return x instanceof AST_String })) { + // quite a corner-case, but we can handle it: + // https://github.com/mishoo/UglifyJS2/issues/203 + // if the code argument is a constant, then we can minify it. + try { + var code = "(function(" + self.args.slice(0, -1).map(function(arg){ + return arg.value; + }).join(",") + "){" + self.args[self.args.length - 1].value + "})()"; + var ast = parse(code); + ast.figure_out_scope({ screw_ie8: compressor.option("screw_ie8") }); + var comp = new Compressor(compressor.options); + ast = ast.transform(comp); + ast.figure_out_scope({ screw_ie8: compressor.option("screw_ie8") }); + ast.mangle_names(); + var fun; + try { + ast.walk(new TreeWalker(function(node){ + if (node instanceof AST_Lambda) { + fun = node; + throw ast; + } + })); + } catch(ex) { + if (ex !== ast) throw ex; + }; + var args = fun.argnames.map(function(arg, i){ + return make_node(AST_String, self.args[i], { + value: arg.print_to_string() + }); + }); + var code = OutputStream(); + AST_BlockStatement.prototype._codegen.call(fun, fun, code); + code = code.toString().replace(/^\{|\}$/g, ""); + args.push(make_node(AST_String, self.args[self.args.length - 1], { + value: code + })); + self.args = args; + return self; + } catch(ex) { + if (ex instanceof JS_Parse_Error) { + compressor.warn("Error parsing code passed to new Function [{file}:{line},{col}]", self.args[self.args.length - 1].start); + compressor.warn(ex.toString()); + } else { + console.log(ex); + throw ex; + } + } + } + break; } } else if (exp instanceof AST_Dot && exp.property == "toString" && self.args.length == 0) { @@ -1598,15 +1770,70 @@ merge(Compressor.prototype, { right: exp.expression }).transform(compressor); } + else if (exp instanceof AST_Dot && exp.expression instanceof AST_Array && exp.property == "join") EXIT: { + var separator = self.args.length == 0 ? "," : self.args[0].evaluate(compressor)[1]; + if (separator == null) break EXIT; // not a constant + var elements = exp.expression.elements.reduce(function(a, el){ + el = el.evaluate(compressor); + if (a.length == 0 || el.length == 1) { + a.push(el); + } else { + var last = a[a.length - 1]; + if (last.length == 2) { + // it's a constant + var val = "" + last[1] + separator + el[1]; + a[a.length - 1] = [ make_node_from_constant(compressor, val, last[0]), val ]; + } else { + a.push(el); + } + } + return a; + }, []); + if (elements.length == 0) return make_node(AST_String, self, { value: "" }); + if (elements.length == 1) return elements[0][0]; + if (separator == "") { + var first; + if (elements[0][0] instanceof AST_String + || elements[1][0] instanceof AST_String) { + first = elements.shift()[0]; + } else { + first = make_node(AST_String, self, { value: "" }); + } + return elements.reduce(function(prev, el){ + return make_node(AST_Binary, el[0], { + operator : "+", + left : prev, + right : el[0], + }); + }, first).transform(compressor); + } + // need this awkward cloning to not affect original element + // best_of will decide which one to get through. + var node = self.clone(); + node.expression = node.expression.clone(); + node.expression.expression = node.expression.expression.clone(); + node.expression.expression.elements = elements.map(function(el){ + return el[0]; + }); + return best_of(self, node); + } } if (compressor.option("side_effects")) { if (self.expression instanceof AST_Function && self.args.length == 0 - && !AST_Block.prototype.has_side_effects.call(self.expression)) { + && !AST_Block.prototype.has_side_effects.call(self.expression, compressor)) { return make_node(AST_Undefined, self).transform(compressor); } } - return self; + if (compressor.option("drop_console")) { + if (self.expression instanceof AST_PropAccess && + self.expression.expression instanceof AST_SymbolRef && + self.expression.expression.name == "console" && + self.expression.expression.undeclared()) { + return make_node(AST_Undefined, self).transform(compressor); + } + } + return self.evaluate(compressor)[0]; }); OPT(AST_New, function(self, compressor){ @@ -1629,7 +1856,7 @@ merge(Compressor.prototype, { OPT(AST_Seq, function(self, compressor){ if (!compressor.option("side_effects")) return self; - if (!self.car.has_side_effects()) { + if (!self.car.has_side_effects(compressor)) { // we shouldn't compress (1,eval)(something) to // eval(something) because that changes the meaning of // eval (becomes lexical instead of global). @@ -1644,16 +1871,34 @@ merge(Compressor.prototype, { } if (compressor.option("cascade")) { if (self.car instanceof AST_Assign - && !self.car.left.has_side_effects() - && self.car.left.equivalent_to(self.cdr)) { - return self.car; + && !self.car.left.has_side_effects(compressor)) { + if (self.car.left.equivalent_to(self.cdr)) { + return self.car; + } + if (self.cdr instanceof AST_Call + && self.cdr.expression.equivalent_to(self.car.left)) { + self.cdr.expression = self.car; + return self.cdr; + } } - if (!self.car.has_side_effects() - && !self.cdr.has_side_effects() + if (!self.car.has_side_effects(compressor) + && !self.cdr.has_side_effects(compressor) && self.car.equivalent_to(self.cdr)) { return self.car; } } + if (self.cdr instanceof AST_UnaryPrefix + && self.cdr.operator == "void" + && !self.cdr.expression.has_side_effects(compressor)) { + self.cdr.operator = self.car; + return self.cdr; + } + if (self.cdr instanceof AST_Undefined) { + return make_node(AST_UnaryPrefix, self, { + operator : "void", + expression : self.car + }); + } return self; }); @@ -1699,6 +1944,14 @@ merge(Compressor.prototype, { return self.evaluate(compressor)[0]; }); + function has_side_effects_or_prop_access(node, compressor) { + var save_pure_getters = compressor.option("pure_getters"); + compressor.options.pure_getters = false; + var ret = node.has_side_effects(compressor); + compressor.options.pure_getters = save_pure_getters; + return ret; + } + AST_Binary.DEFMETHOD("lift_sequences", function(compressor){ if (compressor.option("sequences")) { if (this.left instanceof AST_Seq) { @@ -1710,8 +1963,8 @@ merge(Compressor.prototype, { return seq; } if (this.right instanceof AST_Seq - && !(this.operator == "||" || this.operator == "&&") - && !this.left.has_side_effects()) { + && this instanceof AST_Assign + && !has_side_effects_or_prop_access(this.left, compressor)) { var seq = this.right; var x = seq.to_array(); this.right = x.pop(); @@ -1726,18 +1979,52 @@ merge(Compressor.prototype, { var commutativeOperators = makePredicate("== === != !== * & | ^"); OPT(AST_Binary, function(self, compressor){ - function reverse(op) { - if (!(self.left.has_side_effects() || self.right.has_side_effects())) { - if (op) self.operator = op; - var tmp = self.left; - self.left = self.right; - self.right = tmp; - } - }; + var reverse = compressor.has_directive("use asm") ? noop + : function(op, force) { + if (force || !(self.left.has_side_effects(compressor) || self.right.has_side_effects(compressor))) { + if (op) self.operator = op; + var tmp = self.left; + self.left = self.right; + self.right = tmp; + } + }; if (commutativeOperators(self.operator)) { if (self.right instanceof AST_Constant && !(self.left instanceof AST_Constant)) { - reverse(); + // if right is a constant, whatever side effects the + // left side might have could not influence the + // result. hence, force switch. + + if (!(self.left instanceof AST_Binary + && PRECEDENCE[self.left.operator] >= PRECEDENCE[self.operator])) { + reverse(null, true); + } + } + if (/^[!=]==?$/.test(self.operator)) { + if (self.left instanceof AST_SymbolRef && self.right instanceof AST_Conditional) { + if (self.right.consequent instanceof AST_SymbolRef + && self.right.consequent.definition() === self.left.definition()) { + if (/^==/.test(self.operator)) return self.right.condition; + if (/^!=/.test(self.operator)) return self.right.condition.negate(compressor); + } + if (self.right.alternative instanceof AST_SymbolRef + && self.right.alternative.definition() === self.left.definition()) { + if (/^==/.test(self.operator)) return self.right.condition.negate(compressor); + if (/^!=/.test(self.operator)) return self.right.condition; + } + } + if (self.right instanceof AST_SymbolRef && self.left instanceof AST_Conditional) { + if (self.left.consequent instanceof AST_SymbolRef + && self.left.consequent.definition() === self.right.definition()) { + if (/^==/.test(self.operator)) return self.left.condition; + if (/^!=/.test(self.operator)) return self.left.condition.negate(compressor); + } + if (self.left.alternative instanceof AST_SymbolRef + && self.left.alternative.definition() === self.right.definition()) { + if (/^==/.test(self.operator)) return self.left.condition.negate(compressor); + if (/^!=/.test(self.operator)) return self.left.condition; + } + } } } self = self.lift_sequences(compressor); @@ -1758,8 +2045,8 @@ merge(Compressor.prototype, { && compressor.option("unsafe")) { if (!(self.right.expression instanceof AST_SymbolRef) || !self.right.expression.undeclared()) { - self.left = self.right.expression; - self.right = make_node(AST_Undefined, self.left).optimize(compressor); + self.right = self.right.expression; + self.left = make_node(AST_Undefined, self.left).optimize(compressor); if (self.operator.length == 2) self.operator += "="; } } @@ -1804,11 +2091,6 @@ merge(Compressor.prototype, { } break; } - var exp = self.evaluate(compressor); - if (exp.length > 1) { - if (best_of(exp[0], self) !== self) - return exp[0]; - } if (compressor.option("comparisons")) { if (!(compressor.parent() instanceof AST_Binary) || compressor.parent() instanceof AST_Assign) { @@ -1828,7 +2110,76 @@ merge(Compressor.prototype, { && self.left.operator == "+" && self.left.is_string(compressor)) { return self.left; } - return self; + if (compressor.option("evaluate")) { + if (self.operator == "+") { + if (self.left instanceof AST_Constant + && self.right instanceof AST_Binary + && self.right.operator == "+" + && self.right.left instanceof AST_Constant + && self.right.is_string(compressor)) { + self = make_node(AST_Binary, self, { + operator: "+", + left: make_node(AST_String, null, { + value: "" + self.left.getValue() + self.right.left.getValue(), + start: self.left.start, + end: self.right.left.end + }), + right: self.right.right + }); + } + if (self.right instanceof AST_Constant + && self.left instanceof AST_Binary + && self.left.operator == "+" + && self.left.right instanceof AST_Constant + && self.left.is_string(compressor)) { + self = make_node(AST_Binary, self, { + operator: "+", + left: self.left.left, + right: make_node(AST_String, null, { + value: "" + self.left.right.getValue() + self.right.getValue(), + start: self.left.right.start, + end: self.right.end + }) + }); + } + if (self.left instanceof AST_Binary + && self.left.operator == "+" + && self.left.is_string(compressor) + && self.left.right instanceof AST_Constant + && self.right instanceof AST_Binary + && self.right.operator == "+" + && self.right.left instanceof AST_Constant + && self.right.is_string(compressor)) { + self = make_node(AST_Binary, self, { + operator: "+", + left: make_node(AST_Binary, self.left, { + operator: "+", + left: self.left.left, + right: make_node(AST_String, null, { + value: "" + self.left.right.getValue() + self.right.left.getValue(), + start: self.left.right.start, + end: self.right.left.end + }) + }), + right: self.right.right + }); + } + } + } + // x * (y * z) ==> x * y * z + if (self.right instanceof AST_Binary + && self.right.operator == self.operator + && (self.operator == "*" || self.operator == "&&" || self.operator == "||")) + { + self.left = make_node(AST_Binary, self.left, { + operator : self.operator, + left : self.left, + right : self.right.left + }); + self.right = self.right.right; + return self.transform(compressor); + } + return self.evaluate(compressor)[0]; }); OPT(AST_SymbolRef, function(self, compressor){ @@ -1919,7 +2270,7 @@ merge(Compressor.prototype, { * ==> * exp = foo ? something : something_else; */ - self = make_node(AST_Assign, self, { + return make_node(AST_Assign, self, { operator: consequent.operator, left: consequent.left, right: make_node(AST_Conditional, self, { @@ -1929,6 +2280,38 @@ merge(Compressor.prototype, { }) }); } + if (consequent instanceof AST_Call + && alternative.TYPE === consequent.TYPE + && consequent.args.length == alternative.args.length + && consequent.expression.equivalent_to(alternative.expression)) { + if (consequent.args.length == 0) { + return make_node(AST_Seq, self, { + car: self.condition, + cdr: consequent + }); + } + if (consequent.args.length == 1) { + consequent.args[0] = make_node(AST_Conditional, self, { + condition: self.condition, + consequent: consequent.args[0], + alternative: alternative.args[0] + }); + return consequent; + } + } + // x?y?z:a:a --> x&&y?z:a + if (consequent instanceof AST_Conditional + && consequent.alternative.equivalent_to(alternative)) { + return make_node(AST_Conditional, self, { + condition: make_node(AST_Binary, self, { + left: self.condition, + operator: "&&", + right: consequent.condition + }), + consequent: consequent.consequent, + alternative: alternative + }); + } return self; }); @@ -1962,12 +2345,18 @@ merge(Compressor.prototype, { var prop = self.property; if (prop instanceof AST_String && compressor.option("properties")) { prop = prop.getValue(); - if (is_identifier(prop) || compressor.option("screw_ie8")) { + if (RESERVED_WORDS(prop) ? compressor.option("screw_ie8") : is_identifier_string(prop)) { return make_node(AST_Dot, self, { expression : self.expression, property : prop }); } + var v = parseFloat(prop); + if (!isNaN(v) && v.toString() == prop) { + self.property = make_node(AST_Number, self.property, { + value: v + }); + } } return self; }); diff --git a/lib/mozilla-ast.js b/lib/mozilla-ast.js index 982d621a..bc24dfd6 100644 --- a/lib/mozilla-ast.js +++ b/lib/mozilla-ast.js @@ -51,7 +51,7 @@ start : my_start_token(M), end : my_end_token(M), body : from_moz(M.block).body, - bcatch : from_moz(M.handlers[0]), + bcatch : from_moz(M.handlers ? M.handlers[0] : M.handler), bfinally : M.finalizer ? new AST_Finally(from_moz(M.finalizer)) : null }); }, @@ -148,12 +148,14 @@ }; function From_Moz_Unary(M) { - return new (M.prefix ? AST_UnaryPrefix : AST_UnaryPostfix)({ + var prefix = "prefix" in M ? M.prefix + : M.type == "UnaryExpression" ? true : false; + return new (prefix ? AST_UnaryPrefix : AST_UnaryPostfix)({ start : my_start_token(M), end : my_end_token(M), operator : M.operator, expression : from_moz(M.argument) - }) + }); }; var ME_TO_MOZ = {}; diff --git a/lib/output.js b/lib/output.js index defd0215..b9637929 100644 --- a/lib/output.js +++ b/lib/output.js @@ -46,21 +46,23 @@ function OutputStream(options) { options = defaults(options, { - indent_start : 0, - indent_level : 4, - quote_keys : false, - space_colon : true, - ascii_only : false, - inline_script : false, - width : 80, - max_line_len : 32000, - ie_proof : true, - beautify : false, - source_map : null, - bracketize : false, - semicolons : true, - comments : false, - preserve_line : false + indent_start : 0, + indent_level : 4, + quote_keys : false, + space_colon : true, + ascii_only : false, + unescape_regexps : false, + inline_script : false, + width : 80, + max_line_len : 32000, + beautify : false, + source_map : null, + bracketize : false, + semicolons : true, + comments : false, + preserve_line : false, + screw_ie8 : false, + preamble : null, }, true); var indentation = 0; @@ -95,7 +97,7 @@ function OutputStream(options) { case "\u2029": return "\\u2029"; case '"': ++dq; return '"'; case "'": ++sq; return "'"; - case "\0": return "\\0"; + case "\0": return "\\x00"; } return s; }); @@ -299,6 +301,10 @@ function OutputStream(options) { return OUTPUT; }; + if (options.preamble) { + print(options.preamble.replace(/\r\n?|[\n\u2028\u2029]|\s*$/g, "\n")); + } + var stack = []; return { get : get, @@ -350,18 +356,17 @@ function OutputStream(options) { AST_Node.DEFMETHOD("print", function(stream, force_parens){ var self = this, generator = self._codegen; - stream.push_node(self); - if (force_parens || self.needs_parens(stream)) { - stream.with_parens(function(){ - self.add_comments(stream); - self.add_source_map(stream); - generator(self, stream); - }); - } else { + function doit() { self.add_comments(stream); self.add_source_map(stream); generator(self, stream); } + stream.push_node(self); + if (force_parens || self.needs_parens(stream)) { + stream.with_parens(doit); + } else { + doit(); + } stream.pop_node(); }); @@ -379,15 +384,23 @@ function OutputStream(options) { var start = self.start; if (start && !start._comments_dumped) { start._comments_dumped = true; - var comments = start.comments_before; + var comments = start.comments_before || []; // XXX: ugly fix for https://github.com/mishoo/UglifyJS2/issues/112 - // if this node is `return` or `throw`, we cannot allow comments before - // the returned or thrown value. - if (self instanceof AST_Exit && - self.value && self.value.start.comments_before.length > 0) { - comments = (comments || []).concat(self.value.start.comments_before); - self.value.start.comments_before = []; + // and https://github.com/mishoo/UglifyJS2/issues/372 + if (self instanceof AST_Exit && self.value) { + self.value.walk(new TreeWalker(function(node){ + if (node.start && node.start.comments_before) { + comments = comments.concat(node.start.comments_before); + node.start.comments_before = []; + } + if (node instanceof AST_Function || + node instanceof AST_Array || + node instanceof AST_Object) + { + return true; // don't go inside. + } + })); } if (c.test) { @@ -400,7 +413,7 @@ function OutputStream(options) { }); } comments.forEach(function(c){ - if (c.type == "comment1") { + if (/comment[134]/.test(c.type)) { output.print("//" + c.value + "\n"); output.indent(); } @@ -451,7 +464,7 @@ function OutputStream(options) { || p instanceof AST_Unary // !(foo, bar, baz) || p instanceof AST_Binary // 1 + (2, 3) + 4 ==> 8 || p instanceof AST_VarDef // var a = (1, 2), b = a + a; ==> b == 4 - || p instanceof AST_Dot // (1, {foo:2}).foo ==> 2 + || p instanceof AST_PropAccess // (1, {foo:2}).foo or (1, {foo:2})["foo"] ==> 2 || p instanceof AST_Array // [ 1, (2, 3), 4 ] ==> [ 1, 3, 4 ] || p instanceof AST_ObjectProperty // { foo: (1, 2) }.foo ==> 2 || p instanceof AST_Conditional /* (false, true) ? (a = 10, b = 20) : (c = 30) @@ -476,11 +489,7 @@ function OutputStream(options) { var so = this.operator, sp = PRECEDENCE[so]; if (pp > sp || (pp == sp - && this === p.right - && !(so == po && - (so == "*" || - so == "&&" || - so == "||")))) { + && this === p.right)) { return true; } } @@ -507,8 +516,17 @@ function OutputStream(options) { }); PARENS(AST_Call, function(output){ - var p = output.parent(); - return p instanceof AST_New && p.expression === this; + var p = output.parent(), p1; + if (p instanceof AST_New && p.expression === this) + return true; + + // workaround for Safari bug. + // https://bugs.webkit.org/show_bug.cgi?id=123506 + return this.expression instanceof AST_Function + && p instanceof AST_PropAccess + && p.expression === this + && (p1 = output.parent(1)) instanceof AST_Assign + && p1.left === p; }); PARENS(AST_New, function(output){ @@ -757,7 +775,7 @@ function OutputStream(options) { if (!self.body) return output.force_semicolon(); if (self.body instanceof AST_Do - && output.option("ie_proof")) { + && !output.option("screw_ie8")) { // https://github.com/mishoo/UglifyJS/issues/#issue/57 IE // croaks with "syntax error" on code like this: if (foo) // do ... while(cond); else ... we need block brackets @@ -978,8 +996,12 @@ function OutputStream(options) { DEFPRINT(AST_UnaryPrefix, function(self, output){ var op = self.operator; output.print(op); - if (/^[a-z]/i.test(op)) + if (/^[a-z]/i.test(op) + || (/[+-]$/.test(op) + && self.expression instanceof AST_UnaryPrefix + && /^[+-]/.test(self.expression.operator))) { output.space(); + } self.expression.print(output); }); DEFPRINT(AST_UnaryPostfix, function(self, output){ @@ -990,7 +1012,18 @@ function OutputStream(options) { self.left.print(output); output.space(); output.print(self.operator); - output.space(); + if (self.operator == "<" + && self.right instanceof AST_UnaryPrefix + && self.right.operator == "!" + && self.right.expression instanceof AST_UnaryPrefix + && self.right.expression.operator == "--") { + // space is mandatory to avoid outputting ") && S.newline_before) { + forward(3); + return skip_line_comment("comment4"); + } + } var ch = peek(); if (!ch) return token("eof"); var code = ch.charCodeAt(0); @@ -545,10 +572,10 @@ var UNARY_POSTFIX = makePredicate([ "--", "++" ]); var ASSIGNMENT = makePredicate([ "=", "+=", "-=", "/=", "*=", "%=", ">>=", "<<=", ">>>=", "|=", "^=", "&=" ]); var PRECEDENCE = (function(a, ret){ - for (var i = 0, n = 1; i < a.length; ++i, ++n) { + for (var i = 0; i < a.length; ++i) { var b = a[i]; for (var j = 0; j < b.length; ++j) { - ret[b[j]] = n; + ret[b[j]] = i + 1; } } return ret; @@ -577,13 +604,18 @@ var ATOMIC_START_TOKEN = array_to_hash([ "atom", "num", "string", "regexp", "nam function parse($TEXT, options) { options = defaults(options, { - strict : false, - filename : null, - toplevel : null + strict : false, + filename : null, + toplevel : null, + expression : false, + html5_comments : true, }); var S = { - input : typeof $TEXT == "string" ? tokenizer($TEXT, options.filename) : $TEXT, + input : (typeof $TEXT == "string" + ? tokenizer($TEXT, options.filename, + options.html5_comments) + : $TEXT), token : null, prev : null, peeked : null, @@ -676,12 +708,16 @@ function parse($TEXT, options) { }; }; - var statement = embed_tokens(function() { - var tmp; + function handle_regexp() { if (is("operator", "/") || is("operator", "/=")) { S.peeked = null; S.token = S.input(S.token.value.substr(1)); // force regexp } + }; + + var statement = embed_tokens(function() { + var tmp; + handle_regexp(); switch (S.token.type) { case "string": var dir = S.in_directives, stat = simple_statement(); @@ -746,7 +782,7 @@ function parse($TEXT, options) { return for_(); case "function": - return function_(true); + return function_(AST_Defun); case "if": return if_(); @@ -809,6 +845,18 @@ function parse($TEXT, options) { S.labels.push(label); var stat = statement(); S.labels.pop(); + if (!(stat instanceof AST_IterationStatement)) { + // check for `continue` that refers to this label. + // those should be reported as syntax errors. + // https://github.com/mishoo/UglifyJS2/issues/287 + label.references.forEach(function(ref){ + if (ref instanceof AST_Continue) { + ref = ref.label.start; + croak("Continue label `" + label.name + "` refers to non-IterationStatement.", + ref.line, ref.col, ref.pos); + } + }); + } return new AST_LabeledStatement({ body: stat, label: label }); }; @@ -817,18 +865,22 @@ function parse($TEXT, options) { }; function break_cont(type) { - var label = null; + var label = null, ldef; if (!can_insert_semicolon()) { label = as_symbol(AST_LabelRef, true); } if (label != null) { - if (!find_if(function(l){ return l.name == label.name }, S.labels)) + ldef = find_if(function(l){ return l.name == label.name }, S.labels); + if (!ldef) croak("Undefined label " + label.name); + label.thedef = ldef; } else if (S.in_loop == 0) croak(type.TYPE + " not inside a loop or switch"); semicolon(); - return new type({ label: label }); + var stat = new type({ label: label }); + if (ldef) ldef.references.push(stat); + return stat; }; function for_() { @@ -874,19 +926,12 @@ function parse($TEXT, options) { }); }; - var function_ = function(in_statement, ctor) { - var is_accessor = ctor === AST_Accessor; - var name = (is("name") ? as_symbol(in_statement - ? AST_SymbolDefun - : is_accessor - ? AST_SymbolAccessor - : AST_SymbolLambda) - : is_accessor && (is("string") || is("num")) ? as_atom_node() - : null); + var function_ = function(ctor) { + var in_statement = ctor === AST_Defun; + var name = is("name") ? as_symbol(in_statement ? AST_SymbolDefun : AST_SymbolLambda) : null; if (in_statement && !name) unexpected(); expect("("); - if (!ctor) ctor = in_statement ? AST_Defun : AST_Function; return new ctor({ name: name, argnames: (function(first, a){ @@ -1057,7 +1102,9 @@ function parse($TEXT, options) { var tok = S.token, ret; switch (tok.type) { case "name": - return as_symbol(AST_SymbolRef); + case "keyword": + ret = _make_symbol(AST_SymbolRef); + break; case "num": ret = new AST_Number({ start: tok, end: tok, value: tok.value }); break; @@ -1108,7 +1155,7 @@ function parse($TEXT, options) { } if (is("keyword", "function")) { next(); - var func = function_(false); + var func = function_(AST_Function); func.start = start; func.end = prev(); return subscripts(func, allow_calls); @@ -1156,8 +1203,8 @@ function parse($TEXT, options) { if (name == "get") { a.push(new AST_ObjectGetter({ start : start, - key : name, - value : function_(false, AST_Accessor), + key : as_atom_node(), + value : function_(AST_Accessor), end : prev() })); continue; @@ -1165,8 +1212,8 @@ function parse($TEXT, options) { if (name == "set") { a.push(new AST_ObjectSetter({ start : start, - key : name, - value : function_(false, AST_Accessor), + key : as_atom_node(), + value : function_(AST_Accessor), end : prev() })); continue; @@ -1214,17 +1261,21 @@ function parse($TEXT, options) { } }; + function _make_symbol(type) { + var name = S.token.value; + return new (name == "this" ? AST_This : type)({ + name : String(name), + start : S.token, + end : S.token + }); + }; + function as_symbol(type, noerror) { if (!is("name")) { if (!noerror) croak("Name expected"); return null; } - var name = S.token.value; - var sym = new (name == "this" ? AST_This : type)({ - name : String(S.token.value), - start : S.token, - end : S.token - }); + var sym = _make_symbol(type); next(); return sym; }; @@ -1267,6 +1318,7 @@ function parse($TEXT, options) { var start = S.token; if (is("operator") && UNARY_PREFIX(start.value)) { next(); + handle_regexp(); var ex = make_unary(AST_UnaryPrefix, start.value, maybe_unary(allow_calls)); ex.start = start; ex.end = prev(); @@ -1322,7 +1374,7 @@ function parse($TEXT, options) { condition : expr, consequent : yes, alternative : expression(false, no_in), - end : peek() + end : prev() }); } return expr; @@ -1330,15 +1382,8 @@ function parse($TEXT, options) { function is_assignable(expr) { if (!options.strict) return true; - switch (expr[0]+"") { - case "dot": - case "sub": - case "new": - case "call": - return true; - case "name": - return expr[1] != "this"; - } + if (expr instanceof AST_This) return false; + return (expr instanceof AST_PropAccess || expr instanceof AST_Symbol); }; var maybe_assign = function(no_in) { @@ -1382,6 +1427,10 @@ function parse($TEXT, options) { return ret; }; + if (options.expression) { + return expression(true); + } + return (function(){ var start = S.token; var body = []; diff --git a/lib/scope.js b/lib/scope.js index ea271639..1ce17fa6 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -64,38 +64,41 @@ SymbolDef.prototype = { mangle: function(options) { if (!this.mangled_name && !this.unmangleable(options)) { var s = this.scope; - if (this.orig[0] instanceof AST_SymbolLambda && !options.screw_ie8) + if (!options.screw_ie8 && this.orig[0] instanceof AST_SymbolLambda) s = s.parent_scope; - this.mangled_name = s.next_mangled(options); + this.mangled_name = s.next_mangled(options, this); } } }; -AST_Toplevel.DEFMETHOD("figure_out_scope", function(){ - // This does what ast_add_scope did in UglifyJS v1. - // - // Part of it could be done at parse time, but it would complicate - // the parser (and it's already kinda complex). It's also worth - // having it separated because we might need to call it multiple - // times on the same tree. +AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ + options = defaults(options, { + screw_ie8: false + }); // pass 1: setup scope chaining and handle definitions var self = this; var scope = self.parent_scope = null; - var labels = new Dictionary(); + var defun = null; var nesting = 0; var tw = new TreeWalker(function(node, descend){ + if (options.screw_ie8 && node instanceof AST_Catch) { + var save_scope = scope; + scope = new AST_Scope(node); + scope.init_scope_vars(nesting); + scope.parent_scope = save_scope; + descend(); + scope = save_scope; + return true; + } if (node instanceof AST_Scope) { node.init_scope_vars(nesting); var save_scope = node.parent_scope = scope; - var save_labels = labels; - ++nesting; - scope = node; - labels = new Dictionary(); - descend(); - labels = save_labels; + var save_defun = defun; + defun = scope = node; + ++nesting; descend(); --nesting; scope = save_scope; - --nesting; + defun = save_defun; return true; // don't descend again in TreeWalker } if (node instanceof AST_Directive) { @@ -108,24 +111,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(){ s.uses_with = true; return; } - if (node instanceof AST_LabeledStatement) { - var l = node.label; - if (labels.has(l.name)) - throw new Error(string_template("Label {name} defined twice", l)); - labels.set(l.name, l); - descend(); - labels.del(l.name); - return true; // no descend again - } if (node instanceof AST_Symbol) { node.scope = scope; } - if (node instanceof AST_Label) { - node.thedef = node; - node.init_scope_vars(); - } if (node instanceof AST_SymbolLambda) { - scope.def_function(node); + defun.def_function(node); } else if (node instanceof AST_SymbolDefun) { // Careful here, the scope where this should be defined is @@ -133,31 +123,17 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(){ // scope when we encounter the AST_Defun node (which is // instanceof AST_Scope) but we get to the symbol a bit // later. - (node.scope = scope.parent_scope).def_function(node); + (node.scope = defun.parent_scope).def_function(node); } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { - var def = scope.def_variable(node); + var def = defun.def_variable(node); def.constant = node instanceof AST_SymbolConst; def.init = tw.parent().value; } else if (node instanceof AST_SymbolCatch) { - // XXX: this is wrong according to ECMA-262 (12.4). the - // `catch` argument name should be visible only inside the - // catch block. For a quick fix AST_Catch should inherit - // from AST_Scope. Keeping it this way because of IE, - // which doesn't obey the standard. (it introduces the - // identifier in the enclosing scope) - scope.def_variable(node); - } - if (node instanceof AST_LabelRef) { - var sym = labels.get(node.name); - if (!sym) throw new Error(string_template("Undefined label {name} [{line},{col}]", { - name: node.name, - line: node.start.line, - col: node.start.col - })); - node.thedef = sym; + (options.screw_ie8 ? scope : defun) + .def_variable(node); } }); self.walk(tw); @@ -173,10 +149,6 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(){ func = prev_func; return true; } - if (node instanceof AST_LabelRef) { - node.reference(); - return true; - } if (node instanceof AST_SymbolRef) { var name = node.name; var sym = node.scope.find_variable(name); @@ -187,6 +159,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(){ } else { g = new SymbolDef(self, globals.size(), node); g.undeclared = true; + g.global = true; globals.set(name, g); } node.thedef = g; @@ -194,7 +167,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(){ for (var s = node.scope; s && !s.uses_eval; s = s.parent_scope) s.uses_eval = true; } - if (name == "arguments") { + if (func && name == "arguments") { func.uses_arguments = true; } } else { @@ -240,14 +213,6 @@ AST_SymbolRef.DEFMETHOD("reference", function() { this.frame = this.scope.nesting - def.scope.nesting; }); -AST_Label.DEFMETHOD("init_scope_vars", function(){ - this.references = []; -}); - -AST_LabelRef.DEFMETHOD("reference", function(){ - this.thedef.references.push(this); -}); - AST_Scope.DEFMETHOD("find_variable", function(name){ if (name instanceof AST_Symbol) name = name.name; return this.variables.get(name) @@ -281,6 +246,11 @@ AST_Scope.DEFMETHOD("next_mangled", function(options){ out: while (true) { var m = base54(++this.cname); if (!is_identifier(m)) continue; // skip over "do" + + // https://github.com/mishoo/UglifyJS2/issues/242 -- do not + // shadow a name excepted from mangling. + if (options.except.indexOf(m) >= 0) continue; + // we must ensure that the mangled name does not shadow a name // from some parent scope that is referenced in this or in // inner scopes. @@ -293,6 +263,19 @@ AST_Scope.DEFMETHOD("next_mangled", function(options){ } }); +AST_Function.DEFMETHOD("next_mangled", function(options, def){ + // #179, #326 + // in Safari strict mode, something like (function x(x){...}) is a syntax error; + // a function expression's argument cannot shadow the function expression's name + + var tricky_def = def.orig[0] instanceof AST_SymbolFunarg && this.name && this.name.definition(); + while (true) { + var name = AST_Lambda.prototype.next_mangled.call(this, options, def); + if (!(tricky_def && tricky_def.mangled_name == name)) + return name; + } +}); + AST_Scope.DEFMETHOD("references", function(sym){ if (sym instanceof AST_Symbol) sym = sym.definition(); return this.enclosed.indexOf(sym) < 0 ? null : sym; @@ -382,6 +365,10 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ node.mangled_name = name; return true; } + if (options.screw_ie8 && node instanceof AST_SymbolCatch) { + to_mangle.push(node.definition()); + return; + } }); this.walk(tw); to_mangle.forEach(function(def){ def.mangle(options) }); diff --git a/lib/sourcemap.js b/lib/sourcemap.js index 34299081..663ef12e 100644 --- a/lib/sourcemap.js +++ b/lib/sourcemap.js @@ -49,6 +49,9 @@ function SourceMap(options) { file : null, root : null, orig : null, + + orig_line_diff : 0, + dest_line_diff : 0, }); var generator = new MOZ_SourceMap.SourceMapGenerator({ file : options.file, @@ -61,14 +64,17 @@ function SourceMap(options) { line: orig_line, column: orig_col }); + if (info.source === null) { + return; + } source = info.source; orig_line = info.line; orig_col = info.column; name = info.name; } generator.addMapping({ - generated : { line: gen_line, column: gen_col }, - original : { line: orig_line, column: orig_col }, + generated : { line: gen_line + options.dest_line_diff, column: gen_col }, + original : { line: orig_line + options.orig_line_diff, column: orig_col }, source : source, name : name }); diff --git a/lib/transform.js b/lib/transform.js index 8b4fd9fd..c3c34f58 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -44,7 +44,6 @@ "use strict"; // Tree transformer helpers. -// XXX: eventually I should refactor the compressor to use this infrastructure. function TreeTransformer(before, after) { TreeWalker.call(this); @@ -160,6 +159,7 @@ TreeTransformer.prototype = new TreeWalker; }); _(AST_VarDef, function(self, tw){ + self.name = self.name.transform(tw); if (self.value) self.value = self.value.transform(tw); }); diff --git a/lib/utils.js b/lib/utils.js index c95b9824..7c6a1563 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -82,16 +82,23 @@ function repeat_string(str, i) { }; function DefaultsError(msg, defs) { + Error.call(this, msg); this.msg = msg; this.defs = defs; }; +DefaultsError.prototype = Object.create(Error.prototype); +DefaultsError.prototype.constructor = DefaultsError; + +DefaultsError.croak = function(msg, defs) { + throw new DefaultsError(msg, defs); +}; function defaults(args, defs, croak) { if (args === true) args = {}; var ret = args || {}; if (croak) for (var i in ret) if (ret.hasOwnProperty(i) && !defs.hasOwnProperty(i)) - throw new DefaultsError("`" + i + "` is not a supported option", defs); + DefaultsError.croak("`" + i + "` is not a supported option", defs); for (var i in defs) if (defs.hasOwnProperty(i)) { ret[i] = (args && args.hasOwnProperty(i)) ? args[i] : defs[i]; } @@ -245,6 +252,13 @@ function makePredicate(words) { return new Function("str", f); }; +function all(array, predicate) { + for (var i = array.length; --i >= 0;) + if (!predicate(array[i])) + return false; + return true; +}; + function Dictionary() { this._values = Object.create(null); this._size = 0; diff --git a/package.json b/package.json index 48e36cb3..031e3395 100644 --- a/package.json +++ b/package.json @@ -3,21 +3,25 @@ "description": "JavaScript parser, mangler/compressor and beautifier toolkit", "homepage": "http://lisperator.net/uglifyjs", "main": "tools/node.js", - "version": "2.2.5", + "version": "2.4.13", "engines": { "node" : ">=0.4.0" }, "maintainers": [{ "name": "Mihai Bazon", "email": "mihai.bazon@gmail.com", "web": "http://lisperator.net/" }], - "repositories": [{ + "repository": { "type": "git", "url": "https://github.com/mishoo/UglifyJS2.git" - }], + }, "dependencies": { "async" : "~0.2.6", - "source-map" : "~0.1.7", - "optimist" : "~0.3.5" + "source-map" : "~0.1.33", + "optimist" : "~0.3.5", + "uglify-to-browserify": "~1.0.0" + }, + "browserify": { + "transform": [ "uglify-to-browserify" ] }, "bin": { "uglifyjs" : "bin/uglifyjs" diff --git a/test/compress/arrays.js b/test/compress/arrays.js index 10fe6eb5..e636347f 100644 --- a/test/compress/arrays.js +++ b/test/compress/arrays.js @@ -1,12 +1,74 @@ holes_and_undefined: { input: { + w = [1,,]; x = [1, 2, undefined]; y = [1, , 2, ]; z = [1, undefined, 3]; } expect: { + w=[1,,]; x=[1,2,void 0]; y=[1,,2]; z=[1,void 0,3]; } } + +constant_join: { + options = { + unsafe : true, + evaluate : true + }; + input: { + var a = [ "foo", "bar", "baz" ].join(""); + var a1 = [ "foo", "bar", "baz" ].join(); + var b = [ "foo", 1, 2, 3, "bar" ].join(""); + var c = [ boo(), "foo", 1, 2, 3, "bar", bar() ].join(""); + var c1 = [ boo(), bar(), "foo", 1, 2, 3, "bar", bar() ].join(""); + var c2 = [ 1, 2, "foo", "bar", baz() ].join(""); + var d = [ "foo", 1 + 2 + "bar", "baz" ].join("-"); + var e = [].join(foo + bar); + var f = [].join(""); + var g = [].join("foo"); + } + expect: { + var a = "foobarbaz"; + var a1 = "foo,bar,baz"; + var b = "foo123bar"; + var c = boo() + "foo123bar" + bar(); + var c1 = "" + boo() + bar() + "foo123bar" + bar(); + var c2 = "12foobar" + baz(); + var d = "foo-3bar-baz"; + var e = [].join(foo + bar); + var f = ""; + var g = ""; + } +} + +constant_join_2: { + options = { + unsafe : true, + evaluate : true + }; + input: { + var a = [ "foo", "bar", boo(), "baz", "x", "y" ].join(""); + var b = [ "foo", "bar", boo(), "baz", "x", "y" ].join("-"); + var c = [ "foo", "bar", boo(), "baz", "x", "y" ].join("really-long-separator"); + var d = [ "foo", "bar", boo(), + [ "foo", 1, 2, 3, "bar" ].join("+"), + "baz", "x", "y" ].join("-"); + var e = [ "foo", "bar", boo(), + [ "foo", 1, 2, 3, "bar" ].join("+"), + "baz", "x", "y" ].join("really-long-separator"); + var f = [ "str", "str" + variable, "foo", "bar", "moo" + foo ].join(""); + } + expect: { + var a = "foobar" + boo() + "bazxy"; + var b = [ "foo-bar", boo(), "baz-x-y" ].join("-"); + var c = [ "foo", "bar", boo(), "baz", "x", "y" ].join("really-long-separator"); + var d = [ "foo-bar", boo(), "foo+1+2+3+bar-baz-x-y" ].join("-"); + var e = [ "foo", "bar", boo(), + "foo+1+2+3+bar", + "baz", "x", "y" ].join("really-long-separator"); + var f = "strstr" + variable + "foobarmoo" + foo; + } +} diff --git a/test/compress/concat-strings.js b/test/compress/concat-strings.js new file mode 100644 index 00000000..79192987 --- /dev/null +++ b/test/compress/concat-strings.js @@ -0,0 +1,22 @@ +concat_1: { + options = { + evaluate: true + }; + input: { + var a = "foo" + "bar" + x() + "moo" + "foo" + y() + "x" + "y" + "z" + q(); + var b = "foo" + 1 + x() + 2 + "boo"; + var c = 1 + x() + 2 + "boo"; + + // this CAN'T safely be shortened to 1 + x() + "5boo" + var d = 1 + x() + 2 + 3 + "boo"; + + var e = 1 + x() + 2 + "X" + 3 + "boo"; + } + expect: { + var a = "foobar" + x() + "moofoo" + y() + "xyz" + q(); + var b = "foo1" + x() + "2boo"; + var c = 1 + x() + 2 + "boo"; + var d = 1 + x() + 2 + 3 + "boo"; + var e = 1 + x() + 2 + "X3boo"; + } +} diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index dc2bb671..213b246b 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -141,3 +141,94 @@ ifs_6: { x = foo || bar || baz || boo ? 20 : 10; } } + +cond_1: { + options = { + conditionals: true + }; + input: { + if (some_condition()) { + do_something(x); + } else { + do_something(y); + } + } + expect: { + do_something(some_condition() ? x : y); + } +} + +cond_2: { + options = { + conditionals: true + }; + input: { + if (some_condition()) { + x = new FooBar(1); + } else { + x = new FooBar(2); + } + } + expect: { + x = new FooBar(some_condition() ? 1 : 2); + } +} + +cond_3: { + options = { + conditionals: true + }; + input: { + if (some_condition()) { + new FooBar(1); + } else { + FooBar(2); + } + } + expect: { + some_condition() ? new FooBar(1) : FooBar(2); + } +} + +cond_4: { + options = { + conditionals: true + }; + input: { + if (some_condition()) { + do_something(); + } else { + do_something(); + } + } + expect: { + some_condition(), do_something(); + } +} + +cond_5: { + options = { + conditionals: true + }; + input: { + if (some_condition()) { + if (some_other_condition()) { + do_something(); + } else { + alternate(); + } + } else { + alternate(); + } + + if (some_condition()) { + if (some_other_condition()) { + do_something(); + } + } + } + expect: { + some_condition() && some_other_condition() ? do_something() : alternate(); + some_condition() && some_other_condition() && do_something(); + } +} diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index bf5cd296..89bf0088 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -95,3 +95,71 @@ unused_circular_references_3: { } } } + +unused_keep_setter_arg: { + options = { unused: true }; + input: { + var x = { + _foo: null, + set foo(val) { + }, + get foo() { + return this._foo; + } + } + } + expect: { + var x = { + _foo: null, + set foo(val) { + }, + get foo() { + return this._foo; + } + } + } +} + +unused_var_in_catch: { + options = { unused: true }; + input: { + function foo() { + try { + foo(); + } catch(ex) { + var x = 10; + } + } + } + expect: { + function foo() { + try { + foo(); + } catch(ex) {} + } + } +} + +used_var_in_catch: { + options = { unused: true }; + input: { + function foo() { + try { + foo(); + } catch(ex) { + var x = 10; + } + return x; + } + } + expect: { + function foo() { + try { + foo(); + } catch(ex) { + var x = 10; + } + return x; + } + } +} diff --git a/test/compress/issue-105.js b/test/compress/issue-105.js index 349d732d..ca17adbf 100644 --- a/test/compress/issue-105.js +++ b/test/compress/issue-105.js @@ -1,7 +1,6 @@ typeof_eq_undefined: { options = { - comparisons: true, - unsafe: false + comparisons: true }; input: { a = typeof b.c != "undefined" } expect: { a = "undefined" != typeof b.c } @@ -13,5 +12,14 @@ typeof_eq_undefined_unsafe: { unsafe: true }; input: { a = typeof b.c != "undefined" } - expect: { a = b.c !== void 0 } + expect: { a = void 0 !== b.c } +} + +typeof_eq_undefined_unsafe2: { + options = { + comparisons: true, + unsafe: true + }; + input: { a = "undefined" != typeof b.c } + expect: { a = void 0 !== b.c } } diff --git a/test/compress/issue-126.js b/test/compress/issue-126.js new file mode 100644 index 00000000..7a597b87 --- /dev/null +++ b/test/compress/issue-126.js @@ -0,0 +1,24 @@ +concatenate_rhs_strings: { + options = { + evaluate: true, + unsafe: true, + } + input: { + foo(bar() + 123 + "Hello" + "World"); + foo(bar() + (123 + "Hello") + "World"); + foo((bar() + 123) + "Hello" + "World"); + foo(bar() + 123 + "Hello" + "World" + ("Foo" + "Bar")); + foo("Foo" + "Bar" + bar() + 123 + "Hello" + "World" + ("Foo" + "Bar")); + foo("Hello" + bar() + 123 + "World"); + foo(bar() + 'Foo' + (10 + parseInt('10'))); + } + expect: { + foo(bar() + 123 + "HelloWorld"); + foo(bar() + "123HelloWorld"); + foo((bar() + 123) + "HelloWorld"); + foo(bar() + 123 + "HelloWorldFooBar"); + foo("FooBar" + bar() + "123HelloWorldFooBar"); + foo("Hello" + bar() + "123World"); + foo(bar() + 'Foo' + (10 + parseInt('10'))); + } +} diff --git a/test/compress/issue-143.js b/test/compress/issue-143.js new file mode 100644 index 00000000..4c79790b --- /dev/null +++ b/test/compress/issue-143.js @@ -0,0 +1,48 @@ +/** + * There was an incorrect sort behaviour documented in issue #143: + * (x = f(…)) <= x → x >= (x = f(…)) + * + * For example, let the equation be: + * (a = parseInt('100')) <= a + * + * If a was an integer and has the value of 99, + * (a = parseInt('100')) <= a → 100 <= 100 → true + * + * When transformed incorrectly: + * a >= (a = parseInt('100')) → 99 >= 100 → false + */ + +tranformation_sort_order_equal: { + options = { + comparisons: true, + }; + + input: { (a = parseInt('100')) == a } + expect: { (a = parseInt('100')) == a } +} + +tranformation_sort_order_unequal: { + options = { + comparisons: true, + }; + + input: { (a = parseInt('100')) != a } + expect: { (a = parseInt('100')) != a } +} + +tranformation_sort_order_lesser_or_equal: { + options = { + comparisons: true, + }; + + input: { (a = parseInt('100')) <= a } + expect: { (a = parseInt('100')) <= a } +} +tranformation_sort_order_greater_or_equal: { + options = { + comparisons: true, + }; + + input: { (a = parseInt('100')) >= a } + expect: { (a = parseInt('100')) >= a } +} \ No newline at end of file diff --git a/test/compress/issue-267.js b/test/compress/issue-267.js new file mode 100644 index 00000000..7233d9f1 --- /dev/null +++ b/test/compress/issue-267.js @@ -0,0 +1,11 @@ +issue_267: { + options = { comparisons: true }; + input: { + x = a % b / b * c * 2; + x = a % b * 2 + } + expect: { + x = a % b / b * c * 2; + x = a % b * 2; + } +} diff --git a/test/compress/issue-269.js b/test/compress/issue-269.js new file mode 100644 index 00000000..1d41dea6 --- /dev/null +++ b/test/compress/issue-269.js @@ -0,0 +1,66 @@ +issue_269_1: { + options = {unsafe: true}; + input: { + f( + String(x), + Number(x), + Boolean(x), + + String(), + Number(), + Boolean() + ); + } + expect: { + f( + x + '', +x, !!x, + '', 0, false + ); + } +} + +issue_269_dangers: { + options = {unsafe: true}; + input: { + f( + String(x, x), + Number(x, x), + Boolean(x, x) + ); + } + expect: { + f(String(x, x), Number(x, x), Boolean(x, x)); + } +} + +issue_269_in_scope: { + options = {unsafe: true}; + input: { + var String, Number, Boolean; + f( + String(x), + Number(x, x), + Boolean(x) + ); + } + expect: { + var String, Number, Boolean; + f(String(x), Number(x, x), Boolean(x)); + } +} + +strings_concat: { + options = {unsafe: true}; + input: { + f( + String(x + 'str'), + String('str' + x) + ); + } + expect: { + f( + x + 'str', + 'str' + x + ); + } +} diff --git a/test/compress/negate-iife.js b/test/compress/negate-iife.js new file mode 100644 index 00000000..89c3f064 --- /dev/null +++ b/test/compress/negate-iife.js @@ -0,0 +1,76 @@ +negate_iife_1: { + options = { + negate_iife: true + }; + input: { + (function(){ stuff() })(); + } + expect: { + !function(){ stuff() }(); + } +} + +negate_iife_2: { + options = { + negate_iife: true + }; + input: { + (function(){ return {} })().x = 10; // should not transform this one + } + expect: { + (function(){ return {} })().x = 10; + } +} + +negate_iife_3: { + options = { + negate_iife: true, + }; + input: { + (function(){ return true })() ? console.log(true) : console.log(false); + } + expect: { + !function(){ return true }() ? console.log(false) : console.log(true); + } +} + +negate_iife_3: { + options = { + negate_iife: true, + sequences: true + }; + input: { + (function(){ return true })() ? console.log(true) : console.log(false); + (function(){ + console.log("something"); + })(); + } + expect: { + !function(){ return true }() ? console.log(false) : console.log(true), function(){ + console.log("something"); + }(); + } +} + +negate_iife_4: { + options = { + negate_iife: true, + sequences: true, + conditionals: true, + }; + input: { + if ((function(){ return true })()) { + foo(true); + } else { + bar(false); + } + (function(){ + console.log("something"); + })(); + } + expect: { + !function(){ return true }() ? bar(false) : foo(true), function(){ + console.log("something"); + }(); + } +} diff --git a/test/compress/properties.js b/test/compress/properties.js index 9b066ec9..736d9d88 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -17,10 +17,18 @@ dot_properties: { input: { a["foo"] = "bar"; a["if"] = "if"; + a["*"] = "asterisk"; + a["\u0EB3"] = "unicode"; + a[""] = "whitespace"; + a["1_1"] = "foo"; } expect: { a.foo = "bar"; a["if"] = "if"; + a["*"] = "asterisk"; + a["\u0EB3"] = "unicode"; + a[""] = "whitespace"; + a["1_1"] = "foo"; } } @@ -32,9 +40,15 @@ dot_properties_es5: { input: { a["foo"] = "bar"; a["if"] = "if"; + a["*"] = "asterisk"; + a["\u0EB3"] = "unicode"; + a[""] = "whitespace"; } expect: { a.foo = "bar"; a.if = "if"; + a["*"] = "asterisk"; + a["\u0EB3"] = "unicode"; + a[""] = "whitespace"; } } diff --git a/test/compress/sequences.js b/test/compress/sequences.js index 6f63ace4..46695714 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -101,10 +101,12 @@ lift_sequences_1: { lift_sequences_2: { options = { sequences: true, evaluate: true }; input: { - q = 1 + (foo(), bar(), 5) + 7 * (5 / (3 - (a(), (QW=ER), c(), 2))) - (x(), y(), 5); + foo.x = (foo = {}, 10); + bar = (bar = {}, 10); } expect: { - foo(), bar(), a(), QW = ER, c(), x(), y(), q = 36 + foo.x = (foo = {}, 10), + bar = {}, bar = 10; } } diff --git a/test/run-tests.js b/test/run-tests.js index 0568c6a7..f8e88d48 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -7,8 +7,15 @@ var assert = require("assert"); var sys = require("util"); var tests_dir = path.dirname(module.filename); +var failures = 0; +var failed_files = {}; run_compress_tests(); +if (failures) { + sys.error("\n!!! Failed " + failures + " test cases."); + sys.error("!!! " + Object.keys(failed_files).join(", ")); + process.exit(1); +} /* -----[ utils ]----- */ @@ -83,6 +90,8 @@ function run_compress_tests() { output: output, expected: expect }); + failures++; + failed_files[file] = 1; } } var tests = parse_test(path.resolve(dir, file)); diff --git a/tools/node.js b/tools/node.js index 0bbdf6c6..52405f3a 100644 --- a/tools/node.js +++ b/tools/node.js @@ -51,6 +51,7 @@ for (var i in UglifyJS) { exports.minify = function(files, options) { options = UglifyJS.defaults(options, { + spidermonkey : false, outSourceMap : null, sourceMapURL : null, sourceRoot : null, @@ -62,25 +63,33 @@ exports.minify = function(files, options) { prefix : null, compress : {} }); - if (typeof files == "string") - files = [ files ]; + UglifyJS.base54.reset(); // 1. parse - var toplevel = null; - files.forEach(function(file){ - var code = options.fromString - ? file - : fs.readFileSync(file, "utf8"); - - if (options.prefix !== null) { - file = file.replace(/^\/+/, "").split(/\/+/).slice(options.prefix).join("/"); - } - - toplevel = UglifyJS.parse(code, { - filename: options.fromString ? "?" : file, - toplevel: toplevel + var toplevel = null, + sourcesContent = {}; + + if (options.spidermonkey) { + toplevel = UglifyJS.AST_Node.from_mozilla_ast(files); + } else { + if (typeof files == "string") + files = [ files ]; + files.forEach(function(file){ + var code = options.fromString + ? file + : fs.readFileSync(file, "utf8"); + + if (options.prefix !== null) { + file = file.replace(/^\/+/, "").split(/\/+/).slice(options.prefix).join("/"); + } + + sourcesContent[file] = code; + toplevel = UglifyJS.parse(code, { + filename: options.fromString ? "?" : file, + toplevel: toplevel + }); }); - }); + } // 2. compress if (options.compress) { @@ -110,12 +119,25 @@ exports.minify = function(files, options) { orig: inMap, root: options.sourceRoot }); + if (options.sourceMapIncludeSources) { + for (var file in sourcesContent) { + if (sourcesContent.hasOwnProperty(file)) { + output.source_map.get().setSourceContent(file, sourcesContent[file]); + } + } + } + } if (options.output) { UglifyJS.merge(output, options.output); } var stream = UglifyJS.OutputStream(output); toplevel.print(stream); + + if(options.outSourceMap){ + stream += "\n//# sourceMappingURL=" + options.outSourceMap; + } + return { code : stream + (output.source_map ? "\n//@ sourceMappingURL=" + (options.sourceMapURL || options.outSourceMap) : ""), map : output.source_map + ""