webpack
Version:
Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.
53,879 lines • 1.75 MB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author sheo13666q @sheo13666q
*/
/*
Ported from terser (https://github.com/terser/terser), under its license:
Copyright 2012-2018 (c) Mihai Bazon <mihai.bazon@gmail.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above
copyright notice, this list of conditions and the following
disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
*/
"use strict";
const {
DECLARES_CATCH,
DECLARES_CLASS,
DECLARES_DEFUN,
DECLARES_DEF_CLASS,
DECLARES_FUNARG,
DECLARES_LAMBDA,
EXPORT_KEEP_NAME,
analyzeCompressorScope,
analyzeMinifierScope,
analyzeMinifierTree,
createCompressorDefinition,
createCompressorScope,
isMinifierVariable
} = require("./ScopeAnalyzer");
/* eslint-disable camelcase, new-cap -- terser's own API, which this speaks */
// cspell:ignore Mihai, Bazon, DEFNODE, endpos, endcol, sequencesize, lvalues, binop, loopcontrol, symbolref, funs, jridgewell, DEFMETHOD, Defun, defun, defuns, fnames, funarg, classnames, mangleable, unmangleable, NOINLINE, privatename, argnames, thedef, argname, bcatch, bfinally, MANGLEPROP, nlb, punc, Funarg, propmangle, preferrably, nondeferred, fargs, domprops, noin, Noin, DEFPRINT, defprint, dgimsuyv, nokeyword, etnriaoslucdfmhpgvybkwjxqz, ETNRIAOSLUCDFMHPGVYBKWJXQZ
/** @typedef {{ ast: Ast, [key: string]: EXPECTED_ANY }} MinifierModules terser's own modules */
/**
* terser's modules as the phases read them: what `createModules` builds, and
* what each phase installs, read only by the phases after it.
* @internal
* @typedef {object} TerserModules
* @property {TerserAst} ast the node kinds, walkers and helpers
* @property {ReturnType<typeof createParseTables> & { parseTree: <T>(text: string, givenOptions: ParseOptions, readTokens: (tree: SyntaxTree, program: number, tokenAt: (pos: number, ending: boolean, forward?: number) => number | undefined, table: TokenTable) => T) => { tree: SyntaxTree, program: number, tables: TokenTables, read: T, module: boolean } }} parse the parser and its tables, and the parse a tree printed directly is read with
* @property {{ base54: ReturnType<typeof createFrequency> }} scope the mangler's source of names
* @property {ReturnType<typeof createCompressorFactory>} compress the compressor's factory and methods
* @property {{ OutputStream: (given?: Partial<FormatSettings>) => OutputStream }} output terser's `OutputStream`, the format options as given
* @property {{ domprops: string[] }} domprops the DOM's property names
* @property {new (options: FormatSettings, readonly: boolean, nodeAccess: NodeAccess) => OutputStream} MinifiedOutput webpack's stream for minified output
* @property {HoistHooks} webpackHoist what later phases add to splitting objects
* @property {UnusedHooks} webpackUnused what later phases add to `drop_unused`
* @property {CollapseHooks} webpackCollapse what later phases add to `collapse_vars`
* @property {InlineHooks} webpackInline what later phases add to inlining
* @property {(files: MinifySources, options?: MinifyOptions, fsModule?: typeof import("fs")) => Promise<MinifyOutput>} minify terser's `minify`
* @property {(files: MinifySources, options?: MinifyOptions) => MinifyOutput} minifySync terser's `minify_sync`
* @property {Corrections} corrections the switch the corrections read, once the correct phase installs it
* @property {Improvements} improvements the switch the improvements read, once the improve phase installs it
* @property {(compressor: Walker) => string[] | undefined} reservedAround the names reserved around what a walker visits
* @property {(lambda: Node, reserved: string[]) => boolean} namesInOwnContext whether a function's names stay apart from the reserved ones
* @property {(stream: OutputStream<PrintNode | number>, text: string, print: () => void) => void} printWithLineBreaksOf prints with the line breaks a text holds
* @property {(stream: OutputStream<PrintNode | number>, value: string) => string | undefined} lineBreakTemplateOf a string written as a template where it saves line break escapes
* @property {((tree: Node) => LetCounts | null) | undefined} writePendingLets writes the `const` declarations held back as `let`
* @property {TerserCompressHelpers["common"]} common terser's `compress/common.js`
* @property {TerserCompressHelpers["inference"]} inference the functions of terser's `compress/inference.js`
* @property {TerserCompressHelpers["flags"]} flags terser's `compress/compressor-flags.js`
* @property {TerserCompressHelpers["utils"]} utils terser's `utils/index.js`
* @property {NativeObjects} nativeObjects what the compressor knows of the builtins
* @property {PrintKinds} kindOf terser's node classes the printer asks of a node
* @property {(member: PrintProperty | PrintPropertyDefinition, tables?: TokenTables) => string | null} mapNameOf the name a member's source mapping carries
* @property {(node: PrintNode, output: OutputStream, forceParens?: boolean) => void} printEstree prints an ESTree node
* @property {EstreeGenerators} estreeCodegen per ESTree type, what prints a node of it
* @property {EstreeParensTests} estreeNeedsParens per ESTree type, whether a node of it is parenthesized
* @property {(given?: FormatSettings) => OutputStream} createStringStream a stream printing to a string
* @property {(node: Node, given?: FormatSettings, tables?: TokenTables) => string} printToString terser's `print_to_string`
* @property {(program: PrintProgram, output: OutputStream, releases: boolean | undefined) => void} printProgram prints a program
* @property {(program: PrintProgram, output: OutputStream, options: MangleOptions) => MangleRecording} printRecording prints a program, recording where names go
* @property {(output: OutputStream, text: string, recorded: MangleRecording) => string} patchRecording writes the names given into a recorded print
* @property {(node: PrintFunction, output: OutputStream) => void} printFunctionBody prints a function's body
* @property {(node: PrintNode) => string} printEstreeToString an ESTree node printed to a string
* @property {(identifiers: IdentifierSource) => identifiers is Required<IdentifierSource>} countsFrequency whether a source orders its names by the characters counted
* @property {(printNode: PrintNode, options: MangleOptions, letCounts?: LetCounts | null) => boolean} computeEstreeCharFrequency counts the characters a tree prints
* @property {(tree: SyntaxTree, program: number, at: (pos: number, ending: boolean, forward?: number) => number | undefined, table: TokenTable) => TreeTokens} readTreeTokens the tokens of a parsed tree
* @property {{ enabled: boolean }} treePrinting the switch printing a parsed tree directly reads
* @property {(parsed: ParsedTree, stream: OutputStream, text: string, names?: TreeNames | null) => void} printTree prints a parsed tree
* @property {(parsed: ParsedTree, stream: OutputStream, text: string, options: MangleOptions, mayNamePrivate: boolean) => TreeNames} mangleTree mangles a parsed tree
* @property {(program: PrintProgram, options: MangleOptions, releases?: boolean, shorthand?: boolean) => MinifierScopeAnalysis} analyzeManglingScopes the scopes the mangler names
* @property {(definition: MinifierVariable, options: MangleOptions, keptDefinitions: Set<MinifierVariable> | null, keptClasses?: Set<MinifierVariable> | null) => boolean} isUnmangleable whether a definition's name is kept
* @property {(analysis: MinifierScopeAnalysis, options: MangleOptions) => void} mangleManglingScopes names the scopes' definitions
* @property {(tree: PrintNode, options: MangleOptions, properties: PropertyNames | null, hasPrivateNames: boolean, setMapNames: (PrintProperty | PrintPropertyDefinition)[] | null, kept?: Set<string> | null) => void} renameMangledEstree writes the names given into a tree
* @property {(analysis: MinifierScopeAnalysis, declaredBy?: (node: PrintNode) => string[]) => Set<string> | null} privateNamesReadByEval the private names `eval` may read
* @property {(program: Node, given: ScopeMangleOptions) => PrintFunction | undefined} mangledFunctionOf a `new Function` body's function, mangled
*/
/**
* The helpers `createCompressHelpers` ports, as the phases read them: the
* public `CompressHelpers` with each bag's members typed.
* @internal
* @typedef {object} TerserCompressHelpers
* @property {CompressCommon} common terser's `compress/common.js`
* @property {CompressInference} inference the functions of terser's `compress/inference.js`
* @property {typeof COMPRESSOR_FLAGS & CompressFlagHelpers} flags terser's `compress/compressor-flags.js`
* @property {CompressUtils} utils terser's `utils/index.js`
*/
/**
* terser's `compress/common.js`, ported.
* @internal
* @typedef {object} CompressCommon
* @property {(thing: Node | null) => Node[]} as_statement_array the statements a statement stands for
* @property {(compressor: Compressor, first: Node, second: Node) => Node} best_of the smaller of two equivalent expressions where they stand
* @property {(first: Node, second: Node) => Node} best_of_expression the smaller of two equivalent expressions
* @property {(node: Node) => boolean} can_be_evicted_from_block whether a statement can leave its block without changing scope
* @property {(key: Node | string | undefined) => Node | string | number | boolean | RegExpValue | null | undefined} get_simple_key a property key as a value, where it is a constant
* @property {(loop: Node, parent?: Node) => boolean} has_break_or_continue whether a `break` or `continue` in a loop's body targets it
* @property {Set<string>} identifier_atom the names `Infinity`, `NaN` and `undefined`
* @property {(thing: Node | null) => boolean} is_empty whether a statement does nothing
* @property {(node: Node) => boolean} is_func_expr whether a node is a function or arrow expression
* @property {(node: Node) => boolean} is_identifier_atom whether a node is `Infinity`, `NaN` or `undefined`
* @property {(node: Node) => boolean} is_iife_call whether a node calls a function expression, or such a call's result
* @property {(scopeNode: Node, definitions: Definition[]) => boolean} is_reachable whether a closure in a scope references one of the definitions
* @property {(walker: ParentStack, definition: Definition) => boolean} is_recursive_ref whether a reference names a function or class it is inside
* @property {(reference: Node, test: NodeTest) => boolean | undefined} is_ref_of whether a node references a name one of whose declarations passes a test
* @property {(parent: Node, orig: Node, value: Node) => Node} maintain_this_binding what replaces a node, keeping the `this` a call reads
* @property {(self: Node) => Node} make_empty_function `function(){}`
* @property {(value: ConstantValue, orig: Node) => Node} make_node_from_constant the node for a value
* @property {(orig: Node | undefined, expressions: Node[]) => Node} make_sequence the expressions as one expression
* @property {(array: Node[], node: Node) => Node[]} merge_sequence a list with an expression's own expressions joined
* @property {(object: Node, property: Node | string | undefined) => Node | null | undefined} read_property what an array or object literal holds under a key, null for a hole
* @property {(fn: Node, compressor: Compressor) => boolean | number | undefined} retain_top_func truthy where `top_retain` keeps a function
*/
/**
* A value a constant expression computes, which `make_node_from_constant`
* writes back as a node: any JavaScript value.
* @internal
* @typedef {EvaluatedValue} ConstantValue
*/
/**
* A value constant evaluation computes: a primitive, a regular expression,
* an array or object of such values, or a function standing in for one.
* @internal
* @typedef {string | number | bigint | boolean | symbol | null | undefined | RegExp | EvaluatedArray | { [key: string]: EvaluatedValue } | ((...args: EvaluatedValue[]) => EvaluatedValue)} EvaluatedValue
*/
/**
* An array of evaluated values, apart so that the JSDoc alias can recurse.
* @internal
* @typedef {EvaluatedValue[]} EvaluatedArray
*/
/**
* The function `unsafe` evaluation reads a function expression as.
* @internal
* @typedef {(() => undefined) & { node: Node }} FunctionStandIn
*/
/**
* A built-in function constant folding calls, as it reads one.
* @internal
* @typedef {(...args: EvaluatedArray) => EvaluatedValue} BuiltInFunction
*/
/**
* A global or a prototype, as constant folding reads its functions by name.
* @internal
* @typedef {{ [key: string]: BuiltInFunction }} BuiltInTable
*/
/**
* The functions of terser's `compress/inference.js`, ported.
* @internal
* @typedef {object} CompressInference
* @property {(thing: Node | null | undefined) => Node | null | false | undefined} aborts the statement a statement ends with that aborts, or something falsy
* @property {Set<string>} bitwise_binop the bitwise operators
* @property {(node: Node | undefined, parent: Node | undefined) => Node | undefined} is_lhs what the parent writes to, where it writes the node
* @property {(compressor: Compressor, walker: ParentStack, node: Node, value: Node | false | null | undefined, level: number, immutable?: boolean) => Node | boolean | undefined} is_modified truthy where a node may be written to or mutated
* @property {(node: Node, compressor: Compressor) => boolean | number} is_nullish whether a node is `null` or `undefined`
* @property {(node: Node, compressor: Compressor) => boolean | number} is_nullish_shortcircuited whether an optional chain in a node short-circuits
* @property {(node: Node) => boolean} is_undeclared_ref whether a node references a global never declared
* @property {(node: Node, compressor: Compressor) => number | boolean} is_undefined truthy where a node is `undefined`
* @property {(walker: ParentStack) => boolean} is_used_in_expression whether the expression around a node may use its value
* @property {Set<string>} lazy_op the short-circuiting operators
* @property {Set<string>} unary_side_effects the unary operators writing their operand
*/
/**
* The functions of terser's `compress/compressor-flags.js`, ported.
* @internal
* @typedef {object} CompressFlagHelpers
* @property {number} CLEAR_BETWEEN_PASSES the flags cleared before each pass
* @property {(node: Node, flag: number) => void} clear_flag clears a flag on a node
* @property {(node: Node, flag: number) => number} has_flag the flag where a node carries it, otherwise 0
* @property {(node: Node, flag: number) => void} set_flag sets a flag on a node
*/
/**
* terser's `MAP`: each node of a list transformed, dropping what a transform
* skips and spreading what it splices.
* @internal
* @typedef {((array: Node[], walker: Walker, allowSplicing?: boolean) => Node[]) & { splice: (value: Node[]) => Splice<Node>, skip: SkipMarker }} TransformMap
*/
/**
* The helpers of terser's `utils/index.js`, ported.
* @internal
* @typedef {object} CompressUtils
* @property {TransformMap} MAP terser's `MAP`
* @property {(node: Node, annotation: number) => number} has_annotation the annotation bit where a node carries it, otherwise 0
* @property {(words: string | string[]) => Set<string>} makePredicate the words, from a list or a string of them separated by spaces
* @property {typeof makeNode} make_node terser's `make_node`
* @property {(orig?: Node) => Node} make_void_0 `void 0`
* @property {<T>(name: T, array: T[]) => boolean} member whether a list holds an item
* @property {(source: string) => boolean} regexp_is_safe whether a regular expression may be built without risking a ReDoS
* @property {(source: string) => string} regexp_source_fix a regular expression's source, its line terminators escaped
* @property {typeof removeAll} remove removes an item wherever a list holds it
* @property {typeof alwaysFalse} return_false false, always
*/
/** @typedef {import("./syntax-parser").NodeLike} NodeLike a node as webpack's parser builds one */
/** @typedef {import("./syntax-parser").SyntaxTree} SyntaxTree a tree as webpack's parser writes one */
/** @typedef {import("./syntax-parser").AnyNode} AnyNode any node webpack's parser builds */
/** @typedef {import("./syntax-parser").ExportedNames} ExportedNames the names a module exported so far */
/** @typedef {import("./syntax-parser").ExpressionField} ExpressionField what an `expression` field holds, in whichever node has one */
/** @typedef {import("./syntax-parser").UnfinishedNode} UnfinishedNode a node webpack's parser has started but not finished */
/** @typedef {import("./syntax-parser").Statement} Statement a statement node */
/** @typedef {import("./syntax-parser").ModuleDeclaration} ModuleDeclaration an import or export declaration node */
/**
* @template {import("./syntax-parser").AnyNodeType} T
* @typedef {import("./syntax-parser").NodeOf<T>} NodeOf the node whose `type` is `T`
*/
/** @typedef {import("./ScopeAnalyzer").MinifierScope} MinifierScope a scope as the mangler reads it */
/** @typedef {import("./ScopeAnalyzer").MinifierVariable} MinifierVariable a definition as the mangler reads it */
/** @typedef {import("./ScopeAnalyzer").MinifierLabel} MinifierLabel a label as the mangler reads it */
/** @typedef {import("./ScopeAnalyzer").MinifierScopeAnalysis} MinifierScopeAnalysis the scopes the mangler names */
/** @typedef {import("./ScopeAnalyzer").TreeDefinition} TreeDefinition what a name in a `SyntaxTree` names, as the mangler reads it */
/** @typedef {import("./ScopeAnalyzer").CompressorDialect} CompressorDialect what the compressor's analysis asks of its tree's nodes */
/**
* What a source is minified with. The keys the minifier reads are named; the
* rest are open, so an option a caller passes through is not refused here.
* @typedef {object} MinifyOptions
* @property {boolean | GivenCompressOptions=} compress what the compressor does, or false to leave the tree as read
* @property {boolean | GivenMangleOptions=} mangle which names are renamed, or false to keep them
* @property {GivenFormatOptions=} format how the output is written
* @property {GivenFormatOptions=} output the older spelling of `format`, which may not be named beside it
* @property {DriverOptions["parse"]=} parse how the source is read
* @property {boolean | GivenSourceMapOptions=} sourceMap whether a source map is written, and how
* @property {boolean=} module whether the source is a module
* @property {number=} ecma which edition the output may use
* @property {boolean=} toplevel whether top-level names may be renamed or dropped
* @property {DriverOptions["nameCache"]=} nameCache names held across assets, which this writes into
* @property {boolean | RegExp=} keep_classnames which class names survive
* @property {boolean | RegExp=} keep_fnames which function names survive
* @property {boolean=} ie8 whether the output works around that engine
* @property {boolean=} safari10 whether the output works around that engine
*/
/**
* terser's `compress` options as a caller gives them: `global_defs` any object
* of names, as terser declares it, and `pure_funcs` reading a call as terser's
* tree holds it.
* @typedef {Omit<Partial<CompressOptions>, "global_defs" | "pure_funcs"> & { global_defs?: false | EXPECTED_OBJECT, pure_funcs?: string[] | ((node: PureCallView) => boolean) | null }} GivenCompressOptions
*/
/** @typedef {NodeShape & { print_to_string(given?: Partial<FormatSettings>): string }} PureCallView a call as a `pure_funcs` function reads it: terser's fields and `print_to_string` */
/**
* terser's `mangle` options as a caller gives them, `properties` true for the
* defaults.
* @typedef {Omit<Partial<DriverMangleOptions>, "properties"> & { properties?: boolean | GivenPropertyMangleOptions }} GivenMangleOptions
*/
/**
* terser's `format` options as a caller gives them; `source_map` is left out,
* as the minify writes it from `sourceMap`.
* @typedef {Omit<DriverFormatOptions, "source_map">} GivenFormatOptions
*/
/**
* terser's `sourceMap` options as a caller gives them, `content` a map object
* of any source map library, its JSON or `"inline"`.
* @typedef {Omit<Partial<DriverSourceMapOptions>, "content"> & { content?: DriverSourceMapOptions["content"] | EXPECTED_OBJECT }} GivenSourceMapOptions
*/
/**
* What a minify answers with: the code it wrote, the map it was asked for, and
* the tree where the format options asked for one instead of code.
* @typedef {object} MinifyOutput
* @property {string=} code the minified source
* @property {string | EncodedSourceMap=} map the source map, as a string or an object
* @property {BuiltSourceMap | null=} decoded_map the map with its mappings read
* @property {NodeShape=} ast the tree, where `format.ast` asked for it
*/
/**
* The sources one minify reads: one source, several, or each under the name it
* is reported by; a tree stands in for a source already read.
* @typedef {string | string[] | Record<string, string> | NodeShape} MinifySources
*/
/** @typedef {(sources: MinifySources, options?: MinifyOptions) => Promise<MinifyOutput>} Minify what one call to the minifier takes and answers */
/** @typedef {{ minify: Minify, phases: string[], corrections?: { enabled: boolean }, improvements?: { enabled: boolean, lets: { enabled: boolean }, literalsLast: { enabled: boolean } }, modules: MinifierModules }} Minifier what a caller minifies with, the switches of its corrections and improvements, and the modules it is built from */
/**
* One step building webpack's minifier, installing what it implements onto
* the modules.
* @typedef {{ name: string, install: (modules: MinifierModules) => void }} Phase
*/
/** @typedef {Record<PropertyKey, EXPECTED_ANY>} SymbolDefinition one of terser's `SymbolDef`s */
/**
* One of terser's `SymbolDef`s, as the compressor reads it: its fields the
* shared model types typed for the compressor's tree.
* @typedef {Omit<import("./ScopeAnalyzer").CompressorVariable, "scope" | "identifiers" | "references"> & { scope: Scope, identifiers: Node[], references: Node[] }} Definition
*/
/**
* A scope as the compressor reads it, which the node opening it holds as its
* `block_scope`: its fields the shared model types typed for the compressor's.
* @typedef {Omit<import("./ScopeAnalyzer").CompressorScope, "node" | "upper" | "variables" | "enclosed" | "getBinding" | "getDefunScope" | "findVariable" | "markEnclosed" | "bind" | "defineVariable" | "copyFor" | "adopt"> & ScopeOverrides} Scope
*/
/**
* @typedef {object} ScopeOverrides
* @property {Node} node the node opening it
* @property {Scope | null} upper the enclosing scope
* @property {Definition[]} variables the definitions it declares
* @property {Definition[]} enclosed the definitions it or a scope inside it reads
* @property {(name: string) => Definition | undefined} getBinding the definition it declares under a name
* @property {() => Scope} getDefunScope the nearest scope around it, or it, that is not a block's
* @property {(name: string) => Definition | undefined} findVariable what a name resolves to from here
* @property {(definition: Definition) => void} markEnclosed records that each scope from this one out to a definition's reads it
* @property {(definition: Definition) => void} bind adds a definition it declares
* @property {(symbol: Node, init: Node | null | undefined, isFunction: (node: Node) => boolean) => Definition} defineVariable declares a symbol's name here
* @property {(node: Node) => Scope} copyFor a copy of it for a copy of its node
* @property {(child: Scope, readsArguments: (() => boolean) | null) => void} adopt moves a scope into it
*/
// The helpers of terser's `utils/index.js` the phases call, ours, so a phase
// reads only terser's tree and the methods it replaces.
/**
* terser's `DefaultsError`, what `defaults` throws for an option it does not know.
*/
class DefaultsError extends Error {
/**
* @param {string} message what went wrong
* @param {object} defs the options known
*/
constructor(message, defs) {
super();
this.name = "DefaultsError";
this.message = message;
this.defs = defs;
}
}
/**
* @param {EXPECTED_OBJECT} object an object
* @param {PropertyKey} property a key
* @returns {boolean} whether the object has it as its own
*/
const hasOwn = (object, property) =>
Object.prototype.hasOwnProperty.call(object, property);
/**
* terser's `defaults`: the options given, each one missing taken from the defaults.
* @template {object} T
* @param {object | boolean | null | undefined} given the options given: an object of them, true for all defaults, or something falsy for none
* @param {T} defs the defaults
* @param {boolean=} croak whether an option the defaults lack throws
* @returns {T} the options
*/
const defaults = (given, defs, croak) => {
/** @type {Record<string, T[keyof T] | number> | boolean | null | undefined} */
let args =
/** @type {Record<string, T[keyof T]> | boolean | null | undefined} */ (
given
);
if (args === true) {
args = {};
} else if (args !== null && args !== undefined && typeof args === "object") {
args = { ...args };
}
const options = /** @type {Record<string, T[keyof T] | number>} */ (
args || {}
);
if (croak) {
for (const key in options) {
if (hasOwn(options, key) && !hasOwn(defs, key)) {
throw new DefaultsError(`\`${key}\` is not a supported option`, defs);
}
}
}
for (const key in defs) {
if (!hasOwn(defs, key)) continue;
if (!args || !hasOwn(args, key)) {
options[key] = defs[key];
} else if (key === "ecma" || key === "builtins_ecma") {
let ecma = /** @type {number} */ (args[key]) | 0;
if (ecma > 5 && ecma < 2015) ecma += 2009;
options[key] = ecma;
} else {
options[key] = args[key];
}
}
return /** @type {T} */ (options);
};
/**
* @returns {void}
*/
const noop = () => {};
/**
* @template T
* @param {T[]} array a list
* @param {T} item an item
* @returns {void}
*/
const pushUnique = (array, item) => {
if (!array.includes(item)) array.push(item);
};
/**
* @param {string} text a message with `{name}` holes
* @param {Record<string, string | number | boolean | Comment[] | null | undefined>=} props what fills them
* @returns {string} the message filled
*/
const stringTemplate = (text, props) =>
text.replace(
/\{(.+?)\}/g,
(_match, name) => /** @type {string} */ (props && props[name])
);
/**
* @template T
* @param {T[]} array a list
* @param {T} item an item, removed wherever it is
* @returns {void}
*/
const removeAll = (array, item) => {
for (let i = array.length; --i >= 0;) {
if (array[i] === item) array.splice(i, 1);
}
};
/**
* terser's stable `mergeSort`, splitting and merging exactly as it does so an
* inconsistent comparison still orders alike, but into one buffer.
* @template T
* @param {T[]} array a list
* @param {(a: T, b: T) => number} compare the order
* @returns {T[]} a sorted copy
*/
const mergeSort = (array, compare) => {
const sorted = [...array];
if (sorted.length < 2) return sorted;
/** @type {T[]} */
const buffer = Array.from({ length: sorted.length });
/**
* @param {number} start the first index
* @param {number} end past the last
* @returns {void}
*/
const sortRange = (start, end) => {
if (end - start <= 1) return;
const middle = start + Math.floor((end - start) / 2);
sortRange(start, middle);
sortRange(middle, end);
let left = start;
let right = middle;
let out = start;
while (left < middle && right < end) {
buffer[out++] =
compare(sorted[left], sorted[right]) <= 0
? sorted[left++]
: sorted[right++];
}
while (left < middle) buffer[out++] = sorted[left++];
while (right < end) buffer[out++] = sorted[right++];
for (let i = start; i < end; i++) sorted[i] = buffer[i];
};
sortRange(0, sorted.length);
return sorted;
};
/**
* @template K, V
* @param {Map<K, V[]>} map a map of lists
* @param {K} key a key
* @param {V} value what joins its list
* @returns {void}
*/
const mapAdd = (map, key, value) => {
const list = map.get(key);
if (list !== undefined) list.push(value);
else map.set(key, [value]);
};
/**
* @template V
* @param {Record<string, V>} object what `mapToObject` wrote
* @returns {Map<string, V>} the map it came from
*/
const mapFromObject = (object) => {
const map = new Map();
for (const key in object) {
if (hasOwn(object, key) && key.charAt(0) === "$") {
map.set(key.slice(1), object[key]);
}
}
return map;
};
/**
* @template V
* @param {Map<string, V>} map a map
* @returns {Record<string, V>} it as an object, each key after a `$`
*/
const mapToObject = (map) => {
/** @type {Record<string, V>} */
const object = Object.create(null);
for (const [key, value] of map) object[`$${key}`] = value;
return object;
};
/**
* @param {boolean | RegExp | undefined} setting a `keep_*` option
* @param {string} name a name
* @returns {boolean} whether the option keeps it
*/
const keepName = (setting, name) =>
setting === true || (setting instanceof RegExp && setting.test(name));
/**
* @param {boolean | RegExp | undefined} setting a `keep_*` option
* @returns {boolean} whether it answers each name by where its last match ended
*/
const answersPerCall = (setting) => setting instanceof RegExp && setting.global;
/**
* @param {Node} name a name of an import or export
* @returns {boolean} whether it is the bare `*`
*/
const isUnquotedStar = (name) => name.name === "*" && !name.quote;
/**
* terser's `make_node`: a node, taking its position from another, of the leaf
* kind its properties choose where ESTree types the kind's nodes apart.
* @param {NodeFactory} Type the kind's factory
* @param {Node=} orig where its position comes from
* @param {NodeProps=} props its properties
* @returns {Node} the node
*/
const makeNode = (Type, orig, props) => {
const properties = props || {};
if (orig) {
if (!properties.startToken) properties.startToken = orig.startToken;
if (!properties.endToken) properties.endToken = orig.endToken;
}
const leaf = Type.LEAF;
return (leaf ? leaf(properties) : Type)(properties);
};
/**
* @returns {false} always
*/
const alwaysFalse = () => false;
/**
* Where mangled names come from: the name for each number, and, for a source
* ordering its names by the characters printed, what counts them.
* @typedef {object} IdentifierSource
* @property {(n: number) => string} get the name for a number
* @property {(() => void)=} reset forgets what was counted
* @property {((chars: string, delta: number) => void)=} consider counts the characters of printed text
* @property {(() => void)=} sort orders the names by what was counted
*/
/**
* terser's mangle options, as `formatMangleOptions` defaults them; a key it
* does not default is read as given.
* @internal
* @typedef {object} MangleOptions
* @property {boolean} eval whether names a scope reading `eval` sees are mangled
* @property {IdentifierSource} nth_identifier where mangled names come from
* @property {boolean} ie8 whether the output works around that engine
* @property {boolean | RegExp} keep_classnames which class names survive
* @property {boolean | RegExp} keep_fnames which function names survive
* @property {boolean} module whether the source is a module
* @property {Set<string>} reserved the names never given
* @property {boolean} toplevel whether top-level names are mangled
* @property {boolean=} safari10 whether the output works around that engine
* @property {{ props: Map<string, string> }=} cache the property names given so far, where names are cached
* @property {(boolean | Partial<PropertyMangleOptions>)=} properties whether property names are mangled, and how
*/
/**
* terser's `mangle.properties` options, as `propertyNames` defaults them.
* @internal
* @typedef {object} PropertyMangleOptions
* @property {boolean} builtins whether names builtins have are mangled too
* @property {{ props: Map<string, string> } | null} cache the names given so far, where names are cached
* @property {boolean | string} debug false, or the suffix a debug name ends in
* @property {boolean | "strict"} keep_quoted whether quoted names are kept
* @property {IdentifierSource} nth_identifier where mangled names come from
* @property {boolean} only_cache whether only cached names are mangled
* @property {RegExp | string | null} regex which names are mangled
* @property {string[]} reserved the names never mangled
* @property {boolean} undeclared whether names read off undeclared globals are mangled
* @property {boolean} only_annotated whether only names marked for mangling are mangled
*/
/** @typedef {{ ie8: boolean, nth_identifier: IdentifierSource, module: boolean }} ScopeMangleOptions the mangle options the compressor's scopes are figured out with */
/** @typedef {{ source: string, flags: string }} RegExpValue a regular expression literal's value: its source and flags */
/**
* terser's `format_mangler_options`: the mangle options, defaulted, their
* reserved names a set that always holds `arguments`.
* @param {MinifyOptions["mangle"] | Partial<MangleOptions> | DriverMangleOptions} given the mangle options given
* @param {IdentifierSource} base54 the default source of mangled names
* @returns {MangleOptions} the options
*/
const formatMangleOptions = (given, base54) => {
const options = /** @type {MangleOptions} */ (
/** @type {Omit<MangleOptions, "reserved"> & { reserved: string[] | Set<string> }} */ (
defaults(given, {
eval: false,
nth_identifier: base54,
ie8: false,
keep_classnames: false,
keep_fnames: false,
module: false,
reserved: [],
toplevel: false
})
)
);
if (options.module) options.toplevel = true;
options.reserved = new Set(
Array.isArray(options.reserved) || options.reserved instanceof Set
? options.reserved
: []
);
options.reserved.add("arguments");
return options;
};
/**
* One of terser's AST nodes: the fields and methods the phases share typed, the
* rest open.
* @typedef {{
* TYPE: string,
* type: string,
* mapName: string | null | undefined,
* shorthand: boolean,
* _annotations: number,
* name: string,
* kind: string,
* role: string,
* computed: boolean,
* label: NodeShape,
* property: NodeShape,
* flags: number,
* startToken: Token,
* endToken: Token,
* expression: NodeShape,
* callee: NodeShape,
* object: NodeShape,
* argument: NodeShape,
* discriminant: NodeShape,
* left: NodeShape,
* right: NodeShape,
* operator: string,
* test: NodeShape,
* alternate: NodeShape,
* arguments: NodeShape[],
* elements: (NodeShape | null)[],
* properties: NodeShape[],
* declarations: NodeShape[],
* expressions: NodeShape[],
* segments: NodeShape[],
* params: NodeShape[],
* quote: string | undefined,
* raw: string | undefined,
* optional: boolean,
* tag: NodeShape,
* superClass: NodeShape,
* scope: NodeShape,
* definition: SymbolDefinition,
* fixed_value(): NodeShape | false | null | undefined,
* tail_node(): NodeShape,
* clone(deep?: boolean): NodeShape,
* transform(walker: CompressorShape, inList?: boolean): NodeShape,
* optimize(compressor: CompressorShape): NodeShape,
* has_side_effects(compressor: CompressorShape): boolean,
* may_throw(compressor: CompressorShape): boolean,
* drop_side_effect_free(compressor: CompressorShape, firstInStatement?: boolean): NodeShape | null,
* is_constant_expression(scope?: NodeShape): boolean | "f",
* evaluate(compressor: CompressorShape): EvaluatedValue | NodeShape,
* negate(compressor: CompressorShape, firstInStatement?: boolean): NodeShape,
* size(compressor?: CompressorShape, stack?: NodeShape[]): number,
* [key: string]: EXPECTED_ANY,
* [key: symbol]: number,
* }} NodeShape
*/
/**
* The fields of one of terser's AST nodes: every field any kind has, a field
* holding no node typed as the node the kinds having it hold.
* @internal
* @typedef {object} NodeFields
* @property {string} type its ESTree type
* @property {number} flags terser's bits on it: `_PURE`, `_INLINE`, `_NOINLINE`, `_KEY`, `_MANGLEPROP` and the compressor's
* @property {NodeToken | number} startToken the token it starts at, or its number
* @property {NodeToken | number} endToken the token it ends at, or its number
* @property {number} _annotations a call's or a member's annotations
* @property {string | null | undefined} mapName the name a member's source mapping carries where renaming set it
* @property {string} name a symbol's or a literal's name
* @property {string} kind a declaration's, property's or method's kind
* @property {string} role what a node of a shared ESTree type is to terser
* @property {string} operator a unary, update, binary, logical or assignment operator
* @property {string | undefined} quote the quote a key or a string had
* @property {string | undefined} raw a literal's or a template element's source
* @property {string} directive a directive's text
* @property {string | null | undefined} phase an import's phase
* @property {boolean} atom whether a name is printed as a constant
* @property {boolean} computed whether a key or a property is computed
* @property {boolean} shorthand whether a property is written as its name alone
* @property {boolean} optional whether a call or a property read is optional
* @property {boolean} async whether a function is async
* @property {boolean} generator whether a function is a generator
* @property {boolean} static whether a class member is static
* @property {boolean} method whether a property is a method
* @property {boolean} prefix whether a unary or an update operator comes first
* @property {boolean} delegate whether a `yield` is a `yield*`
* @property {boolean} await whether a loop or a declaration awaits
* @property {boolean} is_array whether a destructuring is an array pattern
* @property {boolean} is_default whether an export is the default one
* @property {boolean} logical whether an assignment is a logical one
* @property {Node} label a labeled statement's label, or the label a `break` or `continue` names
* @property {Node} property a property read's property
* @property {Node} expression a statement's or a chain's expression
* @property {Node} callee a call's callee
* @property {Node} object a property read's object, or a `with`'s
* @property {Node} argument an operand, or a `return`'s, `throw`'s, `await`'s, `yield`'s, spread's or rest's value
* @property {Node} discriminant a `switch`'s value
* @property {Node} left a binary's or an assignment's left side, or a `for`-`in` or `for`-`of`'s binding
* @property {Node} right a binary's or an assignment's right side, or what a `for`-`in` or `for`-`of` iterates
* @property {Node} test a condition
* @property {Node} alternate an `if`'s or a conditional's alternate
* @property {Node | Node[]} consequent an `if`'s or a conditional's consequent, or a `case`'s statements
* @property {Node | Node[]} body a block's statements, or a loop's, label's or function's body
* @property {Node} tag a tagged template's tag
* @property {Node} quasi a tagged template's template
* @property {Node} superClass the class a class extends
* @property {Node} id a function's, class's or declarator's name
* @property {Node} init a declarator's or a `for`'s initializer, or the value a name is declared with
* @property {Node} update a `for`'s update
* @property {Node} block a `try`'s block
* @property {Node} handler a `try`'s `catch`
* @property {Node} finalizer a `try`'s `finally`
* @property {Node} param a `catch`'s binding
* @property {Node} key a member's key
* @property {Node | string | number | boolean | RegExpValue | null} value a member's value, a literal's or a constant name's value, or a template element's cooked text
* @property {Node} source an import's or an export's source
* @property {Node} attributes an import's or an export's attributes
* @property {Node} imported_name an import's default binding
* @property {Node} local a name mapping's local name
* @property {Node} imported a name mapping's imported name
* @property {Node} exported a name mapping's exported name
* @property {Node} exported_value what a default export exports
* @property {Node} exported_definition the declaration an export exports
* @property {Node[]} arguments a call's arguments
* @property {(Node | null)[]} elements an array's elements, null for a hole
* @property {Node[]} properties an object's properties
* @property {Node[]} declarations a declaration's declarators
* @property {Node[]} expressions a sequence's expressions
* @property {Node[]} segments a template's texts and expressions in order
* @property {Node[]} params a function's parameters
* @property {Node[]} cases a `switch`'s cases
* @property {Node[]} args a dynamic import's source and options
* @property {Node[]} imported_names an import's named bindings
* @property {Node[]} exported_names an export's names
* @property {Scope} block_scope the scope a node opening one holds
* @property {Scope} scope the scope a symbol is read or declared in
* @property {Map<string, Definition>} globals the program's undeclared names
* @property {Node[]} references a symbol's or a label's reads
* @property {Node | (() => Node) | false | null | undefined} fixed the value a symbol reads, as `reduce_vars` worked it out, false where unknown
* @property {Definition} definition what a symbol names
* @property {TokenTables | null} tokenTables the program's tables its token numbers are read through
*/
/**
* One of terser's AST nodes, as the phases read and write it: its fields, and
* the kind bits its kind's prototype holds under keys of their own. A key
* holding another value, a walk or a method a phase dispatches on the kind, is
* read through a cast.
* @internal
* @typedef {NodeFields & { [key: symbol]: number }} Node
*/
/** @typedef {NodeFields[keyof NodeFields]} NodeFieldValue any one field's value */
/**
* What the class of a node type is called: the root is `SyntaxNode`, and every
* other class is its type with `Node` after it.
* @param {string} type the node type
* @returns {`${Capitalize<string>}Node`} the class's name
*/
const classNameOf = (type) =>
/** @type {`${Capitalize<string>}Node`} */ (
type === "Node" ? "SyntaxNode" : `${type}Node`
);
// terser's empty slot: a node field holding no node reads `null`, which the
// fields' type leaves out so that every read of a present node stays typed.
const NO_NODE = /** @type {Node} */ (/** @type {Node | null} */ (null));
/** @typedef {{ [K in keyof NodeFields]?: NodeFields[K] | null }} NodeProps the properties a node is built from, null for a child it lacks */
/** @typedef {(value: unknown) => boolean} NodeTest whether a value is a node of one kind, or of a kind one of it */
/**
* What terser's `ast.js` exports: each kind's factory, the predicate
* `is<Kind>Node` for it, and the walkers and helpers, which stay open.
* @typedef {{
* [key: `${Capitalize<string>}Node`]: NodeFactory,
* [key: `is${string}Node`]: NodeTest,
* expressionOf(node: NodeShape): NodeShape | undefined,
* createStackTransformer(before?: (this: CompressorShape, node: NodeShape, descend: (node: NodeShape, walker: CompressorShape) => void, inList: boolean | undefined) => NodeShape | Splice | SkipMarker | null | void, after?: (this: CompressorShape, node: NodeShape, inList: boolean | undefined) => NodeShape | Splice | SkipMarker | null | void): CompressorShape,
* namesOf(node: NodeShape): (NodeShape | null)[],
* wrapOptimizer(name: string, wrap: (original: (self: NodeShape, compressor: CompressorShape) => NodeShape) => (self: NodeShape, compressor: CompressorShape) => NodeShape): boolean,
* [key: string]: EXPECTED_ANY,
* }} Ast
*/
/**
* What terser's `ast.js` exports, as the phases read it: each kind's factory,
* the predicate `is<Kind>Node` for it, and the walkers and helpers.
* @internal
* @typedef {{ [key: `${Capitalize<string>}Node`]: NodeFactory, [key: `is${string}Node`]: NodeTest } & AstHelpers} TerserAst
*/
/**
* The walkers and helpers on terser's `ast.js` the phases install and read.
* @internal
* @typedef {object} AstHelpers
* @property {typeof ParsedToken} ParsedToken terser's token class
* @property {(value: unknown) => value is Node} isSyntaxNode whether a value is a node
* @property {(n: Node) => string | undefined} kindOf the kind of a node, terser's class, as it was made
* @property {(node: Node) => NodeFactory} factoryOf the factory making nodes of a node's kind
* @property {(table: [string, number][]) => symbol} kindBits gives each kind's prototype the bits of a list of kinds, read under the key answered
* @property {symbol} nodeKindBits the key the kind bits the walkers read are read under
* @property {(node: Node, visitor: Visitor) => boolean | void} walkNode hands a node to a visitor, which walks its children
* @property {(node: Node, push: (node: Node) => void) => void} pushChildrenBackwards pushes a node's children last first
* @property {(node: Node, tw: Walker, inList?: boolean) => Node} transformNode terser's `transform`
* @property {(node: Node, tw: Walker) => void} descendNode transforms a node's children, but not the node itself
* @property {(visit?: WalkVisit, before?: TransformBefore, after?: TransformAfter) => Walker} createWalker terser's `TreeWalker`
* @property {(before?: TransformBefore, after?: TransformAfter) => Walker} createTransformer terser's `TreeTransformer`
* @property {(before?: TransformBefore, after?: TransformAfter) => Walker} createStackTransformer a transformer tracking no directive
* @property {(node: Node) => Node | undefined} expressionOf what terser's `expression` field held
* @property {(node: Node) => Node | Node[] | undefined} bodyOf what terser's `body` field held
* @property {(key: Node) => Node | string} terserKey a key as terser held it
* @property {(node: Node) => Node | string | undefined} terserProperty a property read's property as terser held it
* @property {(node: Node) => (Node | null)[]} namesOf what terser's destructuring held as `names`
* @property {(node: Node, callback: (node: Node, toVisit: Node[]) => WalkResult, toVisit?: Node[]) => boolean} walk terser's `walk`
* @property {(node: Node, callback: (node: Node, info: ParentStack) => WalkResult, initialStack?: Node[]) => boolean} walk_parent terser's `walk_parent`
* @property {(body: Node[], visitor: Visitor) => void} walk_body terser's `walk_body`
* @property {symbol} walk_abort what a `walk` callback returns to stop the walk
* @property {number} _PURE the flag of a call annotated pure
* @property {number} _INLINE the flag of a call annotated to inline
* @property {number} _NOINLINE the flag of a call annotated never to inline
* @property {number} _KEY the flag of a string read as a key
* @property {number} _MANGLEPROP the flag of a string read as a mangled property
* @property {WeakSet<Node>} noSideEffectsFunctions the functions a `#__NO_SIDE_EFFECTS__` comment marks, whose calls are pure, and the names declared with them
* @property {(self: Node, deep?: boolean, toplevel?: Node) => Node} cloneNode terser's `clone`
* @property {(self: Node) => boolean} isPinned whether `eval` or `with` can reach the names of the scope a node opens
* @property {(self: Node) => boolean} usesEval whether a scope's node reads `eval`
* @property {(self: Node) => boolean} usesArguments whether a function reads `arguments`
* @property {(self: Node, name: string) => Node} wrapCommonjs the toplevel wrapped as a CommonJS module
* @property {(self: Node, argsValues: boolean | string) => Node} wrapEnclose the toplevel wrapped in a function called at once
* @property {(self: Node) => Node[]} argsAsNames the names a function's parameters bind
* @property {(self: Node) => Node | false | undefined} isBraceless the expression an arrow's body is, where it has no braces
* @property {(self: Node) => number} lengthProperty a function's `length`
* @property {(self: Node) => Node[]} allSymbols the symbols a destructuring declares
* @property {(self: Node) => Node[]} declarationsAsNames the names a declarator declares
* @property {(self: Node) => Node | string | number | boolean | RegExpValue | null} constantValue a constant's value
* @property {(self: Node, visitor: StackVisitor) => void} visitNondeferredClassParts walks what a class runs as it is defined
* @property {(self: Node, visitor: StackVisitor) => void} visitDeferredClassParts walks what a class runs later
* @property {(self: Node, compressor: Compressor) => boolean} runsOnlyHeritage whether a class runs nothing with effects but its heritage
* @property {(self: Node) => boolean} isSelfReferential whether a function reads its own name
* @property {(self: Node, compressor: Compressor) => Node} resolveDefines the toplevel, each global `global_defs` names replaced
* @property {(self: Node, options: boolean | string[]) => Node} dropConsole the toplevel, its `console` calls dropped
* @property {(node: Node, compressor: Compressor, suffix: string) => Node | undefined} findDefinitions what `global_defs` gives a node
* @property {(self: Node, compressor?: Compressor, ancestors?: Node[]) => number} nodeSize the bytes a node prints to
* @property {(kind: string, wrap: (original: (self: Node, other: Node) => boolean) => (self: Node, other: Node) => boolean) => void} wrapShallowEquivalent wraps whether two nodes of a kind are alike, their children aside
* @property {(self: Node, node: Node) => boolean} isEquivalent whether two nodes print alike
* @property {(self: Node, compressor: Compressor) => Node} hoistProperties terser's `hoist_properties`
* @property {(definition: Definition, options?: Partial<MangleOptions>) => boolean | number} isDefinitionUnmangleable terser's `SymbolDef#unmangleable`
* @property {(self: Scope, node: Node) => void} addChildScope moves a function's or class's scope into a scope
* @property {(self: Scope, SymbolClass: NodeFactory, options?: { source?: Node, tentative_name?: string, scope?: Scope, conflict_scopes?: Iterable<Scope>, init?: Node | null }) => Node} createSymbol terser's `create_symbol`
* @property {(self: Node) => boolean} isBlockScope whether a node opens a block scope
* @property {(self: Scope, symbol: Node, init?: Node | null) => Definition} defineVariable declares a symbol's name in a scope
* @property {(self: Scope, symbol: Node, init?: Node) => Definition} defineFunction declares a function's name in a scope
* @property {(self: Node) => boolean} isUnreferenced whether a symbol's definition is read nowhere
* @property {(self: Node) => boolean} isGlobal whether a symbol names a global
* @property {(self: Node, options: Partial<MangleOptions>, context?: { parent_scope?: Scope | null, toplevel?: Node }) => void} figureOutScope terser's `figure_out_scope`
* @property {(node: Node) => Node} tailNode a sequence's last expression, any other node itself
* @property {(self: Node, compressor: Compressor) => void} dropUnused terser's `drop_unused`
* @property {(self: Node, compressor: Compressor) => void} resetOptimizerFlags terser's `reset_opt_flags`
* @property {(estree: NodeLike, ownTokenAt?: (pos: number, ending: boolean) => number | undefined, source?: string, ownTable?: TokenTable) => Node} adoptEstree the minifier's tree, built from an ESTree one
* @property {(node: Node, parent?: Node) => EstreeOut} toEstree the ESTree node of a node
* @property {(node: Node) => boolean} isArgumentsObject whether a symbol reads a function's own `arguments`
* @property {(node: Node, compressor: Compressor) => boolean} dotThrows whether reading a property off a value throws
* @property {(self: Node, includeNewTarget?: boolean) => boolean} containsThis whether a node reads `this`
* @property {(node: Node, scope?: Scope) => boolean | "f"} isConstantExpression whether a node is a constant expression
* @property {(self: Node, key: string, compressor: Compressor) => Node | undefined} flattenObject the value an object literal holds under a key
* @property {(node: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} dropSideEffectFree what of a node has side effects
* @property {(node: Node, compressor: Compressor, firstInStatement?: boolean) => Node} negate a node's negation
* @property {(self: Node, compressor: Compressor) => EvaluatedValue | Node} evaluate a node's value, any value a constant expression computes, or the node where it has none worth writing
* @property {(self: Node) => boolean} isConstant whether a node is a constant
* @property {(node: Node, compressor: Compressor, depth?: number, inChain?: boolean) => EvaluatedValue | Node} evaluateNode a node's value, any value a constant expression computes, or the node where it has none known
* @property {(type: string, wrap: (original: Evaluate) => Evaluate) => void} wrapEvaluate wraps the evaluation of a kind
* @property {(self: Node, compressor: Compressor) => boolean} mayThrowOnAccess whether reading a property off a node's value throws
* @property {(self: Node, compressor: Compressor) => boolean} isCalleePure whether a call's callee is pure
* @property {(node: Node) => boolean} isBoolean whether a node's value is a boolean
* @property {(node: Node, compressor: Compressor) => boolean} isNumber whether a node's value is a number
* @property {(node: Node, compressor: Compressor) => boolean} isBigInt whether a node's value is a bigint
* @property {(node: Node, compressor: Compressor) => boolean} isNumberOrBigInt whether a node's value is a number or a bigint
* @property {(node: Node, compressor: Compressor) => boolean} is32BitInteger whether a node's value is a 32-bit integer
* @property {(node: Node, compressor: Compressor) => boolean} isString whether a node's value is a string
* @property {(node: Node, compressor: Compressor) => boolean} mayThrow whether a node may throw
* @property {(type: string, wrap: (original: (self: Node, compressor: Compressor) => boolean) => (self: Node, compressor: Compressor) => boolean) => void} wrapHasSideEffects wraps whether a kind has side effects
* @property {(type: string, wrap: (original: (self: Node, compressor: Compressor) => boolean) => (self: Node, compressor: Compressor) => boolean) => void} wrapMayThrow wraps whether a kind may throw
* @property {(node: Node, compressor: Compressor) => boolean} hasSideEffects whether a node has side effects
* @property {(node: Node, compressor: Compressor, in32BitContext?: boolean) => Node} bitwiseNegate a node's bitwise negation
* @property {(node: Node, compressor: Compressor) => boolean | undefined} isCallPure whether a call is pure
* @property {(node: Node) => Node | null | false | undefined} aborts the statement a node leaves its block by, where it does
* @property {(type: string, wrap: (original: (self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null) => (self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null) => void} wrapDropSideEffectFree wraps what of a kind has side effects
* @property {(self: Node, insert: boolean, compressor?: Compressor) => void} processExpression terser's `process_expression`
* @property {(self: Node) => Node | false | null | undefined} fixedValue the value a symbol's variable is known to hold, if any
* @property {(self: Node) => boolean} isImmutable whether a reference reads a function expression's own name
* @property {(self: Node, compressor: Compressor) => boolean} isDeclared whether a symbol's name is declared
* @property {(self: Node, compressor: Compressor) => Node} hoistDeclarations terser's `hoist_declarations`
* @property {(self: Node, compressor: Compressor) => Node | null} toAssignments a declaration's declarators as assignments
* @property {(self: Node) => boolean} containsOptional whether a chain holds an optional link
* @property {(self: Node, compressor: Compressor) => Node} liftBinarySequences terser's `lift_sequences` of a binary
* @property {(node: Node, compressor: Compressor) => Node} optimize a node optimized
* @property {(name: string, wrap: (original: Optimize) => Optimize) => boolean} wrapOptimizer wraps the optimizer of a kind
*/
/** @typedef {boolean | symbol | void} WalkResult what a `walk` callback returns: true to skip a node's children, `walk_abort` to stop */
/** @typedef {NodeToken} Token one of terser's tokens */
/**
* One of terser's tokens, a `ParsedToken` or a `SourceToken`, as the phases
* and the stream read it.
* @internal
* @typedef {object} NodeToken
* @property {string} type terser's name for its kind
* @property {string} value what it holds
* @property {number} line its line, from 1
* @property {number} col its column, from 0
* @property {number} pos its offset
* @property {string | null | undefined} file its file
* @property {boolean} nlb whether a line break comes before it
* @property {Comment[]} comments_before the comments before it
* @property {Comment[]} comments_after the comments after it
* @property {number | null=} endpos the offset it ends at, where terser's own parser read it
* @property {number | null=} endline the line it ends on, where terser's own parser read it
* @property {number | null=} endcol the column it ends at, where terser's own parser read it
*/
/** @typedef {{ type: string, value: string, nlb?: boolean }} Comment a comment terser's tokenizer read */
/** @typedef {FormatSettings} FormatOptions terser's `format` options, defaulted */
/**
* The node a `comments` callback reads a comment off, published as terser
* types it, as any node.
* @internal
* @typedef {PrintNode} CommentedNode
*/
/** @typedef {{ type: "comment1" | "comment2" | "comment3" | "comment4", value: string, pos: number, line: number, col: number, file?: string }} KeptComment a comment as a `comments` callback is handed it: terser's comment token, its offset, line from 1 and column from 0, and the file it was read from */
/** @typedef {(this: PrintNode, comment: Comment) => boolean} CommentFilter which comments are printed, `this` the node they are read off */
/**
* What the output stream reads off a node for comments, an ESTree node or a
* parsed tree's node by its index: the tokens it starts and ends at, a value
* no line break may precede, the child the comment walk continues into below a
* node on such a value's leftmost edge, and whether it is a statement.
* @template [N=PrintNode]
* @typedef {{ startToken(node: N): NodeToken | number | null | undefined, endToken(node: N): NodeToken | number | null | undefined, keywordValue(node: N): N | null | undefined, leftEdgeChild(node: N): N | null | undefined, isStatement(node: N): boolean }} NodeAccess
*/
// The `format` options terser's stream reads, each with its default. A terser
// naming any other set keeps its own stream, since an option this does not
// know would be silently ignored.
/** @type {FormatOptions} */
const FORMAT_DEFAULTS = {
ascii_only: false,
beautify: false,
braces: false,
comments: "some",
ecma: 5,
ie8: false,
indent_level: 4,
indent_start: 0,
inline_script: true,
keep_numbers: false,
keep_quoted_props: false,
max_line_len: false,
preamble: null,
preserve_annotations: false,
quote_keys: false,
quote_style: 0,
safari10: false,
semicolons: true,
shebang: true,
shorthand: undefined,
source_map: null,
webkit: false,
width: 80,
wrap_iife: false,
wrap_func_args: false,
_destroy_ast: false
};
// Where terser's stream moves what it wrote into one string, so reading back
// from the end never flattens more than this many characters.
const COMMIT_AFTER = 8000;
// The bytes giving the busiest names the shortest must save before the
// mangler does: below it, renaming costs gzip more than it saves.
const SHORTER_NAMES_MIN_SAVING = 200;
const ANNOTATION = /[@#]__(PURE|INLINE|NOINLINE)__/;
const ONLY_ANNOTATION = /^\s*[@#]__(PURE|INLINE|NOINLINE)__\s*$/;
// The comments `comments: "some"` keeps: a license, a copyright, `@preserve`.
const IMPORTANT_COMMENT = /@preserve|@copyright|@lic|@cc_on|^\**!/i;
// The characters `[$0-9A-Z_a-z]`, the ASCII part of what terser reads as an
// identifier character, by code.
const ASCII_IDENTIFIER = new Uint8Array(128);
for (let code = 0; code < 128; code++) {
ASCII_IDENTIFIER[code] =
code === 36 ||
code === 95 ||
(code >= 48 && code <= 57) ||
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122)
? 1
: 0;
}
// What a token may open with and still follow a pending semicolon that
// `semicolons: false` would write as a line break.
const REQUIRE_SEMICOLON = new Set([
"(",
"[",
"+",
"*",
"/",
"-",
",",
".",
"`"
]);
// The `format` options that only lay out readable output. The printer writes
// minified output alone, so each is read as its default whatever it is given.
const IGNORED_FORMAT_OPTIONS = [
"beautify",
"braces",
"indent_level",
"indent_start",
"max_line_len",
"width"
];
/**
* @param {FormatSettings} options defaulted format options
* @returns {FormatSettings} the same, each layout option at its default
*/
const minifiedOptions = (options) => {
for (const name of IGNORED_FORMAT_OPTIONS) {
/** @type {Record<string, FormatSettingValue>} */ (options)[name] =
/** @type {Record<string, FormatSettingValue>} */ (FORMAT_DEFAULTS)[name];
}
return options;
};
// The format options whose setting leaves the text, less its comments, what
// terser's `print_to_string` writes: `shorthand` is read apart, from `ecma`.
const PRINT_ONCE_FREE = new Set([
...IGNORED_FORMAT_OPTIONS,
"comments",
"preamble",
"shebang",
"shorthand",
"source_map",
"_destroy_ast"
]);
/**
* The `const` declarations a compressed tree writes as `let`, each with the
* node holding it, and the statement lists where declarations neighbor.
* @typedef {object} PendingLets
* @property {Node[]} declarations the declarations
* @property {Node[]} holders what holds each, in order: a loop's head or a node whose statements list it
* @property {Node[][]} bodies the statement lists holding two declarations in a row, where `join_vars` joins them, else none
* @property {boolean} join whether neighboring `let` declarations join, as `join_vars` says
*/
/**
* What writing the `let` declarations changed in the printed text, so a count
* of its characters can be put back to the text mangling named by.
* @typedef {object} LetCounts
* @property {number} converted the `const` declarations written as `let`
* @property {number} joinedConstSpaced the converted declarations joined into the one before, a name following their keyword
* @property {number} joinedConstBare those with a pattern following their keyword
* @property {number} joinedLetSpaced the `let` declarations joined into the one before, a name following their keyword
* @property {number} joinedLetBare those with a pattern following their keyword
*/
/**
* Counts the characters of a print made before the `let` declarations were
* written back to what terser counts: `const` where one was written as `let`,
* and the keyword a joined declaration lost, as `;let ` or `;const `.
* @param {CharacterCounter} identifiers the counter
* @param {LetCounts} counts what writing them changed
* @returns {void}
*/
const countLetsBack = (identifiers, counts) => {
const joinedConst = counts.joinedConstSpaced + counts.joinedConstBare;
const joinedLet = counts.joinedLetSpaced + counts.joinedLetBare;
const kept = counts.converted - joinedConst;
identifiers.consider("const", kept);
identifiers.consider("let", -kept);
identifiers.consider(";const ", counts.joinedConstSpaced);
identifiers.consider(";const", counts.joinedConstBare);
identifiers.consider(";let ", counts.joinedLetSpaced);
identifiers.consider(";let", counts.joinedLetBare);
identifiers.consider(",", -(joinedConst + joinedLet));
};
/**
* @param {FormatSettings} options a stream's defaulted format options
* @returns {boolean} whether its text, less comments, is what `print_to_string` writes, so one print can count the characters mangling reads
*/
const printsAsString = (options) => {
if (options.shorthand) return false;
for (const name of Object.keys(FORMAT_DEFAULTS)) {
if (
!PRINT_ONCE_FREE.has(name) &&
/** @type {Record<string, FormatSettingValue>} */ (options)[name] !==
/** @type {Record<string, FormatSettingValue>} */ (FORMAT_DEFAULTS)[
name
]
) {
return false;
}
}
return true;
};
/**
* @param {number[]} encoded pairs of gap since the previous range and range length
* @returns {Int32Array} each range's first code point and the one past its last, in turn
*/
const decodeRanges = (encoded) => {
const bounds = new Int32Array(encoded.length);
let code = 0;
for (let i = 0; i < encoded.length; i += 2) {
code += encoded[i];
bounds[i] = code;
code += encoded[i + 1];
bounds[i + 1] = code;
}
return bounds;
};
/**
* @param {Int32Array} bounds decoded ranges
* @param {number} code a code point
* @returns {boolean} whether a range holds it
*/
const inRanges = (bounds, code) => {
let low = 0;
let high = bounds.length / 2 - 1;
while (low <= high) {
const middle = (low + high) >> 1;
if (code < bounds[middle * 2]) high = middle - 1;
else if (code >= bounds[middle * 2 + 1]) low = middle + 1;
else return true;
}
return false;
};
const BASIC_IDENTIFIER = /^[a-z_$][a-z0-9_$]*$/i;
/**
* terser's `unicode.js`: what the stream asks of a character to decide whether
* it prints as written. The narrow tables are Unicode 8.0, which older engines
* read; the broad ones are the running engine's own.
* @returns {{ getFullChar: (str: string, pos: number) => string, getFullCharCode: (str: string, pos: number) => number, isBasicIdentifier: (str: string) => boolean, isIdentifierStart: (character: string) => boolean, isIdentifierChar: (character: string) => boolean, isIdentifierStartBroad: (character: string) => boolean, isIdentifierCharBroad: (character: string) => boolean, isIdentifierCharacter: (character: string) => boolean, isIdentifierString: (str: string, allowSurrogates?: boolean) => boolean }} the helpers
*/
const createUnicode = () => {
const {
NARROW_IDENTIFIER_PART_RANGES,
NARROW_IDENTIFIER_START_RANGES
} = require("./data");
const narrowStart = decodeRanges(NARROW_IDENTIFIER_START_RANGES);
const narrowPart = decodeRanges(NARROW_IDENTIFIER_PART_RANGES);
const broadStart = /[_$\p{ID_Start}]/u;
const broadPart = /[$\u200C\u200D\p{ID_Continue}]+/u;
/**
* @param {number} code a UTF-16 code unit
* @returns {boolean} whether it opens a surrogate pair
*/
const isHead = (code) => code >= 0xd800 && code <= 0xdbff;
/**
* @param {number} code a UTF-16 code unit
* @returns {boolean} whether it closes a surrogate pair
*/
const isTail = (code) => code >= 0xdc00 && code <= 0xdfff;
return {
getFullChar(str, pos) {
if (isHead(str.charCodeAt(pos))) {
if (isTail(str.charCodeAt(pos + 1))) {
return str.charAt(pos) + str.charAt(pos + 1);
}
} else if (
isTail(str.charCodeAt(pos)) &&
isHead(str.charCodeAt(pos - 1))
) {
return str.charAt(pos - 1) + str.charAt(pos);
}
return str.charAt(pos);
},
getFullCharCode(str, pos) {
const code = str.charCodeAt(pos);
if (isHead(code)) {
return (
0x10000 + ((code - 0xd800) << 10) + str.charCodeAt(pos + 1) - 0xdc00
);
}
return code;
},
isBasicIdentifier: (str) => BASIC_IDENTIFIER.test(str),
isIdentifierStart: (character) =>
inRanges(narrowStart, /** @type {number} */ (character.codePointAt(0))),
isIdentifierChar: (character) =>
inRanges(narrowPart, /** @type {number} */ (character.codePointAt(0))),
isIdentifierStartBroad: (character) => broadStart.test(character),
isIdentifierCharBroad: (character) => broadPart.test(character),
isIdentifierCharacter: (character) => {
if (character.length === 1) {
const code = character.charCodeAt(0);
if (code < 128) return ASCII_IDENTIFIER[code] === 1;
}
return broadPart.test(character);
},
isIdentifierString(str, allowSurrogates) {
if (BASIC_IDENTIFIER.test(str)) return true;
if (!allowSurrogates && /[\uD800-\uDFFF]/.test(str)) return false;
if (str.length === 0) return false;
let code = /** @type {number} */ (str.codePointAt(0));
if (!inRanges(narrowStart, code)) return false;
for (let i = code > 0xffff ? 2 : 1; i < str.length;) {
code = /** @type {number} */ (str.codePointAt(i));
if (!inRanges(narrowPart, code)) return false;
i += code > 0xffff ? 2 : 1;
}
return true;
}
};
};
// The ESTree types of terser's statement classes, its functions, classes,
// switch cases and catch clauses among them.
const ESTREE_STATEMENT_TYPES = new Set([
"Program",
"ExpressionStatement",
"BlockStatement",
"EmptyStatement",
"DebuggerStatement",
"LabeledStatement",
"DoWhileStatement",
"WhileStatement",
"ForStatement",
"ForInStatement",
"ForOfStatement",
"WithStatement",
"ReturnStatement",
"ThrowStatement",
"BreakStatement",
"ContinueStatement",
"IfStatement",
"SwitchStatement",
"SwitchCase",
"TryStatement",
"CatchClause",
"VariableDeclaration",
"ExportNamedDeclaration",
"ExportAllDeclaration",
"ExportDefaultDeclaration",
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
"ClassDeclaration",
"ClassExpression",
"StaticBlock"
]);
/**
* What the output stream reads off the compressed tree, by ESTree type, each
* answer the one terser's accessors gave.
* @type {NodeAccess}
*/
const ESTREE_NODE_ACCESS = {
startToken: (node) => node.startToken,
endToken: (node) => node.endToken,
keywordValue: (node) => {
switch (node.type) {
case "ReturnStatement":
case "ThrowStatement":
case "AwaitExpression":
case "YieldExpression":
return node.argument || undefined;
default:
return undefined;
}
},
leftEdgeChild: (parent) => {
switch (parent.type) {
case "AwaitExpression":
case "YieldExpression":
return parent.argument;
case "UpdateExpression":
return parent.prefix ? undefined : parent.argument;
case "BinaryExpression":
// terser's `#x in y` is no Binary.
return parent.left.type === "PrivateIdentifier"
? undefined
: parent.left;
case "LogicalExpression":
case "AssignmentExpression":
return parent.left;
case "CallExpression":
return parent.callee;
case "ConditionalExpression":
return parent.test;
case "MemberExpression":
return parent.property.type === "PrivateIdentifier"
? undefined
: parent.object;
case "SequenceExpression":
return parent.expressions[0];
default:
return undefined;
}
},
isStatement: (node) => ESTREE_STATEMENT_TYPES.has(node.type)
};
/**
* webpack's stream for minified output, answering terser's code generators
* through the same members terser's own `OutputStream` does and writing the
* same bytes. It writes no layout, so it never breaks a line behind itself.
* @param {TerserModules} modules terser's modules, read for whether webpack's corrections are on
* @returns {new (options: FormatSettings, readonly: boolean, nodeAccess: NodeAccess) => OutputStream} the stream class
*/
const createMinifiedOutput = (modules) => {
const {
getFullChar,
getFullCharCode,
isBasicIdentifier,
isIdentifierChar,
isIdentifierCharBroad,
isIdentifierCharacter,
isIdentifierStart,
isIdentifierStartBroad
} = createUnicode();
/**
* @param {PrintNode | null | undefined} value a returned, awaited or yielded value
* @param {Set<Comment | Comment[]>} printed the comments and comment lists printed so far
* @param {NodeAccess} nodeAccess what is read off the value's nodes
* @param {TokenTables} tables what their tokens' numbers are read through
* @returns {boolean} whether its leftmost edge holds comments not yet printed
*/
const leftEdgeComments = (value, printed, nodeAccess, tables) => {
for (let inner = value; inner; inner = nodeAccess.leftEdgeChild(inner)) {
const text = tables.commentsOf(nodeAccess.startToken(inner), false);
if (text && text.length !== 0 && !printed.has(text)) return true;
}
return false;
};
// An identifier terser may have to escape. It also requires a character
// past U+00FF, which the caller checks before asking.
const HIGH_IDENTIFIER = /^\p{ID_Start}\p{ID_Continue}*$/u;
/**
* @param {string} str any string
* @param {number} below the first code that is not left as written
* @returns {boolean} whether every character is printable ASCII below `below`
*/
const isPlain = (str, below) => {
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
if (code < 0x20 || code >= below) return false;
}
return true;
};
/**
* @param {string} str any string
* @returns {boolean} whether it holds any surrogate half
*/
const hasSurrogate = (str) => {
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
if (code >= 0xd800 && code <= 0xdfff) return true;
}
return false;
};
/**
* @param {number} codePoint a code point
* @returns {string} its escape
*/
const unicodeEscape = (codePoint) =>
codePoint <= 0xffff
? `\\u${codePoint.toString(16).padStart(4, "0")}`
: `\\u{${codePoint.toString(16)}}`;
/**
* @param {FormatSettings} options defaulted format options
* @returns {CommentFilter} which comments are printed
*/
const commentFilterFor = (options) => {
/** @type {CommentFilter} */
let filter = () => false;
if (options.comments) {
let comments = options.comments;
if (typeof comments === "string" && /^\/.*\/[a-zA-Z]*$/.test(comments)) {
const flagsAt = comments.lastIndexOf("/");
comments = new RegExp(
comments.slice(1, flagsAt),
comments.slice(flagsAt + 1)
);
}
if (comments instanceof RegExp) {
const pattern = comments;
filter = (comment) =>
comment.type !== "comment5" && pattern.test(comment.value);
} else if (typeof comments === "function") {
const callback = comments;
filter = function filter(comment) {
return (
comment.type !== "comment5" &&
callback(this, /** @type {KeptComment} */ (comment))
);
};
} else if (comments === "some") {
filter = (comment) =>
(comment.type === "comment2" || comment.type === "comment1") &&
IMPORTANT_COMMENT.test(comment.value);
} else {
filter = () => true;
}
}
if (options.preserve_annotations) {
const previous = filter;
filter = function filter(comment) {
// terser keeps any comment that names an annotation, prose included.
const annotation =
modules.corrections !== undefined && modules.corrections.enabled
? ONLY_ANNOTATION
: ANNOTATION;
return annotation.test(comment.value) || previous.call(this, comment);
};
}
return filter;
};
// What `append_comments` keeps of a list where nothing in it prints.
/** @type {Comment[]} */
const NO_KEPT_COMMENTS = [];
class MinifiedOutput {
/**
* @param {FormatSettings} options defaulted format options
* @param {boolean} readonly true when printed for its text alone, which drops every comment
* @param {NodeAccess} nodeAccess what comments are read off the printed nodes with
*/
constructor(options, readonly, nodeAccess) {
this.options = options;
this.readonly = readonly;
this.nodeAccess = nodeAccess;
this.commentFilter = commentFilterFor(options);
this.appendsComments =
!readonly && Boolean(options.comments || options.preserve_annotations);
this.sourceMap = options.source_map;
/** @type {string[]} */
this.parts = [];
this.partsLength = 0;
this.current = "";
/**
* Where set, takes the parts before the last two in batches, which
* then no longer read back, so text read once is never held whole.
* @type {((part: string) => void) | undefined}
*/
this.consumePart = undefined;
// Whether a comment, shebang or preamble was written: text terser's
// `print_to_string` leaves out.
this.wroteComments = false;
this.currentColumn = 0;
this.currentLine = 1;
this.currentPosition = 0;
this.hasParentheses = false;
this.mightNeedSpace = false;
this.mightNeedSemicolon = false;
this.needNewlineIndented = false;
this.needSpace = false;
this.lastPrinted = "";
/** @type {NodeToken | number | false | null | undefined} */
this.mappingToken = false;
// What the printed tree's token numbers are read through.
/** @type {TokenTables} */
this.tokenTables = NO_TOKEN_TABLES;
/** @type {string | false | undefined} */
this.mappingName = undefined;
/** @type {PrintNode[]} */
this.stack = [];
this.in_directive = false;
/** @type {PrintNode | null} */
this.use_asm = null;
/** @type {PrintNode | null} */
this.active_scope = null;
/** @type {Set<Comment | Comment[]>} */
this.printed_comments = new Set();
const { ascii_only: asciiOnly, ecma, safari10 } = options;
// Picked once per stream from the options, as terser's are.
/** @type {(str: string, identifier?: boolean, regexp?: boolean) => string} */
this.to_utf8 = asciiOnly
? (str, identifier = false, regexp = false) => {
if (isPlain(str, 0x7f)) return str;
if (ecma >= 2015 && !safari10 && !regexp) {
str = str.replace(
/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
(character) =>
`\\u{${getFullCharCode(character, 0).toString(16)}}`
);
}
return str.replace(/[^ -~]/g, (character) => {
let code = character.charCodeAt(0).toString(16);
if (code.length <= 2 && !identifier) {
while (code.length < 2) code = `0${code}`;
return `\\x${code}`;
}
while (code.length < 4) code = `0${code}`;
return `\\u${code}`;
});
}
: (str) => {
if (!hasSurrogate(str)) return str;
return str.replace(
/[\uD800-\uDBFF][\uDC00-\uDFFF]|([\uD800-\uDBFF]|[\uDC00-\uDFFF])/g,
(match, lone) =>
lone ? `\\u${lone.charCodeAt(0).toString(16)}` : match
);
};
this.identifierToUtf8 = asciiOnly
? this.to_utf8
: /** @type {(str: string) => string} */ (str) => {
if (isPlain(str, 0x100) || !HIGH_IDENTIFIER.test(str)) {
return str;
}
str = str.replace(
/[\uD800-\uDBFF][\uDC00-\uDFFF]|([\uD800-\uDBFF]|[\uDC00-\uDFFF])/g,
(match, lone) =>
lone
? `\\u${lone.charCodeAt(0).toString(16).padStart(4, "0")}`
: match
);
// Escape identifier characters from higher unicode versions.
let character = getFullChar(str, 0);
let escaped = character;
if (
isIdentifierStartBroad(character) &&
!isIdentifierStart(character)
) {
escaped = unicodeEscape(
/** @type {number} */ (character.codePointAt(0))
);
}
for (let i = character.length; i < str.length;) {
character = getFullChar(str, i);
escaped +=
isIdentifierCharBroad(character) && !isIdentifierChar(character)
? unicodeEscape(
/** @type {number} */ (character.codePointAt(0))
)
: character;
i += character.length;
}
return escaped;
};
/** @type {(str: string, quote?: string) => string} */
this.encode_string = (str, quote) => {
const encoded = this.makeString(str, quote);
if (
!options.inline_script ||
(!encoded.includes("<") && !encoded.includes("--"))
) {
return encoded;
}
return encoded
.replace(/<\u002F(script)([>/\t\n\f\r ])/gi, "<\\/$1$2")
.replace(/\u003C!--/g, "\\x3c!--")
.replace(/--\u003E/g, "--\\x3e");
};
}
/**
* @param {string} str a string's value
* @param {string=} quote the quote it was written with
* @returns {string} the string literal
*/
makeString(str, quote) {
const { options } = this;
let doubleQuotes = 0;
let singleQuotes = 0;
let needsEscaping = false;
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
if (code === 34) {
doubleQuotes++;
} else if (code === 39) {
singleQuotes++;
} else if (
code === 92 ||
code === 0 ||
(code >= 8 && code <= 13) ||
code === 0x2028 ||
code === 0x2029 ||
code === 0xfeff
) {
needsEscaping = true;
}
}
if (needsEscaping) {
const original = str;
str = str.replace(
/[\\\b\f\n\r\v\t\u0022\u0027\u2028\u2029\0\uFEFF]/g,
(character, i) => {
switch (character) {
case "\\":
return "\\\\";
case "\n":
return "\\n";
case "\r":
return "\\r";
case "\t":
return "\\t";
case "\b":
return "\\b";
case "\f":
return "\\f";
case "\u000B":
return options.ie8 ? "\\x0B" : "\\v";
case "\u2028":
return "\\u2028";
case "\u2029":
return "\\u2029";
case "\uFEFF":
return "\\ufeff";
case "\0":
return /[0-9]/.test(getFullChar(original, i + 1))
? "\\x00"
: "\\0";
}
return character;
}
);
}
str = this.to_utf8(str);
if (quote === "`") return `\`${str.split("`").join("\\`")}\``;
let single;
switch (options.quote_style) {
case 1:
single = true;
break;
case 2:
single = false;
break;
case 3:
single = quote === "'";
break;
default:
single = doubleQuotes > singleQuotes;
}
if (single) {
return singleQuotes === 0
? `'${str}'`
: `'${str.split("'").join("\\'")}'`;
}
return doubleQuotes === 0
? `"${str}"`
: `"${str.split('"').join('\\"')}"`;
}
/**
* @param {string} str what to append
* @returns {void}
*/
append(str) {
if (this.current.length > COMMIT_AFTER) {
const committed = this.current + str;
this.parts.push(committed);
this.partsLength += committed.length;
this.current = "";
// Joined in batches, as reading a flat string is far quicker than
// reading the concatenations a part is.
if (this.consumePart !== undefined && this.parts.length > 34) {
this.consumePart(this.parts.splice(0, 32).join(""));
}
} else {
this.current += str;
}
}
/**
* @param {number} index an offset into what was written
* @returns {number} the code there, NaN before the start
*/
codeAt(index) {
if (index >= this.partsLength) {
return this.current.charCodeAt(index - this.partsLength);
}
let end = this.partsLength;
for (let i = this.parts.length - 1; i >= 0; i--) {
const part = this.parts[i];
const begin = end - part.length;
if (index >= begin) return part.charCodeAt(index - begin);
end = begin;
}
return Number.NaN;
}
/**
* @returns {number} how much was written
*/
writtenLength() {
return this.partsLength + this.current.length;
}
/**
* @returns {boolean} whether a directive may open here: at the start, or after `;` or `{` and whitespace
*/
expectDirective() {
let n = this.writtenLength();
if (n <= 0) return true;
let code;
while ((code = this.codeAt(--n)) && (code === 32 || code === 10)) {
// skipping trailing whitespace
}
return !code || code === 59 || code === 123;
}
/**
* @returns {boolean} whether only spaces follow the last line break written
*/
hasNLB() {
let n = this.writtenLength() - 1;
while (n >= 0) {
const code = this.codeAt(n--);
if (code === 10) return true;
if (code !== 32) return false;
}
return true;
}
/**
* @param {string | number} value what to print, read as a string
* @returns {void}
*/
print(value) {
const str = String(value);
const { length } = str;
let character = "";
let code = -1;
if (length !== 0) {
code = str.charCodeAt(0);
character =
code >= 0xd800 && code <= 0xdfff ? getFullChar(str, 0) : str[0];
}
if (this.needNewlineIndented && length !== 0) {
this.needNewlineIndented = false;
if (character !== "\n") this.print("\n");
}
if (this.needSpace && length !== 0) {
this.needSpace = false;
if (
code < 128
? code !== 59 &&
code !== 125 &&
code !== 41 &&
code !== 32 &&
(code < 9 || code > 13)
: !/\s/.test(character)
) {
this.mightNeedSpace = true;
}
}
const last = this.lastPrinted;
const previous = last.length === 0 ? "" : last[last.length - 1];
if (this.mightNeedSemicolon) {
this.mightNeedSemicolon = false;
if (
(previous === ":" && character === "}") ||
((length === 0 || (character !== ";" && character !== "}")) &&
previous !== ";")
) {
if (this.options.semicolons || REQUIRE_SEMICOLON.has(character)) {
this.append(";");
this.currentColumn++;
this.currentPosition++;
} else {
if (this.currentColumn > 0) {
this.append("\n");
this.currentPosition++;
this.currentLine++;
this.currentColumn = 0;
}
// Nothing was written, so a later token still owes one.
if (/^\s+$/.test(str)) this.mightNeedSemicolon = true;
}
this.mightNeedSpace = false;
}
}
if (this.mightNeedSpace) {
if (
(isIdentifierCharacter(previous) &&
(isIdentifierCharacter(character) || character === "\\")) ||
(character === "/" && character === previous) ||
((character === "+" || character === "-") && character === last)
) {
this.append(" ");
this.currentColumn++;
this.currentPosition++;
}
this.mightNeedSpace = false;
}
if (this.mappingToken) {
this.addMapping(this.mappingToken, this.mappingName);
this.mappingToken = false;
}
this.append(str);
this.hasParentheses = length !== 0 && str.charCodeAt(length - 1) === 40;
this.currentPosition += length;
const firstBreak = str.indexOf("\n");
if (firstBreak === -1) {
this.currentColumn += length;
} else {
let breaks = 1;
let lastBreak = firstBreak;
for (
let at = str.indexOf("\n", firstBreak + 1);
at !== -1;
at = str.indexOf("\n", at + 1)
) {
breaks++;
lastBreak = at;
}
this.currentLine += breaks;
this.currentColumn = length - lastBreak - 1;
}
this.lastPrinted = str;
}
/**
* @param {NodeToken | number} token the token the next text maps back to, or its number
* @param {string | false | undefined} name the name it carries, or false for none
* @returns {void}
*/
addMapping(token, name) {
try {
token = /** @type {NodeToken} */ (this.tokenTables.of(token));
if (
name !== false &&
(token.type === "name" || token.type === "privatename")
) {
name = token.value;
}
/** @type {SourceMapBuilder} */ (this.sourceMap).add(
/** @type {string} */ (token.file),
this.currentLine,
this.currentColumn,
token.line,
token.col,
isBasicIdentifier(/** @type {string} */ (name))
? /** @type {string} */ (name)
: undefined
);
} catch (_err) {
// terser ignores a mapping it cannot add
}
}
/**
* @param {NodeToken | number | null | undefined} token the token the next text maps back to, or its number, none for a node the compressor made
* @param {(string | false)=} name the name it carries
* @returns {void}
*/
add_mapping(token, name) {
if (!this.sourceMap) return;
this.mappingToken = token;
this.mappingName = name;
}
/**
* @returns {string} what was written
*/
get() {
return this.parts.length === 0
? this.current
: this.parts.join("") + this.current;
}
/**
* @returns {string} what was written
*/
toString() {
return this.get();
}
/**
* @returns {void}
*/
indent() {}
/**
* @returns {void}
*/
newline() {}
/**
* @returns {number} the indentation, which minified output never changes
*/
indentation() {
return 0;
}
/**
* @returns {number} the width of the current line
*/
current_width() {
return this.currentColumn;
}
/**
* @returns {number | boolean} truthy once the line reaches `width`
*/
should_break() {
return this.options.width && this.current_width() >= this.options.width;
}
/**
* @returns {boolean} whether the last thing printed opened a parenthesis
*/
has_parens() {
return this.hasParentheses;
}
/**
* @returns {void}
*/
star() {
this.print("*");
}
/**
* @returns {void}
*/
space() {
this.mightNeedSpace = true;
}
/**
* @returns {void}
*/
comma() {
this.print(",");
this.mightNeedSpace = true;
}
/**
* @returns {void}
*/
colon() {
this.print(":");
this.mightNeedSpace = true;
}
/**
* @returns {string} the last string printed
*/
last() {
return this.lastPrinted;
}
/**
* @returns {void}
*/
semicolon() {
this.mightNeedSemicolon = true;
}
/**
* @returns {void}
*/
force_semicolon() {
this.mightNeedSemicolon = false;
this.print(";");
}
/**
* @param {string} name an identifier
* @returns {void}
*/
print_name(name) {
this.print(this.identifierToUtf8(name.toString(), true));
}
/**
* @param {string} str a string's value
* @param {string=} quote the quote it was written with
* @param {boolean=} escapeDirective whether it must not read as a directive
* @returns {void}
*/
print_string(str, quote, escapeDirective) {
const encoded = this.encode_string(str, quote);
if (escapeDirective === true && !encoded.includes("\\")) {
// Semicolons break the directive prologue.
if (!this.expectDirective()) this.force_semicolon();
this.force_semicolon();
}
this.print(encoded);
}
/**
* @param {string} str a template's raw characters
* @returns {void}
*/
print_template_string_chars(str) {
let encoded = this.encode_string(str, "`");
if (encoded.includes("${")) encoded = encoded.replace(/\${/g, "\\${");
this.print(encoded.slice(1, -1));
}
/**
* @returns {number} the indentation one level in
*/
next_indent() {
return this.options.indent_level;
}
/**
* @template T
* @param {number} column ignored, as minified output does not indent
* @param {() => T} content prints the content
* @returns {T} what `content` returned
*/
with_indent(column, content) {
return content();
}
/**
* @template T
* @param {() => T} content prints the content
* @returns {T} what `content` returned
*/
with_block(content) {
this.print("{");
const result = content();
this.print("}");
return result;
}
/**
* @template T
* @param {() => T} content prints the content
* @returns {T} what `content` returned
*/
with_parens(content) {
this.print("(");
const result = content();
this.print(")");
return result;
}
/**
* @template T
* @param {() => T} content prints the content
* @returns {T} what `content` returned
*/
with_square(content) {
this.print("[");
const result = content();
this.print("]");
return result;
}
/**
* @template {keyof FormatSettings} K
* @param {K} name a format option
* @returns {FormatSettings[K]} its value
*/
option(name) {
return this.options[name];
}
/**
* @param {Node} scope a function just printed
* @returns {void}
*/
gc_scope(scope) {
if (!this.options._destroy_ast) return;
/** @type {Node[]} */ (/** @type {Node} */ (scope.body).body).length = 0;
scope.params.length = 0;
}
/**
* @param {string} comment a comment's text
* @returns {string} what is printed of it
*/
filterComment(comment) {
if (!this.options.preserve_annotations) {
comment = comment.replace(ANNOTATION, " ");
}
if (/^\s*$/.test(comment)) return "";
return comment.replace(/(<\s*\/\s*)(script)/i, "<\\/$2");
}
/**
* @param {PrintNode} node the node about to be printed
* @returns {void}
*/
prepend_comments(node) {
if (this.readonly) return;
const { nodeAccess } = this;
const start = nodeAccess.startToken(node);
if (!start) return;
// There cannot be a newline between return/yield and its value.
const keywordWithValue = nodeAccess.keywordValue(node);
const tables = this.tokenTables;
const before = tables.commentsOf(start, false);
// WHY: terser records every token's comment list as printed, one Set
// entry per token. An empty list prints nothing and no reader tells it
// apart from an unrecorded one; only the start of output reads more.
if (
!keywordWithValue &&
this.currentPosition !== 0 &&
before !== undefined &&
before.length === 0
) {
return;
}
const printed = this.printed_comments;
// The same holds for a keyword's value: where neither the keyword nor
// the value's leftmost edge has comments left to print, none is made.
if (
keywordWithValue &&
this.currentPosition !== 0 &&
(before === undefined || before.length === 0 || printed.has(before)) &&
!leftEdgeComments(keywordWithValue, printed, nodeAccess, tables)
) {
return;
}
const startToken = tables.of(start);
if (!startToken) return;
if (before && printed.has(before)) {
if (!keywordWithValue) return;
startToken.comments_before = [];
}
/** @type {Comment[]} */
let comments = startToken.comments_before;
if (!comments) comments = startToken.comments_before = [];
printed.add(comments);
// terser walks the value's leftmost edge for comments a line break
// there would move after the keyword; one child per node is on it.
for (
let inner = keywordWithValue;
inner;
inner = nodeAccess.leftEdgeChild(inner)
) {
const text = tables.commentsOf(nodeAccess.startToken(inner), false);
if (text && !printed.has(text)) {
printed.add(text);
comments = [...comments, ...text];
}
}
if (this.currentPosition === 0) {
if (
comments.length > 0 &&
this.options.shebang &&
comments[0].type === "comment5" &&
!printed.has(comments[0])
) {
this.wroteComments = true;
this.print(`#!${/** @type {Comment} */ (comments.shift()).value}\n`);
}
const { preamble } = this.options;
if (preamble) {
this.wroteComments = true;
this.print(preamble.replace(/\r\n?|[\n\u2028\u2029]|\s*$/g, "\n"));
}
}
if (comments.length === 0) return;
// Most lists print nothing, so a list is made only for what prints.
/** @type {Comment[] | undefined} */
let kept;
for (let i = 0; i < comments.length; i++) {
const comment = comments[i];
if (this.commentFilter.call(node, comment) && !printed.has(comment)) {
if (kept === undefined) kept = [];
kept.push(comment);
}
}
if (kept === undefined) return;
this.wroteComments = true;
comments = kept;
let lastNlb = this.hasNLB();
let brokeLine = false;
for (let i = 0; i < comments.length; i++) {
const comment = comments[i];
printed.add(comment);
if (!lastNlb) {
if (comment.nlb) {
this.print("\n");
lastNlb = true;
} else if (i > 0) {
this.mightNeedSpace = true;
}
}
if (/comment[134]/.test(comment.type)) {
const value = this.filterComment(comment.value);
if (value) this.print(`//${value}\n`);
lastNlb = true;
} else if (comment.type === "comment2") {
const value = this.filterComment(comment.value);
if (value) this.print(`/*${value}*/`);
lastNlb = false;
brokeLine = value.includes("\n");
}
}
if (!lastNlb) {
// A comment holding a break is read back as breaking the line after it.
if (
startToken.nlb ||
(brokeLine &&
modules.corrections !== undefined &&
modules.corrections.enabled)
) {
this.print("\n");
} else {
this.mightNeedSpace = true;
}
}
}
/**
* @param {PrintNode} node the node just printed
* @param {boolean=} tail true for the comments inside an empty body
* @returns {void}
*/
append_comments(node, tail) {
if (!this.appendsComments) return;
const token = this.nodeAccess.endToken(node);
if (!token) return;
const printed = this.printed_comments;
/** @type {Comment[] | undefined} */
const comments = this.tokenTables.commentsOf(token, !tail);
if (!comments || printed.has(comments)) return;
if (!this.nodeAccess.isStatement(node)) {
for (let i = 0; i < comments.length; i++) {
if (/comment[134]/.test(comments[i].type)) return;
}
}
printed.add(comments);
// Most lists print nothing, so a list is made only for what prints.
/** @type {Comment[]} */
let kept = NO_KEPT_COMMENTS;
for (let i = 0; i < comments.length; i++) {
if (this.commentFilter.call(node, comments[i])) {
if (kept === NO_KEPT_COMMENTS) kept = [];
kept.push(comments[i]);
}
}
for (let i = 0; i < kept.length; i++) {
const comment = kept[i];
if (printed.has(comment)) continue;
printed.add(comment);
this.wroteComments = true;
this.needSpace = false;
if (this.needNewlineIndented) {
this.print("\n");
this.needNewlineIndented = false;
} else if (comment.nlb && (i > 0 || !this.hasNLB())) {
this.print("\n");
} else if (i > 0 || !tail) {
this.mightNeedSpace = true;
}
if (/comment[134]/.test(comment.type)) {
const value = this.filterComment(comment.value);
if (value) this.print(`//${value}`);
this.needNewlineIndented = true;
} else if (comment.type === "comment2") {
const value = this.filterComment(comment.value);
if (value) this.print(`/*${value}*/`);
this.needSpace = true;
}
}
}
/**
* @returns {number} the current line, from 1
*/
line() {
return this.currentLine;
}
/**
* @returns {number} the current column, from 0
*/
col() {
return this.currentColumn;
}
/**
* @returns {number} how many characters were written
*/
pos() {
return this.currentPosition;
}
/**
* @param {PrintNode} node the node being printed
* @returns {void}
*/
push_node(node) {
this.stack.push(node);
}
/**
* @returns {PrintNode} the node done printing
*/
pop_node() {
return /** @type {PrintNode} */ (this.stack.pop());
}
/**
* @param {number=} n how many levels further out
* @returns {PrintNode | undefined} the node holding the one being printed
*/
parent(n) {
return this.stack[this.stack.length - 2 - (n || 0)];
}
}
return MinifiedOutput;
};
/**
* Installs webpack's stream for minified output. terser prints a tree twice
* per minify — once for the mangler's character frequencies, once for the
* result — and both go through here.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installOutput = (modules) => {
// Shared with the phases after this one, which print into it.
modules.MinifiedOutput = createMinifiedOutput(modules);
};
/**
* terser's `Splice`: what a transform returns to put several nodes in its place.
* @template [T=NodeShape]
*/
class Splice {
/**
* @param {T[]} value the nodes
*/
constructor(value) {
this.v = value;
}
}
// A child as a walk lists it: `?` where it may be absent, `~` where it may not
// be a node, `^` a key reached where computed, `*` where it is a list, `+` one
// holding null for a hole, `f.body` a block's list.
const NODE_CHILD = /^(\?)?([*~+^])?([A-Za-z_]+(?:\.body)?)$/;
/**
* How a node's children are walked: in order, handing each to a visitor, and
* last first, pushing each onto the stack of a walk that pops them.
* @typedef {object} Traversal
* @property {((node: Node, visitor: Visitor) => void) | undefined} descend walks the node's children, undefined where it has none to walk
* @property {((node: Node, push: (node: Node) => void) => void) | undefined} backwards pushes them last first, undefined where it has none
*/
/**
* What a walk hands each node to: terser's `TreeWalker`, or a visitor of its
* own, its children walked by `descend` where it has any.
* @typedef {object} Visitor
* @property {(node: Node, descend?: (node: Node, visitor: Visitor) => void) => boolean | void} _visit visits a node, answering whether the walk stops
*/
/**
* A visitor tracking the nodes it is in, as a walk of a class's parts needs.
* @internal
* @typedef {Visitor & Pick<Walker, "push" | "pop">} StackVisitor
*/
/**
* The directives in force where a walk is, by their text, each the node
* declaring it; a layer a function or class adds names it under a symbol.
* @internal
* @typedef {{ [directive: string]: Node | true, [owner: symbol]: Node }} Directives
*/
/**
* terser's `TreeWalker` or `TreeTransformer`, a plain object `createWalker`
* makes: the visit or the transform, the nodes the one visited is in, and the
* directives in force there.
* @internal
* @typedef {object} Walker
* @property {WalkVisit | undefined} visit what a walk hands each node to
* @property {TransformBefore | undefined} before what a transform hands each node to on entering it
* @property {TransformAfter | undefined} after what a transform hands each node to on leaving it
* @property {Node[]} stack the nodes the one visited is in, it last
* @property {Directives} directives the directives in force
* @property {boolean} webpackSkipsSqueezed whether a node the compressor squeezed is returned as it is
* @property {boolean} webpackCopiesLists whether a list is copied before it is transformed
* @property {boolean} webpackDefunsFirst whether a block's functions are transformed before its other statements
* @property {((node: Node, visitor: Visitor) => void) | undefined} webpackDescend what walks the children of the node visited
* @property {(() => void) | undefined} webpackDescendCurrent `webpackDescend`, handed the node visited
* @property {(node: Node, descend?: (node: Node, visitor: Visitor) => void) => boolean | void} _visit visits a node, answering whether the walk stops
* @property {(node: Node) => void} push enters a node
* @property {() => void} pop leaves the node entered last
* @property {(n?: number) => Node} parent a parent of the node visited, the nearest where `n` is 0, none past the root
* @property {() => Node} self the node visited
* @property {(test: (node: Node) => boolean) => Node | undefined} find_parent the nearest node on the stack passing a test
* @property {() => boolean} is_within_loop whether the node visited can run more than once in its function
* @property {() => Scope | undefined} find_scope the scope the node visited declares into
* @property {(directive: string) => Node | true | undefined} has_directive the directive, where it is in force
* @property {(node: Node) => Node | undefined} loopcontrol_target the statement a `break` or `continue` leaves or continues
*/
/**
* The walker `reduce_vars` marks with: which definitions are safe to read
* where it is, by id, and the loop it is in.
* @internal
* @typedef {Walker & { safe_ids: Record<number, boolean>, in_loop: Node | null, loop_ids: Map<number, Node | null>, defs_to_safe_ids: Map<number, Record<number, boolean>> }} ReduceWalker
*/
/** @typedef {(this: Walker, node: Node, descend: () => void) => boolean | void} WalkVisit what a walk hands each node to, a truthy result skipping its children */
/** @typedef {(this: Walker, node: Node, descend: (node: Node, walker: Walker) => void, inList: boolean | undefined) => Node | Splice<Node> | SkipMarker | null | void} TransformBefore what a transform hands each node to on entering it, what it returns replacing it */
/** @typedef {(this: Walker, node: Node, inList: boolean | undefined) => Node | Splice<Node> | SkipMarker | null | void} TransformAfter what a transform hands each node to on leaving it, what it returns replacing it */
/** @typedef {Record<string, never>} SkipMarker `MAP.skip`, which a transform returns to drop a node from its list */
/**
* The statements reaching each child of a walk list, one each.
* @param {string[]} children the children, in the order they are reached
* @param {(value: string) => string} reach the statement reaching one node
* @param {boolean} backwards whether a list is reached last element first
* @returns {string} the statements
*/
const reachChildren = (children, reach, backwards) =>
children
.map((child, index) => {
const [, optional, kind, field] = /** @type {RegExpExecArray} */ (
NODE_CHILD.exec(child)
);
const value = `node.${field}`;
let statement;
if (kind === "*") {
statement = backwards
? `{ var i${index} = ${value}.length; while (i${index}--) ${reach(`${value}[i${index}]`)} }`
: `{ var list${index} = ${value}; for (var i${index} = 0, len${index} = list${index}.length; i${index} < len${index}; i${index}++) ${reach(`list${index}[i${index}]`)} }`;
} else if (kind === "+") {
statement = backwards
? `{ var i${index} = ${value}.length; while (i${index}--) if (${value}[i${index}] !== null) ${reach(`${value}[i${index}]`)} }`
: `{ var list${index} = ${value}; for (var i${index} = 0, len${index} = list${index}.length; i${index} < len${index}; i${index}++) if (list${index}[i${index}] !== null) ${reach(`list${index}[i${index}]`)} }`;
} else if (kind === "~") {
statement = `if (isSyntaxNode(${value})) ${reach(value)}`;
} else if (kind === "^") {
statement = `if (node.computed) ${reach(value)}`;
} else {
statement = reach(value);
}
return optional ? `if (${value}) ${statement}` : statement;
})
.join("\n");
/**
* Builds the walks and transforms keyed by each node's ESTree `type`: its
* children in terser's order, chosen per node where one type holds nodes
* terser walked apart, such as a directive and an expression statement.
* @param {TerserAst} ast the node classes, read for their predicates
* @returns {void}
*/
const installTraversal = (ast) => {
const TRAVERSAL_KEY = Symbol("traversal");
const DESCEND_KEY = Symbol("descend");
const { isDefunNode, isDirectiveNode, isSyntaxNode, NumberNode } = ast;
/**
* @param {string[]} walk the children as a walk reaches them
* @param {string[]} backwards the children as a backwards push reaches them, last first
* @returns {Traversal} the traversal
*/
const compile = (walk, backwards) => ({
// eslint-disable-next-line no-new-func
descend: new Function(
"isSyntaxNode",
"walkNode",
`"use strict"; return function descend(node, visitor) {${reachChildren(walk, (value) => `walkNode(${value}, visitor);`, false)}};`
)(isSyntaxNode, walkNode),
// eslint-disable-next-line no-new-func
backwards: new Function(
"isSyntaxNode",
`"use strict"; return function backwards(node, push) {${reachChildren(backwards, (value) => `push(${value});`, true)}};`
)(isSyntaxNode)
});
/**
* terser's walk: the node handed to the visitor, with what walks its
* children.
* @param {Node} node the node
* @param {Visitor} visitor the walker
* @returns {boolean | void} whether the visitor stopped the walk
*/
const walkNode = (node, visitor) => {
const traversal = traversalOf(node);
return visitor._visit(
node,
traversal === undefined ? undefined : traversal.descend
);
};
/** @type {Traversal} */
const LEAF = { descend: undefined, backwards: undefined };
const ARGUMENT = compile(["argument"], ["argument"]);
const OPTIONAL_ARGUMENT = compile(["argument"], ["?argument"]);
const LABEL = compile(["label"], ["?label"]);
const BLOCK = compile(["*body"], ["*body"]);
const LAMBDA = compile(
["?id", "*params", "*body.body"],
["*body.body", "*params", "?id"]
);
const CLASS = compile(
["?id", "?superClass", "*body.body"],
["*body.body", "?superClass", "?id"]
);
const OPERANDS = compile(["left", "right"], ["right", "left"]);
const FOR_IN = compile(
["left", "right", "body"],
["body", "?right", "?left"]
);
const NAME_MAPPING = compile(
["?imported", "?exported", "local"],
["local", "?exported", "?imported"]
);
const EXPORT = compile(
["?exported_definition", "?exported_value", "?*exported_names", "?source"],
["?source", "?*exported_names", "?exported_value", "?exported_definition"]
);
const CALL = compile(["*arguments", "callee"], ["*arguments", "callee"]);
const DESTRUCTURING = compile(
["?+elements", "?*properties"],
["?*properties", "?+elements"]
);
const EXPRESSION = compile(["expression"], ["expression"]);
const KEY_VALUE = compile(["^key", "value"], ["value", "^key"]);
const MEMBER = compile(["~key", "value"], ["value", "~key"]);
const FIELD = compile(["~key", "~value"], ["~value", "~key"]);
const PRIVATE_FIELD = compile(["~value"], ["~value"]);
const DOT = compile(["object"], ["object"]);
const SUB = compile(["object", "property"], ["property", "object"]);
const CASE = compile(["test", "*consequent"], ["*consequent", "test"]);
const DEFAULT = compile(["*consequent"], ["*consequent"]);
/**
* Each type's traversal, or the function choosing one by what a node holds.
* @type {Record<string, Traversal | ((node: Node) => Traversal)>}
*/
const TRAVERSALS = {
DebuggerStatement: LEAF,
ExpressionStatement: EXPRESSION,
BlockStatement: BLOCK,
EmptyStatement: LEAF,
LabeledStatement: compile(["label", "body"], ["body", "label"]),
DoWhileStatement: compile(["body", "test"], ["test", "body"]),
WhileStatement: compile(["test", "body"], ["body", "test"]),
ForStatement: compile(
["?init", "?test", "?update", "body"],
["body", "?update", "?test", "?init"]
),
ForInStatement: FOR_IN,
ForOfStatement: FOR_IN,
WithStatement: compile(["object", "body"], ["body", "object"]),
Program: BLOCK,
FunctionExpression: LAMBDA,
ArrowFunctionExpression: LAMBDA,
FunctionDeclaration: LAMBDA,
ClassDeclaration: CLASS,
ClassExpression: CLASS,
ClassBody: BLOCK,
StaticBlock: BLOCK,
TaggedTemplateExpression: compile(["tag", "quasi"], ["quasi", "tag"]),
TemplateLiteral: compile(["*segments"], ["*segments"]),
TemplateElement: LEAF,
// A `return`, `throw`, `yield`, `break` or `continue` with nothing after
// it has no children to walk.
ReturnStatement: (node) => (node.argument ? OPTIONAL_ARGUMENT : LEAF),
ThrowStatement: (node) => (node.argument ? OPTIONAL_ARGUMENT : LEAF),
BreakStatement: (node) => (node.label ? LABEL : LEAF),
ContinueStatement: (node) => (node.label ? LABEL : LEAF),
AwaitExpression: ARGUMENT,
YieldExpression: (node) => (node.argument ? OPTIONAL_ARGUMENT : LEAF),
IfStatement: compile(
["test", "consequent", "?alternate"],
["?alternate", "consequent", "test"]
),
SwitchStatement: compile(
["discriminant", "*cases"],
["*cases", "discriminant"]
),
SwitchCase: DEFAULT,
TryStatement: compile(
["block", "?handler", "?finalizer"],
["?finalizer", "?handler", "block"]
),
CatchClause: compile(["?param", "*body.body"], ["*body.body", "?param"]),
VariableDeclaration: compile(["*declarations"], ["*declarations"]),
VariableDeclarator: compile(["id", "?init"], ["?init", "id"]),
ImportSpecifier: NAME_MAPPING,
ImportNamespaceSpecifier: NAME_MAPPING,
ExportSpecifier: NAME_MAPPING,
ImportDeclaration: compile(
["?imported_name", "?*imported_names", "source"],
["source", "?*imported_names", "?imported_name"]
),
MetaProperty: LEAF,
ImportExpression: compile(["*args"], ["*args"]),
ExportAllDeclaration: EXPORT,
ExportDefaultDeclaration: EXPORT,
ExportNamedDeclaration: EXPORT,
CallExpression: CALL,
NewExpression: CALL,
SequenceExpression: compile(["*expressions"], ["*expressions"]),
MemberExpression: DOT,
ChainExpression: EXPRESSION,
UnaryExpression: ARGUMENT,
UpdateExpression: ARGUMENT,
BinaryExpression: OPERANDS,
LogicalExpression: OPERANDS,
AssignmentExpression: OPERANDS,
AssignmentPattern: OPERANDS,
ConditionalExpression: compile(
["test", "consequent", "alternate"],
["alternate", "consequent", "test"]
),
ArrayExpression: compile(["+elements"], ["+elements"]),
ObjectExpression: compile(["*properties"], ["*properties"]),
Property: MEMBER,
MethodDefinition: MEMBER,
PropertyDefinition: FIELD,
ArrayPattern: DESTRUCTURING,
ObjectPattern: DESTRUCTURING,
RestElement: ARGUMENT,
SpreadElement: ARGUMENT
};
// The kinds terser walked apart from the rest of their type, by its class.
/** @type {Record<string, Traversal>} */
const KIND_TRAVERSALS = {
Case: CASE,
ClassPrivateProperty: PRIVATE_FIELD,
Directive: LEAF,
ObjectKeyVal: KEY_VALUE,
Sub: SUB
};
/**
* @param {Node} node a node
* @returns {Traversal | undefined} how its children are walked
*/
const traversalOf = (node) => {
let traversal =
/** @type {Node & { [TRAVERSAL_KEY]: Traversal | ((node: Node) => Traversal) | undefined }} */ (
node
)[TRAVERSAL_KEY];
if (traversal === undefined) traversal = TRAVERSALS[node.type];
return typeof traversal === "function" ? traversal(node) : traversal;
};
/**
* Pushes a node's children last first, as terser's `_children_backwards`.
* @param {Node} node the node
* @param {(node: Node) => void} push what each child is handed to
* @returns {void}
*/
const pushChildrenBackwards = (node, push) => {
const traversal = traversalOf(node);
if (traversal !== undefined && traversal.backwards !== undefined) {
traversal.backwards(node, push);
}
};
/**
* terser's `MAP`: each node of a list transformed, dropping what a transform
* skips and spreading what it splices.
* @param {Node[]} array the nodes
* @param {Walker} tw the transformer
* @param {boolean=} allowSplicing whether a transform may splice
* @returns {Node[]} the nodes transformed
*/
const doList = (array, tw, allowSplicing = true) => {
// Made at the first node replaced; a list nothing replaces comes back as
// is, but to a deep copy, which may hold no list of the original's.
/** @type {Node[] | undefined} */
let result;
for (let i = 0; i < array.length; ++i) {
const item = array[i];
const transformed = /** @type {Node | Splice<Node> | SkipMarker} */ (
transformNode(item, tw, allowSplicing)
);
if (result === undefined) {
if (transformed === item) continue;
result = array.slice(0, i);
}
if (isSyntaxNode(transformed)) {
result.push(transformed);
} else if (transformed instanceof Splice) {
result.push(...transformed.v);
}
}
if (result !== undefined) return result;
return tw.webpackCopiesLists === true ? [...array] : array;
};
/**
* `doList` over a list holding null for a hole, each hole kept in place.
* @param {(Node | null)[]} list the nodes and holes
* @param {Walker} tw the transformer
* @returns {(Node | null)[]} them transformed
*/
const doListWithHoles = (list, tw) => {
if (!list.includes(null)) return doList(/** @type {Node[]} */ (list), tw);
/** @type {(Node | null)[]} */
const result = [];
/** @type {Node[]} */
let run = [];
for (const item of list) {
if (item !== null) {
run.push(item);
continue;
}
if (run.length > 0) result.push(...doList(run, tw));
result.push(null);
run = [];
}
if (run.length > 0) result.push(...doList(run, tw));
return result;
};
/** @typedef {(self: Node, tw: Walker) => void} Descend */
/** @type {Descend} */
const descendNothing = () => {};
/**
* Records each global a node reads as enclosed by every scope out from the
* read: terser records only names a scope around declares, which a callee
* squeezed into its caller before the caller moves would leave unchecked.
* @param {Node | Splice<Node>} node the node
* @returns {void}
*/
const encloseGlobals = (node) => {
if (!isSyntaxNode(node)) return;
ast.walk(node, (/** @type {Node} */ child) => {
if (
ast.isSymbolRefNode(child) &&
child.definition &&
child.definition.undeclared &&
child.scope
) {
child.scope.markEnclosed(child.definition);
}
});
};
/**
* Whether a function declaration may be squeezed before its caller: it holds
* no function or class and reads no other declaration of the list. One that
* takes in a function first is then kept from moving into its caller itself.
* @param {Node} declaration the declaration
* @param {Map<Node, Node>} declarations the list's declarations, by their names' nodes
* @returns {boolean} true for such a leaf
*/
const isLeafDeclaration = (declaration, declarations) =>
!ast.walk(declaration, (/** @type {Node} */ node) =>
(node !== declaration &&
(ast.isLambdaNode(node) || ast.isClassNode(node))) ||
(ast.isSymbolRefNode(node) &&
node.definition &&
declarations.has(node.definition.identifiers[0]))
? ast.walk_abort
: undefined
);
/**
* `doList` over statements, where a function declaration read by one
* declared before it is transformed first, so a call there reads its body squeezed.
* @param {Node[]} statements the statements
* @param {Walker} tw the transformer
* @returns {Node[]} them transformed
*/
const doStatements = (statements, tw) => {
if (tw.webpackDefunsFirst !== true) return doList(statements, tw);
// Each declaration after the first statement, by its name's node.
/** @type {Map<Node, Node> | undefined} */
let found;
for (let i = 1; i < statements.length; i++) {
const statement = statements[i];
if (isDefunNode(statement) && statement.id) {
if (found === undefined) found = new Map();
found.set(statement.id, statement);
}
}
if (found === undefined) return doList(statements, tw);
const declarations = found;
// A transform may drop a statement from the list as it runs, so each is
// found by itself rather than by its index.
/** @type {Map<Node, Node | Splice<Node>>} */
const done = new Map();
/**
* @param {Node} statement a statement
* @returns {Node | Splice<Node>} it transformed
*/
const transform = (statement) => {
const finished = done.get(statement);
if (finished !== undefined) return finished;
if (isDefunNode(statement)) {
// Marked before its callees, so a recursive pair ends.
done.set(statement, statement);
/** @type {Node[]} */
const callees = [];
ast.walk(statement, (/** @type {Node} */ node) => {
if (!ast.isSymbolRefNode(node) || !node.definition) return;
const callee = declarations.get(node.definition.identifiers[0]);
if (
callee !== undefined &&
!done.has(callee) &&
!callees.includes(callee) &&
isLeafDeclaration(callee, declarations)
) {
callees.push(callee);
}
});
for (const callee of callees) transform(callee);
if (callees.length > 0) {
const transformed = transformNode(statement, tw, true);
encloseGlobals(transformed);
done.set(statement, transformed);
return transformed;
}
}
const transformed = transformNode(statement, tw, true);
done.set(statement, transformed);
return transformed;
};
/** @type {Node[]} */
const result = [];
for (let i = 0; i < statements.length; ++i) {
const transformed = transform(statements[i]);
if (transformed instanceof Splice) {
result.push(...transformed.v);
} else if (isSyntaxNode(transformed)) {
result.push(transformed);
}
}
return result;
};
/** @type {Descend} */
const descendBlock = (self, tw) => {
self.body = doStatements(/** @type {Node[]} */ (self.body), tw);
};
/** @type {Descend} */
const descendLambda = (self, tw) => {
if (self.id) self.id = transformNode(self.id, tw);
self.params = doList(self.params, tw, false);
/** @type {Node} */ (self.body).body = doStatements(
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body),
tw
);
};
/** @type {Descend} */
const descendClass = (self, tw) => {
if (self.id) self.id = transformNode(self.id, tw);
if (self.superClass) self.superClass = transformNode(self.superClass, tw);
/** @type {Node} */ (self.body).body = doList(
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body),
tw
);
};
/** @type {Descend} */
const descendObject = (self, tw) => {
self.object = transformNode(self.object, tw);
};
/** @type {Descend} */
const descendConsequent = (self, tw) => {
self.consequent = doList(/** @type {Node[]} */ (self.consequent), tw);
};
/** @type {Descend} */
const descendArgument = (self, tw) => {
self.argument = transformNode(self.argument, tw);
};
/** @type {Descend} */
const descendOptionalArgument = (self, tw) => {
if (self.argument) self.argument = transformNode(self.argument, tw);
};
/** @type {Descend} */
const descendLabel = (self, tw) => {
if (self.label) self.label = transformNode(self.label, tw);
};
/** @type {Descend} */
const descendOperands = (self, tw) => {
self.left = transformNode(self.left, tw);
self.right = transformNode(self.right, tw);
};
/** @type {Descend} */
const descendForIn = (self, tw) => {
self.left = transformNode(self.left, tw);
self.right = transformNode(self.right, tw);
self.body = transformNode(/** @type {Node} */ (self.body), tw);
};
/** @type {Descend} */
const descendMember = (self, tw) => {
if (isSyntaxNode(self.key)) self.key = transformNode(self.key, tw);
if (self.value) {
self.value = transformNode(/** @type {Node} */ (self.value), tw);
}
};
/** @type {Descend} */
const descendNameMapping = (self, tw) => {
if (self.imported) self.imported = transformNode(self.imported, tw);
if (self.exported) self.exported = transformNode(self.exported, tw);
self.local = transformNode(self.local, tw);
};
/** @type {Descend} */
const descendExport = (self, tw) => {
if (self.exported_definition) {
self.exported_definition = transformNode(self.exported_definition, tw);
}
if (self.exported_value) {
self.exported_value = transformNode(self.exported_value, tw);
}
if (self.exported_names) doList(self.exported_names, tw);
if (self.source) self.source = transformNode(self.source, tw);
};
/** @type {Descend} */
const descendCall = (self, tw) => {
self.callee = transformNode(self.callee, tw);
self.arguments = doList(self.arguments, tw, false);
};
/** @type {Descend} */
const descendDestructuring = (self, tw) => {
if (self.is_array) {
self.elements = doListWithHoles(self.elements, tw);
} else {
self.properties = doList(self.properties, tw);
}
};
/**
* Each type's children as terser's `transform.js` replaces them.
* @type {Record<string, Descend>}
*/
const DESCENDS = {
DebuggerStatement: descendNothing,
ExpressionStatement: (self, tw) => {
self.expression = transformNode(self.expression, tw);
},
BlockStatement: descendBlock,
EmptyStatement: descendNothing,
LabeledStatement: (self, tw) => {
self.label = transformNode(self.label, tw);
self.body = transformNode(/** @type {Node} */ (self.body), tw);
},
DoWhileStatement: (self, tw) => {
self.body = transformNode(/** @type {Node} */ (self.body), tw);
self.test = transformNode(self.test, tw);
},
WhileStatement: (self, tw) => {
self.test = transformNode(self.test, tw);
self.body = transformNode(/** @type {Node} */ (self.body), tw);
},
ForStatement: (self, tw) => {
if (self.init) self.init = transformNode(self.init, tw);
if (self.test) self.test = transformNode(self.test, tw);
if (self.update) self.update = transformNode(self.update, tw);
self.body = transformNode(/** @type {Node} */ (self.body), tw);
},
ForInStatement: descendForIn,
ForOfStatement: descendForIn,
WithStatement: (self, tw) => {
self.object = transformNode(self.object, tw);
self.body = transformNode(/** @type {Node} */ (self.body), tw);
},
Program: descendBlock,
FunctionExpression: descendLambda,
ArrowFunctionExpression: descendLambda,
FunctionDeclaration: descendLambda,
ClassDeclaration: descendClass,
ClassExpression: descendClass,
ClassBody: descendNothing,
StaticBlock: descendBlock,
TaggedTemplateExpression: (self, tw) => {
self.tag = transformNode(self.tag, tw);
self.quasi = transformNode(self.quasi, tw);
},
TemplateLiteral: (self, tw) => {
self.segments = doList(self.segments, tw);
},
TemplateElement: descendNothing,
ReturnStatement: descendOptionalArgument,
ThrowStatement: descendOptionalArgument,
BreakStatement: descendLabel,
ContinueStatement: descendLabel,
AwaitExpression: descendArgument,
YieldExpression: descendOptionalArgument,
IfStatement: (self, tw) => {
self.test = transformNode(self.test, tw);
self.consequent = transformNode(
/** @type {Node} */ (self.consequent),
tw
);
if (self.alternate) self.alternate = transformNode(self.alternate, tw);
},
SwitchStatement: (self, tw) => {
self.discriminant = transformNode(self.discriminant, tw);
self.cases = doList(self.cases, tw);
},
SwitchCase: descendConsequent,
TryStatement: (self, tw) => {
self.block = transformNode(self.block, tw);
if (self.handler) self.handler = transformNode(self.handler, tw);
if (self.finalizer) self.finalizer = transformNode(self.finalizer, tw);
},
CatchClause: (self, tw) => {
if (self.param) self.param = transformNode(self.param, tw);
/** @type {Node} */ (self.body).body = doList(
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body),
tw
);
},
VariableDeclaration: (self, tw) => {
self.declarations = doList(self.declarations, tw);
},
VariableDeclarator: (self, tw) => {
self.id = transformNode(self.id, tw);
if (self.init) self.init = transformNode(self.init, tw);
},
ImportSpecifier: descendNameMapping,
ImportNamespaceSpecifier: descendNameMapping,
ExportSpecifier: descendNameMapping,
ImportDeclaration: (self, tw) => {
if (self.imported_name) {
self.imported_name = transformNode(self.imported_name, tw);
}
if (self.imported_names) doList(self.imported_names, tw);
self.source = transformNode(self.source, tw);
},
MetaProperty: descendNothing,
ImportExpression: (self, tw) => {
self.args = doList(self.args, tw);
},
ExportAllDeclaration: descendExport,
ExportDefaultDeclaration: descendExport,
ExportNamedDeclaration: descendExport,
CallExpression: descendCall,
NewExpression: descendCall,
SequenceExpression: (self, tw) => {
const result = doList(self.expressions, tw);
self.expressions = result.length ? result : [NumberNode({ value: 0 })];
},
MemberExpression: descendObject,
ChainExpression: (self, tw) => {
self.expression = transformNode(self.expression, tw);
},
UnaryExpression: descendArgument,
UpdateExpression: descendArgument,
BinaryExpression: descendOperands,
LogicalExpression: descendOperands,
AssignmentExpression: descendOperands,
AssignmentPattern: descendOperands,
ConditionalExpression: (self, tw) => {
self.test = transformNode(self.test, tw);
self.consequent = transformNode(
/** @type {Node} */ (self.consequent),
tw
);
self.alternate = transformNode(self.alternate, tw);
},
ArrayExpression: (self, tw) => {
self.elements = doListWithHoles(self.elements, tw);
},
ObjectExpression: (self, tw) => {
self.properties = doList(self.properties, tw);
},
Property: descendMember,
MethodDefinition: descendMember,
PropertyDefinition: descendMember,
ArrayPattern: descendDestructuring,
ObjectPattern: descendDestructuring,
RestElement: descendArgument,
SpreadElement: descendArgument
};
// The kinds terser transformed apart from the rest of their type, by its class.
/** @type {Record<string, Descend>} */
const KIND_DESCENDS = {
Case: (self, tw) => {
self.test = transformNode(self.test, tw);
descendConsequent(self, tw);
},
Directive: descendNothing,
ObjectKeyVal: (self, tw) => {
if (self.computed) self.key = transformNode(self.key, tw);
if (self.value) {
self.value = transformNode(/** @type {Node} */ (self.value), tw);
}
},
Sub: (self, tw) => {
descendObject(self, tw);
self.property = transformNode(self.property, tw);
}
};
/**
* terser's `transform`: `before` may replace the node, else its children are
* transformed and `after` may replace it. The compressor skips a node it
* already squeezed, but a directive, whose push records it.
* @param {Node} node the node
* @param {Walker} tw the transformer
* @param {boolean=} inList whether a list holds it, so it may splice
* @returns {Node} what replaces it, or a splice or nothing where a list holds it
*/
const transformNode = (node, tw, inList) => {
if (
tw.webpackSkipsSqueezed === true &&
(node.flags & COMPRESSOR_FLAGS.SQUEEZED) !== 0 &&
!isDirectiveNode(node)
) {
return node;
}
let descend = /** @type {Node & { [DESCEND_KEY]: Descend | undefined }} */ (
node
)[DESCEND_KEY];
if (descend === undefined) descend = DESCENDS[node.type] || descendNothing;
let transformed;
tw.push(node);
if (tw.before) transformed = tw.before(node, descend, inList);
if (transformed === undefined) {
transformed = node;
descend(transformed, tw);
if (tw.after) {
const replaced = tw.after(transformed, inList);
if (replaced !== undefined) transformed = replaced;
}
}
tw.pop();
return /** @type {Node} */ (transformed);
};
/**
* Transforms a node's children, but not the node itself.
* @param {Node} node the node
* @param {Walker} tw the transformer
* @returns {void}
*/
const descendNode = (node, tw) => {
let descend = /** @type {Node & { [DESCEND_KEY]: Descend | undefined }} */ (
node
)[DESCEND_KEY];
if (descend === undefined) descend = DESCENDS[node.type] || descendNothing;
descend(node, tw);
};
// Each kind's prototype holds its type's traversal and descent, so a node
// finds them in one property read; only a plain object looks its type up.
for (const [prototypeKey, prototype] of NODE_KIND_PROTOTYPES) {
const type = /** @type {string} */ (prototype.type);
const kind = prototypeKey.slice(0, prototypeKey.indexOf("|"));
const ancestry = NODE_KIND_ANCESTRY[kind];
const walkedApart = ancestry.find((name) => hasOwn(KIND_TRAVERSALS, name));
/** @type {{ type: string, [TRAVERSAL_KEY]: Traversal | ((node: Node) => Traversal) | undefined }} */ (
prototype
)[TRAVERSAL_KEY] =
walkedApart === undefined
? TRAVERSALS[type] || LEAF
: KIND_TRAVERSALS[walkedApart];
const transformedApart = ancestry.find((name) =>
hasOwn(KIND_DESCENDS, name)
);
/** @type {{ type: string, [DESCEND_KEY]: Descend | undefined }} */ (
prototype
)[DESCEND_KEY] =
transformedApart === undefined
? DESCENDS[type] || descendNothing
: KIND_DESCENDS[transformedApart];
}
ast.walkNode = walkNode;
ast.pushChildrenBackwards = pushChildrenBackwards;
ast.transformNode = transformNode;
ast.descendNode = descendNode;
};
/**
* Installs webpack's own copies of the methods terser's `ast.js` writes by hand
* on its node classes and its `TreeWalker`: cloning, computed keys, the names
* a declaration binds, and the walker's look-ups of its stack.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installNodes = (modules) => {
const { descendNode, transformNode, walkNode } = modules.ast;
const { ast, parse, utils } = modules;
const {
isBlockNode,
isIterationStatementNode,
isLabeledStatementNode,
isScopeNode,
isArrowNode,
isClassPrivatePropertyNode,
isClassPropertyNode,
isClassStaticBlockNode,
isConciseMethodNode,
isDefaultAssignNode,
isDestructuringNode,
isDirectiveNode,
isExpansionNode,
isLambdaNode,
isLoopControlNode,
isPrivateMethodNode,
isReturnNode,
isSymbolDeclarationNode,
isSymbolFunargNode,
isSymbolRefNode,
isThisNode,
createTransformer,
createWalker
} = ast;
/**
* terser's `_clone`: a copy of a node, its children copied too where deep.
* @param {Node} self the node
* @param {boolean=} deep whether its children are cloned too
* @returns {Node} the copy
*/
const copyNode = (self, deep) => {
if (deep) {
const copy = ast.cloneNode(self);
descendNode(copy, deepCopier);
return copy;
}
return ast.factoryOf(self)(self);
};
// One transformer replaces every child of every deep copy with its own deep
// copy, so a copy allocates no transformer per node.
const deepCopier = createTransformer((/** @type {Node} */ node) =>
ast.cloneNode(node, true)
);
deepCopier.webpackCopiesLists = true;
/**
* terser's `clone`: a copy of a node, taking a copy of what it alone may
* hold, a block's scope, a scope's variables, a label's references.
* @param {Node} self the node, or a block's scope
* @param {boolean=} deep whether its children are cloned too
* @param {Node=} toplevel the toplevel to work a scope's scope out against
* @returns {Node} the copy
*/
ast.cloneNode = (self, deep, toplevel) => {
const copy = copyNode(self, deep);
if (isLabeledStatementNode(self)) {
// A copy whose `break` and `continue` reach the copied label.
if (deep) {
const { label } = copy;
const definition = self.label;
walkNode(
copy,
createWalker((/** @type {Node} */ node) => {
if (
isLoopControlNode(node) &&
node.label &&
/** @type {Definition | Node} */ (node.label.definition) ===
definition
) {
node.label.definition = /** @type {Definition} */ (
/** @type {Definition | Node} */ (label)
);
label.references.push(node);
}
})
);
}
} else if (isScopeNode(self) && !isClassStaticBlockNode(self)) {
const scope = self.block_scope;
if (deep && scope && toplevel) {
ast.figureOutScope(copy, {}, { toplevel, parent_scope: scope.upper });
} else if (scope) {
copy.block_scope.detach();
}
} else if (
(isBlockNode(self) || isIterationStatementNode(self)) &&
self.block_scope
) {
copy.block_scope = self.block_scope.copyFor(copy);
}
return copy;
};
/**
* @param {Node} self a function, a class or the program
* @returns {boolean} whether `eval` or `with` can reach the names of the scope it opens
*/
ast.isPinned = function isPinned(self) {
const scope = self.block_scope;
return scope !== undefined && scope.isPinned();
};
/**
* @param {Node} self a function, a class or the program
* @returns {boolean} whether a direct `eval` can reach the names of the scope it opens
*/
ast.usesEval = (self) => {
const scope = self.block_scope;
return scope !== undefined && scope.usesEval;
};
/**
* @param {Node} self a function
* @returns {boolean} whether it reads its own `arguments`
*/
ast.usesArguments = (self) => {
const scope = self.block_scope;
return scope !== undefined && scope.usesArguments;
};
/**
* The toplevel parsed out of a wrapper, its `"$ORIG"` directive replaced by
* this toplevel's body.
* @param {Node} toplevel the toplevel wrapped
* @param {string} wrapper the source of the wrapper
* @returns {Node} the wrapped toplevel
*/
const wrapBody = (toplevel, wrapper) => {
const { body, tokenTables } = toplevel;
return transformNode(
parse.parse(
wrapper,
tokenTables ? { [PARSE_ONTO_TABLES]: tokenTables } : undefined
),
createTransformer((/** @type {Node} */ node) => {
if (isDirectiveNode(node) && node.directive === "$ORIG") {
return utils.MAP.splice(/** @type {Node[]} */ (body));
}
})
);
};
/**
* @param {Node} self the toplevel
* @param {string} name the global its exports are written to
* @returns {Node} the toplevel wrapped as a CommonJS module
*/
ast.wrapCommonjs = function wrapCommonjs(self, name) {
return wrapBody(
self,
`(function(exports){'$ORIG';})(typeof ${name}=='undefined'?(${name}={}):${name});`
);
};
/**
* @param {Node} self the toplevel
* @param {boolean | string} argsValues `parameters:arguments` the wrapper is called with
* @returns {Node} the toplevel wrapped in a function called at once
*/
ast.wrapEnclose = function wrapEnclose(self, argsValues) {
const given = typeof argsValues === "string" ? argsValues : "";
let index = given.indexOf(":");
if (index < 0) index = given.length;
return wrapBody(
self,
`(function(${given.slice(0, index)}){"$ORIG"})(${given.slice(index + 1)})`
);
};
/**
* @param {Node} self a function
* @returns {Node[]} the names its parameters bind
*/
ast.argsAsNames = function argsAsNames(self) {
const { params } = self;
if (
params.every((/** @type {Node} */ argument) =>
isSymbolDeclarationNode(argument)
)
) {
return params;
}
/** @type {Node[]} */
const out = [];
for (const argument of params) {
if (isDestructuringNode(argument)) {
out.push(...ast.allSymbols(argument));
} else if (isExpansionNode(argument)) {
out.push(...ast.allSymbols(argument.argument));
} else if (isDefaultAssignNode(argument)) {
out.push(...ast.allSymbols(argument.left));
} else {
out.push(argument);
}
}
return out;
};
/**
* @param {Node} self a function
* @returns {Node | false | undefined} the value it returns at once, where it is all it does
*/
ast.isBraceless = function isBraceless(self) {
return (
isReturnNode(
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body)[0]
) &&
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body)[0].argument
);
};
/**
* @param {Node} self a function
* @returns {number} its `length`, the parameters before the first default or rest
*/
ast.lengthProperty = function lengthProperty(self) {
const correcting =
modules.corrections !== undefined && modules.corrections.enabled;
let length = 0;
for (const argument of self.params) {
if (isSymbolFunargNode(argument) || isDestructuringNode(argument)) {
length++;
} else if (correcting) {
// terser counts the plain ones past a default or a rest too.
break;
}
}
return length;
};
/**
* terser's `all_symbols`: the names a pattern, a default or a declared name
* binds, those of functions in its defaults aside.
* @param {Node} self the pattern, default or name
* @returns {Node[]} the names
*/
ast.allSymbols = (self) => {
if (isDefaultAssignNode(self)) return ast.allSymbols(self.left);
if (!isDestructuringNode(self)) {
if (!isSymbolDeclarationNode(self)) {
throw new TypeError("all_symbols is not a function");
}
return [self];
}
/** @type {Node[]} */
const out = [];
walkNode(
self,
createWalker((/** @type {Node} */ node) => {
if (isSymbolDeclarationNode(node)) out.push(node);
if (isLambdaNode(node)) return true;
})
);
return out;
};
/**
* @param {Node} self a declarator
* @returns {Node[]} the names it declares
*/
ast.declarationsAsNames = function declarationsAsNames(self) {
return isSymbolDeclarationNode(self.id)
? [self.id]
: ast.allSymbols(self.id);
};
/**
* @param {Node} self a constant
* @returns {Node | string | number | boolean | RegExpValue | null} its value
*/
ast.constantValue = function constantValue(self) {
return self.value;
};
/**
* Walks what a class runs as it is defined: its heritage, static blocks,
* computed keys and static field values.
* @param {Node} self a class
* @param {StackVisitor} visitor the walker
* @returns {void}
*/
ast.visitNondeferredClassParts = function visitNondeferredClassParts(
self,
visitor
) {
if (self.superClass) walkNode(self.superClass, visitor);
for (const prop of /** @type {Node[]} */ (
/** @type {Node} */ (self.body).body
)) {
if (isClassStaticBlockNode(prop)) {
walkNode(prop, visitor);
continue;
}
if (prop.computed) {
visitor.push(prop);
walkNode(prop.key, visitor);
visitor.pop();
}
if (
(isClassPrivatePropertyNode(prop) && prop.static && prop.value) ||
(isClassPropertyNode(prop) && prop.static && prop.value)
) {
visitor.push(prop);
walkNode(/** @type {Node} */ (prop.value), visitor);
visitor.pop();
}
}
};
/**
* Walks what a class runs later: its methods and instance field values.
* @param {Node} self a class
* @param {StackVisitor} visitor the walker
* @returns {void}
*/
ast.visitDeferredClassParts = function visitDeferredClassParts(
self,
visitor
) {
for (const prop of /** @type {Node[]} */ (
/** @type {Node} */ (self.body).body
)) {
if (isConciseMethodNode(prop) || isPrivateMethodNode(prop)) {
walkNode(prop, visitor);
} else if (
(isClassPropertyNode(prop) && !prop.static && prop.value) ||
(isClassPrivatePropertyNode(prop) && !prop.static && prop.value)
) {
visitor.push(prop);
walkNode(/** @type {Node} */ (prop.value), visitor);
visitor.pop();
}
}
};
/**
* Whether what a class runs as it is defined, its effects found, is only
* what it extends: the improvements answer, where terser reads every part.
* @param {Node} self a class
* @param {Compressor} compressor the compressor
* @returns {boolean} true where nothing else of it runs with effects
*/
ast.runsOnlyHeritage = (self, compressor) => false;
/**
* @param {Node} self a class
* @returns {boolean} whether what it runs as it is defined reads the class
*/
ast.isSelfReferential = function isSelfReferential(self) {
const thisId = self.id && self.id.definition.id;
let found = false;
let classThis = true;
ast.visitNondeferredClassParts(
self,
createWalker(
(/** @type {Node} */ node, /** @type {() => void} */ descend) => {
if (found) return true;
if (isThisNode(node)) return (found = classThis);
if (isSymbolRefNode(node)) {
return (found = node.definition.id === thisId);
}
if (isLambdaNode(node) && !isArrowNode(node)) {
const outer = classThis;
classThis = false;
descend();
classThis = outer;
return true;
}
}
)
);
return found;
};
};
/**
* Installs webpack's `global_defs` replacement, which swaps each global the
* option names for the value it gives, and `drop_console`.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installDefines = (modules) => {
const { transformNode } = modules.ast;
const { ast } = modules;
const {
ArrayNode,
isConstantNode,
isSyntaxNode,
ObjectNode,
ObjectKeyValNode,
isCallExpressionNode,
isPropAccessNode,
SymbolPropertyNode,
createTransformer
} = ast;
const helpers = /** @type {TerserCompressHelpers} */ (
createCompressHelpers(
/** @type {MinifierModules} */ (
/** @type {MinifierModules | TerserModules} */ (modules)
)
)
);
const { make_node: makeNode, make_void_0: makeVoid0 } = helpers.utils;
const {
make_empty_function: makeEmptyFunction,
make_node_from_constant: makeNodeFromConstant
} = helpers.common;
const {
is_lhs: isLhs,
is_undeclared_ref: isUndeclaredRef,
is_used_in_expression: isUsedInExpression
} = helpers.inference;
const { SQUEEZED, set_flag: setFlag } = helpers.flags;
/**
* terser's `to_node`: the node a `global_defs` value stands for.
* @param {EvaluatedValue | Node} value the value
* @param {Node} orig where its position comes from
* @returns {Node} the node
*/
const toNode = (value, orig) => {
if (isSyntaxNode(value)) {
// Anything but a constant may hold functions, so it is never shared.
const given = value;
const node = isConstantNode(given) ? given : ast.cloneNode(given, true);
return makeNode(ast.factoryOf(node), orig, node);
}
if (Array.isArray(value)) {
return makeNode(ArrayNode, orig, {
elements: value.map((element) => toNode(element, orig))
});
}
if (value && typeof value === "object") {
/** @type {Node[]} */
const properties = [];
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
properties.push(
makeNode(ObjectKeyValNode, orig, {
key: SymbolPropertyNode({ name: key }),
value: toNode(
/** @type {{ [key: string]: EvaluatedValue }} */ (value)[key],
orig
),
computed: false,
kind: "init",
method: false,
shorthand: false
})
);
}
}
return makeNode(ObjectNode, orig, { properties });
}
return makeNodeFromConstant(/** @type {ConstantValue} */ (value), orig);
};
/**
* @param {Node} node a global or `import.meta`
* @param {Compressor} compressor the compressor
* @param {string} name what it reads, with the properties read off it
* @returns {Node | undefined} what `global_defs` gives it
*/
const defined = (node, compressor, name) => {
const defines =
/** @type {Exclude<CompressOptions["global_defs"], false>} */ (
compressor.option("global_defs")
);
return Object.prototype.hasOwnProperty.call(defines, name)
? toNode(defines[name], node)
: undefined;
};
/**
* @returns {undefined} nothing: a node names no `global_defs` value
*/
const findDefinitionsForNode = () => undefined;
/**
* @param {Node} self an optional chain
* @param {Compressor} compressor the compressor
* @param {string} suffix the properties read off it
* @returns {Node | undefined} what `global_defs` gives it
*/
const findDefinitionsForChain = (self, compressor, suffix) =>
ast.findDefinitions(self.expression, compressor, suffix);
/**
* @param {Node} self a property read
* @param {Compressor} compressor the compressor
* @param {string} suffix the properties read off it
* @returns {Node | undefined} what `global_defs` gives it
*/
const findDefinitionsForDot = (self, compressor, suffix) =>
ast.findDefinitions(
self.object,
compressor,
`.${self.property.name}${suffix}`
);
/**
* terser's asks whether the name is global, which throws where it has no
* definition, and then gives nothing either way.
* @param {Node} self a name declared
* @returns {undefined} nothing
*/
const findDefinitionsForSymbolDeclaration = (self) => {
ast.isGlobal(self);
return undefined;
};
/**
* @param {Node} self a name read
* @param {Compressor} compressor the compressor
* @param {string} suffix the properties read off it
* @returns {Node | undefined} what `global_defs` gives it
*/
const findDefinitionsForSymbolRef = (self, compressor, suffix) => {
if (!ast.isGlobal(self)) return undefined;
return defined(self, compressor, self.name + suffix);
};
/**
* @param {Node} self `import.meta`
* @param {Compressor} compressor the compressor
* @param {string} suffix the properties read off it
* @returns {Node | undefined} what `global_defs` gives it
*/
const findDefinitionsForImportMeta = (self, compressor, suffix) =>
defined(self, compressor, `import.meta${suffix}`);
/**
* @param {Node} self the toplevel
* @param {Compressor} compressor the compressor
* @returns {Node} the toplevel, each global `global_defs` names replaced
*/
ast.resolveDefines = function resolveDefines(self, compressor) {
if (!compressor.option("global_defs")) return self;
ast.figureOutScope(self, { ie8: compressor.option("ie8") });
return transformNode(
self,
createTransformer(
/**
* @this {Walker} the transformer
* @param {Node} node the node visited
* @returns {Node | undefined} what replaces it
*/
function replace(node) {
const definition = ast.findDefinitions(node, compressor, "");
if (!definition) return;
let level = 0;
let child = node;
let parent;
while ((parent = this.parent(level++))) {
if (!isPropAccessNode(parent)) break;
if (parent.object !== child) break;
child = parent;
}
if (isLhs(child, parent)) return;
return definition;
}
)
);
};
/**
* @param {Node} self the toplevel
* @param {boolean | string[]} options which `console` methods to drop, all where `true`
* @returns {Node} the toplevel without those calls
*/
ast.dropConsole = function dropConsole(self, options) {
const isArray = Array.isArray(options);
const tt = createTransformer((/** @type {Node} */ call) => {
if (!isCallExpressionNode(call)) return;
const exp = call.callee;
if (!isPropAccessNode(exp)) return;
let name = exp.object;
let property = ast.terserProperty(exp);
let depth = 2;
while (ast.expressionOf(name)) {
property = ast.terserProperty(name);
name = /** @type {Node} */ (ast.expressionOf(name));
depth++;
}
if (isArray && !options.includes(/** @type {string} */ (property))) {
return;
}
if (isUndeclaredRef(name) && name.name === "console") {
if (
depth === 3 &&
!["call", "apply"].includes(
/** @type {string} */ (ast.terserProperty(exp))
) &&
isUsedInExpression(tt)
) {
// A used call to a method of `console.log`, as in
// `console.log.bind(console)`; `call` and `apply` return undefined.
exp.object = makeEmptyFunction(call);
setFlag(exp.object, SQUEEZED);
call.arguments = [];
} else {
return makeVoid0(call);
}
}
});
return transformNode(self, tt);
};
// terser's `_find_defs`, by ESTree type; a type absent reads the default.
/** @type {Record<string, (self: Node, compressor: Compressor, suffix: string) => Node | undefined>} */
const FIND_DEFINITIONS = {
ChainExpression: findDefinitionsForChain,
Identifier: (self, compressor, suffix) =>
ast.isSymbolDeclarationNode(self)
? findDefinitionsForSymbolDeclaration(self)
: ast.isSymbolRefNode(self)
? findDefinitionsForSymbolRef(self, compressor, suffix)
: findDefinitionsForNode(),
Literal: (self, compressor, suffix) =>
ast.isSymbolRefNode(self)
? findDefinitionsForSymbolRef(self, compressor, suffix)
: findDefinitionsForNode(),
MemberExpression: (self, compressor, suffix) =>
ast.isDotNode(self)
? findDefinitionsForDot(self, compressor, suffix)
: findDefinitionsForNode(),
MetaProperty: (self, compressor, suffix) =>
ast.isImportMetaNode(self)
? findDefinitionsForImportMeta(self, compressor, suffix)
: findDefinitionsForNode()
};
/**
* terser's `_find_defs`: the `global_defs` value a node names, if any.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @param {string} suffix the property names read after it
* @returns {Node | undefined} the value
*/
ast.findDefinitions = (node, compressor, suffix) =>
(FIND_DEFINITIONS[node.type] || findDefinitionsForNode)(
node,
compressor,
suffix
);
};
// Which of terser's classes a node is, as bits on each class's prototype, so
// a hot path reads one property where terser runs an `instanceof` chain.
const KIND_SCOPE = 1;
const KIND_LAMBDA = 1 << 1;
const KIND_CLASS = 1 << 2;
const KIND_TOPLEVEL = 1 << 3;
const KIND_DIRECTIVE = 1 << 4;
const KIND_OBJECT_PROPERTY = 1 << 5;
const KIND_BLOCK = 1 << 6;
/**
* Gives an ast `nodeKindBits`, the kind bits the walkers and the compressor
* read off each node, once.
* @param {Pick<TerserModules, "ast">} modules terser's modules
* @returns {void}
*/
const markNodeKinds = ({ ast }) => {
if (ast.nodeKindBits !== undefined) return;
ast.nodeKindBits = ast.kindBits([
["Scope", KIND_SCOPE],
["Lambda", KIND_LAMBDA],
["Class", KIND_CLASS],
["Toplevel", KIND_TOPLEVEL],
["Directive", KIND_DIRECTIVE],
["ObjectProperty", KIND_OBJECT_PROPERTY],
["Block", KIND_BLOCK]
]);
};
/**
* Installs webpack's compressor dispatch. It is terser's `before`, the hook
* every node of a pass goes through, and `in_computed_key`, which the
* optimizers ask of every ancestor, reading a node's class from its kind bits.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installCompressor = (modules) => {
const { ast } = modules;
const { methods: compressorMethods } = modules.compress;
const { SQUEEZED } = modules.flags;
markNodeKinds(modules);
const KIND_BITS = ast.nodeKindBits;
// Whether a node `before` ran on came back other than the one its parent held.
let replacedChild = false;
/**
* @this {Walker} the compressor
* @param {Node} node the node visited
* @param {(node: Node, tw: Walker) => void} descend transforms its children
* @returns {Node} what replaces it
*/
compressorMethods.before = function before(node, descend) {
if ((node.flags & SQUEEZED) !== 0) return node;
const original = node;
const outer = replacedChild;
let wasScope = false;
if ((node[KIND_BITS] & KIND_SCOPE) !== 0) {
node = ast.hoistProperties(node, /** @type {Compressor} */ (this));
node = ast.hoistDeclarations(node, /** @type {Compressor} */ (this));
wasScope = true;
}
// Twice, as terser does: the first pass can replace a child that the
// second then optimizes in its new place. A child the first pass kept is
// squeezed, which the second skips, so it runs only after a replacement.
replacedChild = false;
descend(node, this);
if (replacedChild) descend(node, this);
const optimized = ast.optimize(node, /** @type {Compressor} */ (this));
if (
wasScope &&
optimized !== null &&
optimized !== undefined &&
(optimized[KIND_BITS] & KIND_SCOPE) !== 0
) {
ast.dropUnused(optimized, /** @type {Compressor} */ (this));
descend(optimized, this);
}
if (optimized === node) optimized.flags |= SQUEEZED;
replacedChild = outer || optimized !== original;
return optimized;
};
/**
* @this {Compressor} the compressor
* @returns {boolean} whether the node visited is an object property's key
*/
compressorMethods.in_computed_key = function in_computed_key() {
if (!this.option("evaluate")) return false;
const { stack } = this;
const self = stack[stack.length - 1];
for (let i = stack.length - 2; i >= 0; i--) {
const parent = stack[i];
if (
parent.key === self &&
(parent[KIND_BITS] & KIND_OBJECT_PROPERTY) !== 0
) {
return true;
}
}
return false;
};
};
/**
* How many commas or semicolons a list of expressions or statements prints.
* @param {{ length: number }} array the list
* @returns {number} its separators
*/
const listOverhead = (array) => array.length && array.length - 1;
/**
* @param {Node} func a function
* @returns {number} what `async` and `*` add to it
*/
const lambdaModifiers = (func) =>
(func.generator ? 1 : 0) + (func.async ? 6 : 0);
/**
* @param {boolean} isStatic whether a class member is static
* @returns {number} what `static ` adds to it
*/
const staticSize = (isStatic) => (isStatic ? 7 : 0);
/**
* Whether a destructuring pattern writes an expression: its element,
* property value, rest or defaulted target.
* @param {Node} self the expression
* @param {Compressor} compressor the compressor, `self`'s parent first
* @param {TerserAst} ast the node kinds
* @returns {boolean} true when a pattern writes it
*/
const isPatternTarget = (self, compressor, ast) => {
/** @type {Node | NodeShape} */
let node = self;
let level = 0;
let parent = compressor.parent(level);
while (
parent &&
((ast.isObjectPropertyNode(parent) && parent.value === node) ||
ast.isExpansionNode(parent) ||
(ast.isDefaultAssignNode(parent) && parent.left === node))
) {
node = parent;
parent = compressor.parent(++level);
}
return Boolean(parent) && ast.isDestructuringNode(parent);
};
/**
* terser's `first_in_statement`: whether the node a walk is at is the first
* thing its statement prints, so an object or function there needs parentheses.
* @param {TerserModules} modules terser's modules
* @returns {(stack: { parent: (n?: number) => Node | undefined }) => boolean | undefined} the test
*/
const createFirstInStatement = ({ ast }) => {
const {
isBinaryNode,
isCallExpressionNode,
isChainNode,
isConditionalNode,
isDotNode,
isPrefixedTemplateStringNode,
isSequenceNode,
isStatementNode,
isSubNode,
isUnaryPostfixNode
} = ast;
return (stack) => {
let node = stack.parent(-1);
for (let i = 0, parent; (parent = stack.parent(i)); i++) {
if (isStatementNode(parent) && ast.bodyOf(parent) === node) {
return true;
}
if (
(isSequenceNode(parent) && parent.expressions[0] === node) ||
(isCallExpressionNode(parent) && parent.callee === node) ||
(isPrefixedTemplateStringNode(parent) && parent.tag === node) ||
(isDotNode(parent) && parent.object === node) ||
(isSubNode(parent) && parent.object === node) ||
(isChainNode(parent) && parent.expression === node) ||
(isConditionalNode(parent) && parent.test === node) ||
(isBinaryNode(parent) && parent.left === node) ||
(isUnaryPostfixNode(parent) && parent.argument === node)
) {
node = parent;
} else {
return false;
}
}
return undefined;
};
};
/**
* Installs webpack's `size`, how many bytes the compressor reckons a node
* prints to. It is terser's walk, but its stacks are kept from one call to the
* next rather than built, with the compressor's stack copied, on every call.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installSize = (modules) => {
const { pushChildrenBackwards } = modules.ast;
const { ast } = modules;
const {
isArrowNode,
isAssignNode,
isBinaryNode,
isCallNode,
isConditionalNode,
isDestructuringNode,
isPrivateInNode,
isPropAccessNode,
isSymbolNode,
isUnaryNode
} = ast;
const { PRECEDENCE } = /** @type {{ PRECEDENCE: Record<string, number> }} */ (
modules.parse
);
/** @type {MangleOptions | false | undefined} */
let mangleOptions;
const firstInStatement = createFirstInStatement(modules);
/**
* terser's `needs_parens` of an assignment, where the walk is at.
* @param {Node} node the assignment
* @param {Node} parent what holds it, if anything
* @returns {boolean} whether it prints in parentheses
*/
const assignNeedsParens = (node, parent) =>
isUnaryNode(parent) ||
(isBinaryNode(parent) && !isAssignNode(parent)) ||
(isCallNode(parent) && parent.callee === node) ||
(isConditionalNode(parent) && parent.test === node) ||
(isPropAccessNode(parent) && parent.object === node) ||
// `({a, b} = c)`, an object destructuring assignment.
(isDestructuringNode(node.left) && node.left.is_array === false);
/**
* terser's `needs_parens` of a binary expression, or of an assignment
* (`assignNeedsParens`), where the walk is at.
* @param {Node} node the binary expression
* @param {Node} parent what holds it, if anything
* @returns {boolean} whether it prints in parentheses
*/
const binaryNeedsParens = (node, parent) => {
if (isAssignNode(node)) return assignNeedsParens(node, parent);
if (isCallNode(parent) && parent.callee === node) return true;
if (isUnaryNode(parent)) return true;
if (isPropAccessNode(parent) && parent.object === node) {
return true;
}
const { operator } = node;
if (isBinaryNode(parent)) {
const parentOperator = parent.operator;
// `??` mixed with `||` or `&&` is a syntax error without parens.
if (
(operator === "??" &&
(parentOperator === "||" || parentOperator === "&&")) ||
(parentOperator === "??" && (operator === "||" || operator === "&&"))
) {
return true;
}
const parentPrecedence = PRECEDENCE[parentOperator];
const precedence = PRECEDENCE[operator];
if (
parentPrecedence > precedence ||
(parentPrecedence === precedence &&
(node === parent.right || parentOperator === "**"))
) {
return true;
}
}
if (isPrivateInNode(parent)) {
const parentPrecedence = PRECEDENCE.in;
const precedence = PRECEDENCE[operator];
return (
parentPrecedence > precedence ||
(parentPrecedence === precedence && node === parent.right)
);
}
return false;
};
/**
* @param {Node} self the symbol
* @returns {number} its size, once mangled where it will be
*/
function symbolSize(self) {
const { definition } = self;
return mangleOptions &&
definition &&
// A label and its references point at the label, which terser always mangles.
(modules.ast.isLabelNode(definition) ||
!modules.ast.isDefinitionUnmangleable(definition, mangleOptions))
? 1
: self.name.length;
}
/**
* @param {Node} self a symbol printed as named
* @returns {number} its size
*/
function nameSize(self) {
return self.name.length;
}
/**
* @param {Node} self a method or accessor
* @returns {number} its size, less the part its kind adds
*/
function methodSize(self) {
return staticSize(self.static);
}
/**
* @param {Node} self a class field
* @returns {number} its size
*/
function fieldSize(self) {
return staticSize(self.static) + (self.value ? 1 : 0);
}
/**
* @param {Node} self a declaration list
* @returns {number} its size, for a keyword of four letters
*/
function definitionsSize(self) {
return 4 + listOverhead(self.declarations);
}
/**
* @param {Node} self a symbol a mangler may rename
* @returns {number} its size
*/
function referenceSize(self) {
if (self.name === "arguments") return 9;
return symbolSize(self);
}
// terser's `size.js`: the bytes each node prints to beyond its children's.
/** @type {Record<string, (self: Node, info: ParentStack) => number>} */
const sizes = {
Node: () => 0,
Debugger: () => 8,
Directive: (self) => 2 + self.directive.length,
Block: (self) => 2 + listOverhead(/** @type {Node[]} */ (self.body)),
Toplevel: (self) => listOverhead(/** @type {Node[]} */ (self.body)),
EmptyStatement: () => 1,
LabeledStatement: () => 2,
Do: () => 9,
While: () => 7,
For: () => 8,
ForIn: () => 8,
With: () => 6,
Expansion: () => 3,
Accessor: (self) =>
lambdaModifiers(self) +
4 +
listOverhead(self.params) +
listOverhead(
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body)
),
Function: (self, info) =>
(firstInStatement(info) ? 2 : 0) +
lambdaModifiers(self) +
12 +
listOverhead(self.params) +
listOverhead(
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body)
),
Defun: (self) =>
lambdaModifiers(self) +
13 +
listOverhead(self.params) +
listOverhead(
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body)
),
Arrow: (self) => {
let argumentsAndArrow = 2 + listOverhead(self.params);
if (!(self.params.length === 1 && isSymbolNode(self.params[0]))) {
argumentsAndArrow += 2;
}
const bodyOverhead = ast.isBraceless(self)
? 0
: listOverhead(
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body)
) + 2;
return lambdaModifiers(self) + argumentsAndArrow + bodyOverhead;
},
Destructuring: () => 2,
TemplateString: (self) => 2 + Math.floor(self.segments.length / 2) * 3,
// A tagged template's invalid escape has no value, only the raw text printed.
TemplateSegment: (self) =>
/** @type {string} */ (self.value === null ? self.raw : self.value)
.length,
Return: (self) => (self.argument ? 7 : 6),
Throw: () => 6,
Break: (self) => (self.label ? 6 : 5),
Continue: (self) => (self.label ? 9 : 8),
If: () => 4,
Switch: (self) => 8 + listOverhead(self.cases),
Case: (self) => 5 + listOverhead(/** @type {Node[]} */ (self.consequent)),
Default: (self) =>
8 + listOverhead(/** @type {Node[]} */ (self.consequent)),
Try: () => 3,
Catch: (self) =>
7 +
listOverhead(
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body)
) +
(self.param ? 2 : 0),
Finally: (self) => 7 + listOverhead(/** @type {Node[]} */ (self.body)),
Var: definitionsSize,
Let: definitionsSize,
Const: (self) => 6 + listOverhead(self.declarations),
Using: (self) => (self.await ? 6 : 0) + 6 + listOverhead(self.declarations),
VarDefLike: (self) => (self.init ? 1 : 0),
NameMapping: (self) => (self.local ? 4 : 0),
Import: (self) => {
let size = 6;
if (self.imported_name) size += 1;
if (self.imported_name || self.imported_names) size += 5;
if (self.imported_names) size += 2 + listOverhead(self.imported_names);
return size;
},
ImportMeta: () => 11,
DynamicImport: (self) =>
self.phase
? 9 + self.phase.length + listOverhead(self.args)
: 8 + listOverhead(self.args),
Export: (self) => {
let size = 7 + (self.is_default ? 8 : 0);
if (self.exported_value) size += ownSize(self.exported_value);
if (self.exported_names) size += 2 + listOverhead(self.exported_names);
if (self.source) size += 5;
return size;
},
Call: (self) => (self.optional ? 4 : 2) + listOverhead(self.arguments),
New: (self) => 6 + listOverhead(self.arguments),
Sequence: (self) => listOverhead(self.expressions),
Dot: (self) => self.property.name.length + (self.optional ? 2 : 1),
DotHash: (self) => self.property.name.length + (self.optional ? 3 : 2),
Sub: (self) => (self.optional ? 4 : 2),
Unary: (self) => {
if (self.operator === "typeof") return 7;
if (self.operator === "void") return 5;
return self.operator.length;
},
Binary: (self, info) => {
if (self.operator === "in") return 4;
let size = self.operator.length;
// `1+ +a` keeps a space between the two operators.
if (
(self.operator === "+" || self.operator === "-") &&
isUnaryNode(self.right) &&
self.right.operator === self.operator
) {
size += 1;
}
if (binaryNeedsParens(self, info.parent())) {
size += 2;
}
return size;
},
Conditional: () => 3,
Array: (self) => 2 + listOverhead(self.elements),
Object: (self, info) =>
(firstInStatement(info) ? 4 : 2) + listOverhead(self.properties),
ObjectKeyVal: (self) =>
// A key not computed is a name no walk reaches, as terser's string was.
(self.computed ? 0 : self.key.name.length) + 1,
ObjectGetter: (self) => 5 + staticSize(self.static),
ObjectSetter: (self) => 5 + staticSize(self.static),
ConciseMethod: methodSize,
PrivateMethod: (self) => methodSize(self) + 1,
PrivateGetter: (self) => methodSize(self) + 4,
PrivateSetter: (self) => methodSize(self) + 4,
PrivateIn: () => 5,
Class: (self) => (self.id ? 8 : 7) + (self.superClass ? 8 : 0),
ClassStaticBlock: (self) =>
8 + listOverhead(/** @type {Node[]} */ (self.body)),
ClassProperty: fieldSize,
ClassPrivateProperty: (self) => fieldSize(self) + 1,
Symbol: symbolSize,
SymbolClassProperty: nameSize,
SymbolRef: referenceSize,
SymbolDeclaration: referenceSize,
NewTarget: () => 10,
SymbolImportForeign: nameSize,
SymbolExportForeign: nameSize,
This: () => 4,
Super: () => 5,
String: (self) => /** @type {string} */ (self.value).length + 2,
Number: (self) => {
const { value } = /** @type {{ value: number }} */ (self);
if (value === 0) return 1;
if (value > 0 && Math.floor(value) === value) {
return Math.floor(Math.log10(value) + 1);
}
return value.toString().length;
},
BigInt: (self) => /** @type {string} */ (self.value).length,
RegExp: (self) => /** @type {RegExpValue} */ (self.value).toString().length,
Null: () => 4,
NaN: () => 3,
Undefined: () => 6,
Infinity: () => 8,
True: () => 4,
False: () => 5,
Await: () => 6,
Yield: () => 6
};
// Each kind's size under a key on its prototype: the size of the nearest of
// its classes `sizes` has, as terser's `_size` was a method of its classes.
const SIZE_OF = Symbol("size");
for (const [prototypeKey, prototype] of NODE_KIND_PROTOTYPES) {
const kind = prototypeKey.slice(0, prototypeKey.indexOf("|"));
/** @type {Record<symbol, (typeof sizes)[string]>} */ (prototype)[SIZE_OF] =
sizes[
/** @type {string} */ (
NODE_KIND_ANCESTRY[kind].find((name) => hasOwn(sizes, name))
)
];
}
/**
* terser's `_size`: what a node prints of its own, its children aside.
* @param {Node} node a node
* @param {ParentStack=} info its ancestors, as `walk_parent` hands them
* @returns {number} its size
*/
const ownSize = (node, info) =>
/** @type {Node & { [SIZE_OF]: (self: Node, info: ParentStack) => number }} */ (
node
)[SIZE_OF](node, /** @type {ParentStack} */ (info));
/**
* A byte counter over one node at a time, its stacks kept between counts.
* @returns {(node: Node, compressor: Compressor | undefined, ancestors: Node[] | undefined) => number} the counter
*/
const createCounter = () => {
/** @type {Node[]} */
const toVisit = [];
/** @type {Node[]} */
const stack = [];
/** @type {number[]} */
const popAt = [];
/** @type {Node[] | undefined} */
let outer;
/** @type {Node | undefined} */
let current;
const push = toVisit.push.bind(toVisit);
// What terser's `walk_parent` hands each node: its ancestors, nearest
// first, the walk's own before the stack it started inside.
// Past the root it reads undefined, which `ParentStack` leaves out.
const info = /** @type {ParentStack} */ ({
/**
* @param {number=} n how many ancestors up, -1 for the node itself
* @returns {Node | undefined} that ancestor
*/
parent: (n = 0) => {
if (n === -1) return current;
if (outer !== undefined) {
// terser's copy holds both stacks, and past its end reads the outer again.
const held = outer.length + stack.length;
if (n >= held) return outer[outer.length - (n - held + 1)];
if (n >= stack.length) {
return outer[outer.length - (n - stack.length + 1)];
}
}
return stack[stack.length - (1 + n)];
}
});
return (node, compressor, ancestors) => {
mangleOptions = compressor && compressor._mangle_options;
outer = ancestors || (compressor && compressor.stack) || undefined;
let total = 0;
try {
toVisit.push(node);
while (toVisit.length !== 0) {
current = /** @type {Node} */ (toVisit.pop());
while (
popAt.length !== 0 &&
toVisit.length === popAt[popAt.length - 1]
) {
stack.pop();
popAt.pop();
}
total += ownSize(current, info);
// A braceless arrow's body is a `return` that prints nothing.
if (isArrowNode(current) && ast.isBraceless(current)) {
total += ownSize(
/** @type {Node[]} */ (/** @type {Node} */ (current.body).body)[0]
.argument,
info
);
continue;
}
const before = toVisit.length;
pushChildrenBackwards(current, push);
if (toVisit.length > before) {
stack.push(current);
popAt.push(before - 1);
}
}
} finally {
toVisit.length = 0;
stack.length = 0;
popAt.length = 0;
outer = undefined;
current = undefined;
mangleOptions = undefined;
}
return total;
};
};
// One counter per depth of sizes asked from inside another, each with stacks
// of its own, kept for the next size asked at that depth.
/** @type {ReturnType<typeof createCounter>[]} */
const counters = [];
let depth = 0;
/**
* @param {Node} self the node
* @param {Compressor=} compressor the compressor asking
* @param {Node[]=} ancestors the stack the node sits in
* @returns {number} the bytes it prints to
*/
ast.nodeSize = function nodeSize(self, compressor, ancestors) {
let counter = counters[depth];
if (counter === undefined) counter = counters[depth] = createCounter();
depth++;
try {
return counter(self, compressor, ancestors);
} finally {
depth--;
}
};
};
/**
* Installs webpack's `equivalent_to`, whether two trees print the same. It is
* terser's walk of both side by side, its two stacks kept between calls.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installEquivalent = ({ ast }) => {
const { pushChildrenBackwards } = ast;
/** @type {(this: Node, other: Node) => boolean} */
const passThrough = () => true;
/**
* @param {NodeFieldValue | null | undefined} first a value
* @param {NodeFieldValue | null | undefined} second another
* @returns {boolean} whether both are absent or they are the one value
*/
const same = (first, second) =>
first === null || first === undefined
? second === null || second === undefined
: first === second;
/**
* @param {Node} self a node
* @param {Node} other another of its class
* @returns {boolean} whether the two are `static` alike
*/
function sameStatic(self, other) {
return self.static === other.static;
}
/**
* Whether two lists hold their holes at the same places, which a walk skips
* where terser compared a hole node of its own.
* @param {(Node | null)[]} first a list
* @param {(Node | null)[]} second another
* @returns {boolean} whether they are as long, with holes alike
*/
const sameHoles = (first, second) => {
if (first.length !== second.length) return false;
for (let i = 0; i < first.length; i++) {
if ((first[i] === null) !== (second[i] === null)) return false;
}
return true;
};
/**
* @param {Node} self a node
* @param {Node} other another of its class
* @returns {boolean} whether the two hold the same value
*/
function sameValue(self, other) {
return self.value === other.value;
}
// terser's `equivalent-to.js`: what two nodes of one class must share, their
// children aside. A node's own children compare by identity, as terser's do.
/** @type {Record<string, (self: Node, other: Node) => boolean>} */
const shallow = {
Node: (self) => {
throw new Error(
`did not find a shallow_cmp function for ${ast.kindOf(self)}`
);
},
Debugger: passThrough,
Directive: (self, other) => self.directive === other.directive,
SimpleStatement: passThrough,
Block: passThrough,
EmptyStatement: passThrough,
LabeledStatement: (self, other) => self.label.name === other.label.name,
Do: passThrough,
While: passThrough,
For: (self, other) =>
same(self.init, other.init) &&
same(self.test, other.test) &&
same(self.update, other.update),
ForIn: passThrough,
ForOf: passThrough,
With: passThrough,
Toplevel: passThrough,
Expansion: passThrough,
Lambda: (self, other) =>
self.generator === other.generator && self.async === other.async,
Destructuring: (self, other) =>
self.is_array === other.is_array &&
sameHoles(ast.namesOf(self), ast.namesOf(other)),
PrefixedTemplateString: passThrough,
TemplateString: passThrough,
TemplateSegment: sameValue,
Jump: passThrough,
LoopControl: passThrough,
Await: passThrough,
Yield: (self, other) => self.delegate === other.delegate,
If: (self, other) => same(self.alternate, other.alternate),
Switch: passThrough,
SwitchBranch: passThrough,
Try: (self, other) =>
self.block === other.block &&
same(self.handler, other.handler) &&
same(self.finalizer, other.finalizer),
Catch: (self, other) => same(self.param, other.param),
Finally: passThrough,
DefinitionsLike: passThrough,
VarDefLike: (self, other) => same(self.init, other.init),
NameMapping: passThrough,
Import: (self, other) =>
(self.imported_name || null) === (other.imported_name || null) &&
(self.imported_names || null) === (other.imported_names || null) &&
(self.attributes || null) === (other.attributes || null) &&
(self.phase || null) === (other.phase || null),
ImportMeta: passThrough,
DynamicImport: (self, other) =>
(self.phase || null) === (other.phase || null) &&
self.args.length === other.args.length,
Export: (self, other) =>
same(self.exported_definition, other.exported_definition) &&
same(self.exported_value, other.exported_value) &&
same(self.exported_names, other.exported_names) &&
same(self.attributes, other.attributes) &&
self.source === other.source &&
self.is_default === other.is_default,
Call: passThrough,
Sequence: passThrough,
PropAccess: passThrough,
Chain: passThrough,
Dot: (self, other) =>
self.property.name === other.property.name &&
Boolean(self.quote) === Boolean(other.quote),
DotHash: (self, other) => self.property.name === other.property.name,
Unary: (self, other) => self.operator === other.operator,
Binary: (self, other) => self.operator === other.operator,
PrivateIn: passThrough,
Conditional: passThrough,
Array: (self, other) => sameHoles(self.elements, other.elements),
Object: passThrough,
ObjectProperty: passThrough,
ObjectKeyVal: (self, other) =>
(self.computed
? self.key === other.key
: !other.computed && self.key.name === other.key.name) &&
self.quote === other.quote,
ObjectSetter: sameStatic,
ObjectGetter: sameStatic,
ConciseMethod: sameStatic,
PrivateMethod: sameStatic,
Class: (self, other) =>
same(self.id, other.id) && same(self.superClass, other.superClass),
ClassProperty: sameStatic,
ClassPrivateProperty: sameStatic,
Symbol: (self, other) => self.name === other.name,
NewTarget: passThrough,
This: passThrough,
Super: passThrough,
String: sameValue,
Number: sameValue,
BigInt: sameValue,
RegExp: (self, other) =>
/** @type {RegExpValue} */ (self.value).flags ===
/** @type {RegExpValue} */ (other.value).flags &&
/** @type {RegExpValue} */ (self.value).source ===
/** @type {RegExpValue} */ (other.value).source,
Atom: passThrough
};
// Each kind's comparison, its own or that of the nearest kind it is one of.
/** @type {Map<string, (self: Node, other: Node) => boolean>} */
const shallowByKind = new Map();
for (const kind of Object.keys(NODE_KIND_ANCESTRY)) {
const owner = NODE_KIND_ANCESTRY[kind].find((name) =>
Object.prototype.hasOwnProperty.call(shallow, name)
);
if (owner !== undefined) shallowByKind.set(kind, shallow[owner]);
}
/**
* Replaces what two nodes of a kind must share, their children aside.
* @param {string} kind the kind, by terser's class
* @param {(original: (self: Node, other: Node) => boolean) => (self: Node, other: Node) => boolean} wrap builds the replacement
* @returns {void}
*/
ast.wrapShallowEquivalent = (kind, wrap) => {
shallowByKind.set(
kind,
wrap(
/** @type {(self: Node, other: Node) => boolean} */ (
shallowByKind.get(kind)
)
)
);
};
/**
* @param {Node | null} first a node
* @param {Node | null} second another
* @returns {boolean} whether they match, children aside
*/
const shallowEqual = (first, second) => {
if (first === null && second === null) return true;
const node = /** @type {Node} */ (first);
const kind = ast.kindOf(node);
return (
kind === ast.kindOf(/** @type {Node} */ (second)) &&
/** @type {(self: Node, other: Node) => boolean} */ (
shallowByKind.get(/** @type {string} */ (kind))
)(node, /** @type {Node} */ (second))
);
};
/**
* A comparison of two trees, its stacks kept between comparisons.
* @returns {(first: Node, second: Node) => boolean} the comparison
*/
const createComparison = () => {
/** @type {Node[]} */
const firstStack = [];
/** @type {Node[]} */
const secondStack = [];
const pushFirst = firstStack.push.bind(firstStack);
const pushSecond = secondStack.push.bind(secondStack);
return (first, second) => {
if (!shallowEqual(first, second)) return false;
firstStack.push(first);
secondStack.push(second);
try {
while (firstStack.length !== 0 && secondStack.length !== 0) {
const firstNode = /** @type {Node} */ (firstStack.pop());
const secondNode = /** @type {Node} */ (secondStack.pop());
if (!shallowEqual(firstNode, secondNode)) return false;
pushChildrenBackwards(firstNode, pushFirst);
pushChildrenBackwards(secondNode, pushSecond);
if (firstStack.length !== secondStack.length) return false;
}
return firstStack.length === 0 && secondStack.length === 0;
} finally {
firstStack.length = 0;
secondStack.length = 0;
}
};
};
const compare = createComparison();
let comparing = false;
/**
* @param {Node} self the node
* @param {Node} node another
* @returns {boolean} whether the two print the same
*/
ast.isEquivalent = function isEquivalent(self, node) {
// A comparison asked from inside another compares on stacks of its own.
if (comparing) return createComparison()(self, node);
comparing = true;
try {
return compare(self, node);
} finally {
comparing = false;
}
};
};
/**
* Installs terser's `hoist_properties`, behind webpack's guard: terser rebuilds
* every list under a scope each time the compressor reaches it, so a walk
* first looks for a declaration it could hoist.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installHoist = (modules) => {
const { ast, common, utils } = modules;
const { transformNode } = ast;
/** @type {HoistHooks} */
const hoistHooks = { splits: null };
modules.webpackHoist = hoistHooks;
const {
isExpansionNode,
isObjectNode,
isPropAccessNode,
SymbolRefNode,
isSymbolRefNode,
isSymbolUsingNode,
isToplevelNode,
VarDefNode,
isVarDefNode,
createTransformer,
terserKey,
terserProperty,
walk
} = ast;
const { get_simple_key: getSimpleKey } = common;
const { MAP, make_node: makeNode } = utils;
// What the check reads off a node, one bit a kind, as a node's kind takes
// no prototype walk to read.
const HOIST_SCOPE = 1;
const HOIST_VAR_DEF = 1 << 1;
const HOIST_PROP_ACCESS = 1 << 2;
const hoistKindOf = ast.kindBits([
["Scope", HOIST_SCOPE],
["VarDef", HOIST_VAR_DEF],
["PropAccess", HOIST_PROP_ACCESS]
]);
/**
* @param {Node} property a property of an object literal
* @returns {boolean} whether it keeps the object from being split
*/
const isSpreadOrComputed = (property) =>
isExpansionNode(property) || property.computed;
/**
* terser's transform: each object a declaration holds, read only through its
* properties, split into one variable per property.
* @param {Node} scope the node of the scope
* @param {Set<Node>} splits the declarations whose objects are split
* @returns {Node} the node, transformed
*/
const hoist = (scope, splits) => {
/** @type {Map<number, Map<string, Definition>>} */
const definitionsById = new Map();
const hoister = createTransformer(
/**
* @this {Walker} the transformer
* @param {Node} node the node
* @param {(node: Node, walker: Walker) => void} descend what transforms its children
* @returns {Node | Splice<Node> | undefined} its replacement, if any
*/
function before(node, descend) {
if (isVarDefNode(node)) {
if (splits.has(node)) {
const symbol = node.id;
const definition = symbol.definition;
const value = node.init;
descend(node, this);
/** @type {Map<string, Definition>} */
const definitions = new Map();
/** @type {Node[]} */
const assignments = [];
for (const { key, value: propertyValue } of value.properties) {
const propertyScope = hoister.find_scope();
const hoisted = ast.createSymbol(
scope.block_scope,
ast.factoryOf(symbol),
{
source: symbol,
scope: propertyScope,
conflict_scopes: new Set([
/** @type {Scope} */ (propertyScope),
...symbol.definition.references.map(
(/** @type {Node} */ reference) => reference.scope
)
]),
tentative_name: `${symbol.name}_${terserKey(key)}`
}
);
definitions.set(String(terserKey(key)), hoisted.definition);
assignments.push(
makeNode(VarDefNode, node, {
id: hoisted,
init: /** @type {Node} */ (propertyValue)
})
);
}
definitionsById.set(definition.id, definitions);
return MAP.splice(assignments);
}
} else if (isPropAccessNode(node) && isSymbolRefNode(node.object)) {
const definitions = definitionsById.get(node.object.definition.id);
if (definitions) {
const definition = /** @type {Definition} */ (
definitions.get(String(getSimpleKey(terserProperty(node))))
);
const reference = makeNode(SymbolRefNode, node, {
name: definition.name,
scope: node.object.scope,
definition
});
reference.scope.addReference(reference);
return reference;
}
}
}
);
return transformNode(scope, hoister);
};
/**
* @param {Node} self the node of the scope whose properties would be hoisted
* @param {Compressor} compressor the compressor
* @returns {Node} the node, transformed where terser hoists anything
*/
function hoistProperties(self, compressor) {
if (
!compressor.option("hoist_props") ||
compressor.has_directive("use asm")
) {
return self;
}
const scope = self;
const topRetain =
(isToplevelNode(scope) && compressor.top_retain) || (() => false);
/** @type {Set<Node> | undefined} */
let splits;
// terser's tests, in the order its transform makes them, which runs only
// where one passes.
walk(scope, (/** @type {Node} */ node) => {
const kind = node[hoistKindOf];
// A nested scope's own pass checks its names: none of them is this one's.
if (kind & HOIST_SCOPE && node !== scope) return true;
if (kind & HOIST_VAR_DEF) {
const symbol = node.id;
let definition;
let value;
if (
symbol.scope === scope.block_scope &&
!isSymbolUsingNode(symbol) &&
!(definition = symbol.definition).assignments &&
!definition.direct_access &&
!definition.single_use &&
!compressor.exposed(definition) &&
!topRetain(definition) &&
(value = ast.fixedValue(symbol)) === node.init &&
isObjectNode(value) &&
!value.properties.some(isSpreadOrComputed) &&
(hoistHooks.splits === null
? definition.escaped !== 1
: hoistHooks.splits(scope, definition, value))
) {
if (splits === undefined) splits = new Set();
splits.add(node);
}
} else if (kind & HOIST_PROP_ACCESS && isSymbolRefNode(node.object)) {
// Read as terser's transform reads it, so a missing definition throws.
// eslint-disable-next-line no-unused-expressions
node.object.definition.id;
}
});
if (
splits !== undefined &&
modules.corrections !== undefined &&
modules.corrections.enabled
) {
// terser splits an object read bare where the read stays, as `o;` or
// `typeof o`, which the split leaves reading a name nothing declares.
const bare = definitionsReadBare(scope);
for (const node of splits) {
if (bare.has(node.id.definition)) splits.delete(node);
}
}
return splits === undefined || splits.size === 0
? self
: hoist(self, splits);
}
/**
* @param {Node} scope the node of the scope
* @returns {Set<Definition>} the definitions a reference in it reads other
* than as the object of a property access
*/
const definitionsReadBare = (scope) => {
/** @type {Set<Node>} */
const objects = new Set();
/** @type {Set<Definition>} */
const bare = new Set();
walk(scope, (/** @type {Node} */ node) => {
if (
node[hoistKindOf] & HOIST_PROP_ACCESS &&
isSymbolRefNode(node.object)
) {
objects.add(node.object);
} else if (isSymbolRefNode(node) && !objects.has(node)) {
bare.add(node.definition);
}
});
return bare;
};
ast.hoistProperties = hoistProperties;
};
/**
* Installs webpack's scope analysis: terser's, which `ScopeAnalyzer` runs on
* the compressor's tree, and the scope helpers the compressor calls.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installScope = (modules) => {
const { ast, parse } = modules;
const {
isArrowNode,
isClassPrivatePropertyNode,
isDefunNode,
isFunctionNode,
isLambdaNode,
isObjectKeyValNode,
isSymbolClassNode,
isSymbolDefClassNode,
isSymbolDefunNode,
SymbolFunargNode,
isSymbolLambdaNode,
isSymbolMethodNode,
isSymbolNode,
isSymbolRefNode,
isToplevelNode,
walk,
walk_abort: walkAbort
} = ast;
const jsError = parse.js_error;
/**
* Throws the parse error a misplaced or repeated declaration is.
* @param {PrintNode} node the node a name of which is wrong
* @param {string} message the message
* @param {TokenTables=} tables what the tree's token numbers are read through
* @returns {never} never
*/
const raise = (node, message, tables) => {
const start = /** @type {NodeToken} */ (
tables ? tables.of(node.startToken) : node.startToken
);
return jsError(message, start.file, start.line, start.col, start.pos);
};
/** @type {CompressorDialect} */
const dialect = {
isSymbol: isSymbolNode,
isFunction: isFunctionNode,
isDefun: isDefunNode,
isObjectKeyValue: isObjectKeyValNode,
isPrivateField: isClassPrivatePropertyNode,
argumentsSymbol: (lambda) =>
SymbolFunargNode({
name: "arguments",
startToken: lambda.startToken,
endToken: lambda.endToken
})
};
/**
* terser's `figure_out_scope` for its compressor: see `analyzeCompressorScope`.
* @param {Node} self the scope's node analysed, usually the toplevel
* @param {import("./ScopeAnalyzer").MinifierScopeOptions} options the mangler's options
* @param {{ parent_scope?: Scope | null, toplevel?: Node }=} context where a nested scope sits
* @returns {void}
*/
function figureOutScope(self, options, context = {}) {
const { parent_scope: parentScope, toplevel = self } = context;
options = defaults(options, {
cache: null,
ie8: false,
safari10: false,
module: false
});
if (!isToplevelNode(toplevel)) {
throw new Error("Invalid toplevel scope");
}
const { corrections } = modules;
analyzeCompressorScope(
self,
// The correct phase scopes a parameter list apart from its body.
corrections !== undefined && corrections.enabled
? { ...options, parameterScopes: true }
: options,
raise,
dialect,
parentScope || null,
toplevel
);
}
/**
* terser's `SymbolDef#unmangleable`.
* @param {Definition} definition a definition
* @param {Partial<MangleOptions>=} options the mangle options
* @returns {boolean | number} whether its name has to be kept
*/
ast.isDefinitionUnmangleable = (definition, options = {}) => {
const first = definition.identifiers[0];
return (
(definition.global && !options.toplevel) ||
definition.export & EXPORT_KEEP_NAME ||
definition.undeclared ||
(!options.eval && definition.scope.isPinned()) ||
((isSymbolLambdaNode(first) || isSymbolDefunNode(first)) &&
keepName(options.keep_fnames, first.name)) ||
isSymbolMethodNode(first) ||
((isSymbolClassNode(first) || isSymbolDefClassNode(first)) &&
keepName(options.keep_classnames, first.name))
);
};
/**
* Moves the scope of a function or class into another, as terser's
* `add_child_scope`: see `CompressorScope#adopt`.
* @param {Scope} self the scope receiving it
* @param {Node} node the function or class moved in
* @returns {void}
*/
ast.addChildScope = (self, node) => {
self.adopt(
node.block_scope,
isArrowNode(node)
? () =>
// An arrow reads `arguments` of the function it sits in.
Boolean(
walk(node, (/** @type {Node} */ inner) => {
if (
isSymbolRefNode(inner) &&
isLambdaNode(inner.scope.node) &&
inner.name === "arguments"
) {
return walkAbort;
}
if (isLambdaNode(inner) && !isArrowNode(inner)) {
return true;
}
})
)
: null
);
};
/**
* Declares a symbol the compressor introduces, under a name no scope that
* can see it uses.
* @param {Scope} self the scope declaring it
* @param {NodeFactory} SymbolClass the symbol's factory
* @param {{ source?: Node, tentative_name?: string, scope?: Scope, conflict_scopes?: Iterable<Scope>, init?: Node | null }=} options where it goes and what it is named after
* @returns {Node} the symbol
*/
ast.createSymbol = function createSymbol(
self,
SymbolClass,
{
source,
tentative_name: tentativeName,
scope,
conflict_scopes: conflictScopes = [/** @type {Scope} */ (scope)],
init = null
} = {}
) {
/** @type {Set<Scope>} */
const visible = new Set();
for (const start of new Set(conflictScopes)) {
for (
let current = /** @type {Scope | null | undefined} */ (start);
current !== null && current !== undefined && !visible.has(current);
current = current.upper
) {
visible.add(current);
}
}
/** @type {string | undefined} */
let symbolName;
if (tentativeName) {
const base = tentativeName.replace(/(?:^[^a-z_$]|[^a-z0-9_$])/gi, "_");
symbolName = base;
let i = 0;
/**
* @returns {boolean} whether a visible scope takes the name
*/
const taken = () => {
for (const visibleScope of visible) {
if (
visibleScope.conflictingDefinitionShallow(
/** @type {string} */ (symbolName)
)
) {
return true;
}
}
return false;
};
while (taken()) symbolName = `${base}$${i++}`;
}
if (!symbolName) {
throw new Error("No symbol name could be generated in create_symbol()");
}
const symbol = makeNode(SymbolClass, source, { name: symbolName, scope });
const definition = defineVariable(self, symbol, init || null);
/** @type {Scope} */ (scope).markEnclosed(definition);
return symbol;
};
// The nodes whose block opens a scope of its own, as `is_block_scope` reads
// them: a scope's own block, a switch's or a function's, is not one.
const BLOCK_SCOPE_TYPES = new Set([
"BlockStatement",
"SwitchStatement",
"CatchClause",
"DoWhileStatement",
"WhileStatement",
"ForStatement",
"ForInStatement",
"ForOfStatement"
]);
/**
* terser's `is_block_scope` of a node: whether its block opens a scope.
* @param {Node} self a node
* @returns {boolean} whether it does
*/
ast.isBlockScope = (self) => BLOCK_SCOPE_TYPES.has(self.type);
/**
* terser's `def_variable`.
* @param {Scope} self a scope
* @param {Node} symbol a declaring symbol
* @param {Node | null=} init what it is initialized to
* @returns {Definition} its definition, made or joined
*/
const defineVariable = (self, symbol, init) =>
self.defineVariable(symbol, init, isFunctionNode);
ast.defineVariable = defineVariable;
/**
* terser's `def_function`.
* @param {Scope} self a scope
* @param {Node} symbol a function's name
* @param {Node=} init the function
* @returns {Definition} its definition
*/
const defineFunction = (self, symbol, init) => {
const definition = defineVariable(self, symbol, init);
if (!definition.init || isDefunNode(definition.init)) {
definition.init = init;
}
return definition;
};
ast.defineFunction = defineFunction;
/**
* @param {Node} self a symbol
* @returns {boolean} whether nothing reads it
*/
ast.isUnreferenced = function isUnreferenced(self) {
return !self.definition.references.length && !self.scope.isPinned();
};
/**
* @param {Node} self a symbol
* @returns {boolean} whether it names a global
*/
ast.isGlobal = function isGlobal(self) {
return self.definition.global;
};
/**
* @param {Node} self a node
* @returns {Node} the node its value is read from last
*/
const tailNodeOfNode = (self) => self;
/**
* @param {Node} self a sequence
* @returns {Node} its last expression
*/
const tailNodeOfSequence = (self) =>
self.expressions[self.expressions.length - 1];
ast.figureOutScope = figureOutScope;
// terser's `tail_node`, by ESTree type; a type absent reads the default.
/** @type {Record<string, (self: Node) => Node>} */
const TAIL_NODE = {
SequenceExpression: tailNodeOfSequence
};
/**
* terser's `tail_node`: the node a node's value is read from last.
* @param {Node} node a node
* @returns {Node} that node
*/
ast.tailNode = (node) => (TAIL_NODE[node.type] || tailNodeOfNode)(node);
};
// What the unused phase's passes test a node for, one bit a class.
const UNUSED_LAMBDA = 1;
const UNUSED_CLASS = 1 << 1;
const UNUSED_DEFUN = 1 << 2;
const UNUSED_DEF_CLASS = 1 << 3;
const UNUSED_FUNARG = 1 << 4;
const UNUSED_DEFINITIONS = 1 << 5;
const UNUSED_ASSIGN = 1 << 6;
const UNUSED_UNARY = 1 << 7;
const UNUSED_REFERENCE = 1 << 8;
const UNUSED_SCOPE = 1 << 9;
const UNUSED_STATIC_BLOCK = 1 << 10;
const UNUSED_CLASS_EXPRESSION = 1 << 11;
const UNUSED_FUNCTION = 1 << 12;
const UNUSED_ACCESSOR = 1 << 13;
const UNUSED_FOR = 1 << 14;
const UNUSED_LABELED = 1 << 15;
const UNUSED_BLOCK_STATEMENT = 1 << 16;
const UNUSED_SEQUENCE = 1 << 17;
const UNUSED_CATCH = 1 << 18;
// What the scan of `drop_unused` reads anything off; any other node it only
// descends.
const UNUSED_SCANNED =
UNUSED_ASSIGN | UNUSED_UNARY | UNUSED_REFERENCE | UNUSED_CLASS | UNUSED_SCOPE;
const UNUSED_SCANNED_FIRST =
UNUSED_SCANNED |
UNUSED_DEFUN |
UNUSED_DEF_CLASS |
UNUSED_FUNARG |
UNUSED_DEFINITIONS;
// What the drop of `drop_unused` changes or enters a scope at; any other node
// it only descends.
const UNUSED_DROPPED =
UNUSED_ASSIGN |
UNUSED_UNARY |
UNUSED_SCOPE |
UNUSED_CATCH |
UNUSED_DEFINITIONS |
UNUSED_FOR |
UNUSED_LABELED |
UNUSED_BLOCK_STATEMENT;
// What the scan of `drop_unused` records of a function for the scopes around
// it, one event a node it reads: its kind in the low four bits, its flags above.
const SCAN_END = 0;
const SCAN_SCOPE = 1;
const SCAN_CLASS = 2;
const SCAN_CLASS_DEFERRED = 3;
const SCAN_ASSIGN = 4;
const SCAN_REFERENCE = 5;
const SCAN_INCLUDE = 6;
const SCAN_KIND = 15;
// A scope in strict code, or an included function whose path to it is.
const SCAN_STRICT = 1 << 4;
const SCAN_USES_ARGUMENTS = 1 << 5;
// A function or class declaration, which the first pass walks later if used.
const SCAN_DECLARATION = 1 << 6;
const SCAN_SIDE_EFFECTS = 1 << 7;
const SCAN_SELF_REFERENTIAL = 1 << 8;
// An assignment, with a right side, rather than an update.
const SCAN_ASSIGNMENT = 1 << 9;
// An assignment whose target refers to a block-scoped declaration.
const SCAN_BLOCK_NAME = 1 << 10;
// Above the flags, a scope or class event carries where its range ends.
const SCAN_END_UNIT = 1 << 11;
/**
* What the scan of `drop_unused` found in a function: one event per node it
* reads, which each scope around the function replays instead of walking it
* again. A function nested in another's log holds a view of the same arrays.
* @typedef {object} UnusedScanLog
* @property {number[]} ops each event's kind, flags and, where it has one, end
* @property {(Node | UnusedScanLog | null)[]} refs the node or log an event is of
* @property {number} start the first event of the function
* @property {number} end the index after its last
*/
/**
* @typedef {object} UnusedHooks what later phases add to `drop_unused`
* @property {((lambda: Node, parameter: Node, compressor: Compressor) => boolean) | null} keepsParameter whether a trailing parameter it would trim stays
* @property {((pattern: Node, isUnused: (symbol: Node) => boolean, compressor: Compressor, init: Node | null) => void) | null} prunePattern drops the names nothing reads from a pattern
* @property {((definition: Node, compressor: Compressor) => boolean) | null} dropsPattern whether a declaration whose pattern names nothing can go
* @property {((compressor: Compressor) => boolean) | null} dropsCatchBinding whether a `catch` binding nothing reads can go
* @property {((compressor: Compressor) => boolean) | null} keepsLoopBinding whether a `for` head's lexical name nothing reads stays, its initializer having effects
* @property {((compressor: Compressor) => boolean) | null} keepsListPosition whether an expression dropped from an array or argument list leaves a `0` in its place
* @property {((compressor: Compressor) => boolean) | null} keepsImmutableWrite whether a write to a `const` or a class expression's name stays, as it throws
* @property {((assign: Node, compressor: Compressor) => boolean) | null} keepsConvertingWrite whether a compound write stays, as converting its sides may throw
* @property {((compressor: Compressor) => boolean) | null} dropsLetWrite whether a write to a `let` its declaration has run before, in the same function, can go with the binding
* @property {((compressor: Compressor) => boolean) | null} dropsEmptiedStatement whether a statement a dropped write leaves as `0` goes too
* @property {((compressor: Compressor) => boolean) | null} dropsUnusedImport whether an import binding nothing kept reads goes, its import left bare
* @property {((lambda: Node, parameter: Node, compressor: Compressor) => boolean) | null} trimsUncountedParameter whether a trailing parameter nothing reads, which `length` does not count, goes where `keep_fargs` keeps the rest
*/
/**
* Installs webpack's `drop_unused`. It is terser's, three passes over a scope,
* reading which classes a node is from one set of bits per class rather than
* by an `instanceof` chain per visited node.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installUnused = (modules) => {
const { transformNode, walkNode } = modules.ast;
let dropUnusedRuns = 0;
// What a later phase may keep of a trailing parameter this would trim, and
// what it drops of a pattern or a `catch` binding nothing reads.
/** @type {UnusedHooks} */
const hooks = {
keepsParameter: null,
prunePattern: null,
dropsPattern: null,
dropsCatchBinding: null,
keepsLoopBinding: null,
keepsListPosition: null,
keepsImmutableWrite: null,
keepsConvertingWrite: null,
dropsLetWrite: null,
dropsEmptiedStatement: null,
dropsUnusedImport: null,
trimsUncountedParameter: null
};
/**
* Drops the bindings of a module's imports nothing kept reads; a deferred or
* source-phase import evaluates nothing until a binding is read, so it stays.
* @param {Node[]} body the module's statements
* @param {(definition: Definition) => boolean} isInUse whether a binding is read
* @returns {void}
*/
const dropUnusedImports = (body, isInUse) => {
const read = (/** @type {Node} */ symbol) =>
Boolean(symbol.definition) && isInUse(symbol.definition);
for (const statement of body) {
if (!modules.ast.isImportNode(statement) || statement.phase) continue;
if (statement.imported_name && !read(statement.imported_name)) {
statement.imported_name = NO_NODE;
}
if (statement.imported_names) {
const kept = statement.imported_names.filter(
(/** @type {Node} */ mapping) => read(mapping.local)
);
statement.imported_names =
kept.length > 0
? kept
: /** @type {Node[]} */ (/** @type {Node[] | null} */ (null));
}
}
};
modules.webpackUnused = hooks;
const { ast, common, inference, utils } = modules;
const {
AssignNode,
BlockStatementNode,
isBlockStatementNode,
isCallNode,
isDefaultAssignNode,
isDestructuringNode,
isDirectiveNode,
EmptyStatementNode,
isExpansionNode,
isExportNode,
isForNode,
isForInNode,
isLetNode,
NumberNode,
SimpleStatementNode,
isSimpleStatementNode,
isSequenceNode,
isSymbolBlockDeclarationNode,
isSymbolCatchNode,
isSymbolDeclarationNode,
isSymbolLetNode,
SymbolRefNode,
isSwitchBranchNode,
isSymbolVarNode,
isToplevelNode,
isVarNode,
createStackTransformer,
walk
} = ast;
const { MAP, return_false: returnFalse } = utils;
const remove = removeAll;
const {
make_sequence: makeSequence,
maintain_this_binding: maintainThisBinding,
is_empty: isEmpty,
is_ref_of: isRefOf,
can_be_evicted_from_block: canBeEvictedFromBlock
} = common;
const { is_used_in_expression: isUsedInExpression } = inference;
const { WRITE_ONLY, UNUSED } = modules.flags;
// Which of the kinds the unused phase tests a node is, read off its kind
// once rather than by a chain of tests per visited node.
const kindBitsOf = ast.kindBits([
["Lambda", UNUSED_LAMBDA],
["Class", UNUSED_CLASS],
["Defun", UNUSED_DEFUN],
["DefClass", UNUSED_DEF_CLASS],
["SymbolFunarg", UNUSED_FUNARG],
["Definitions", UNUSED_DEFINITIONS],
["Assign", UNUSED_ASSIGN],
["Unary", UNUSED_UNARY],
["SymbolRef", UNUSED_REFERENCE],
["Scope", UNUSED_SCOPE],
["ClassStaticBlock", UNUSED_STATIC_BLOCK],
["ClassExpression", UNUSED_CLASS_EXPRESSION],
["Function", UNUSED_FUNCTION],
["Accessor", UNUSED_ACCESSOR],
["For", UNUSED_FOR],
["LabeledStatement", UNUSED_LABELED],
["BlockStatement", UNUSED_BLOCK_STATEMENT],
["Sequence", UNUSED_SEQUENCE],
["Catch", UNUSED_CATCH]
]);
/**
* @param {Node} node a node
* @returns {boolean} whether it is a scope whose body opens with `"use strict"`
*/
const headsWithUseStrict = (node) => {
if (!(node[kindBitsOf] & UNUSED_SCOPE)) {
return false;
}
const body = ast.bodyOf(node);
// A class is a scope too, whose members `bodyOf` does not hand back.
if (body === undefined) return false;
for (let i = 0; i < /** @type {Node[]} */ (body).length; i++) {
const statement = /** @type {Node[]} */ (body)[i];
if (!isDirectiveNode(statement)) return false;
if (statement.directive === "use strict") return true;
}
return false;
};
/**
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} what it assigns where only written to
*/
const writeOnlyTarget = (node, compressor) => {
const kind = node[kindBitsOf];
let target;
if (
kind & UNUSED_ASSIGN &&
!node.logical &&
(node.flags & WRITE_ONLY || node.operator === "=")
) {
if (
node.operator !== "=" &&
hooks.keepsConvertingWrite !== null &&
hooks.keepsConvertingWrite(node, compressor)
) {
return;
}
target = node.left;
} else if (kind & UNUSED_UNARY && node.flags & WRITE_ONLY) {
target = node.argument;
} else {
return;
}
return isImmutableWrite(target, compressor) ? undefined : target;
};
/**
* @param {Node} target what a write assigns
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it names a `const` or a class expression's own name, which the write throws on
*/
const isImmutableWrite = (target, compressor) => {
if (
hooks.keepsImmutableWrite === null ||
!ast.isSymbolRefNode(target) ||
!hooks.keepsImmutableWrite(compressor)
) {
return false;
}
const first = target.definition.identifiers[0];
return ast.isSymbolConstNode(first) || ast.isSymbolClassNode(first);
};
/**
* Reads the scope analysis's `DECLARES_*` kind rather than loading the first
* declaring symbol, which only a definition the compressor made, of kind 0, needs.
* @param {Definition} definition a definition
* @returns {boolean} whether its first declaring symbol is a catch parameter
*/
const isCatchDefinition = (definition) =>
definition.kind === DECLARES_CATCH ||
(definition.kind === 0 && isSymbolCatchNode(definition.identifiers[0]));
/**
* The binding around a catch clause that a read of its parameter keeps, as
* terser keeps it; improved, only where the clause declares that name again.
* @param {Definition} definition the catch parameter
* @returns {Definition | false | undefined} the binding kept, if any
*/
const redefinedByCatch = (definition) => {
const redefined =
definition.scope.isBlockScope() &&
definition.scope.getDefunScope().getBinding(definition.name);
if (
!redefined ||
modules.improvements === undefined ||
!modules.improvements.enabled
) {
return redefined;
}
const { identifiers } = definition;
return walk(definition.scope.node, (/** @type {Node} */ node) =>
isSymbolDeclarationNode(node) &&
node.name === definition.name &&
!identifiers.includes(node)
? ast.walk_abort
: undefined
)
? redefined
: undefined;
};
// A recording: one walk of a function nested in the scope being dropped
// from, writing its scan as events into these scratch arrays, copied to
// their size at the end for the logs of the functions walked to share.
/** @type {number[]} */
const recordOps = [];
/** @type {(Node | UnusedScanLog | null)[]} */
const recordRefs = [];
let recordCount = 0;
/** @type {UnusedScanLog[]} */
const recordLogs = [];
// How many scopes on the path from the function recorded are strict.
let recordStrict = 0;
// The compressor a recording reads, null outside one.
let recordCompressor = /** @type {Compressor} */ (
/** @type {Compressor | null} */ (null)
);
let recordKeepsAssign = false;
// Every scope holding a log, released once the top level has been dropped
// from: no scope around it is left to replay one.
/** @type {Scope[]} */
const loggedScopes = [];
// The scopes a replay is in, outermost first.
/** @type {Node[]} */
const replayScopes = [];
let replayDepth = 0;
/**
* @param {number} op an event's kind and flags
* @param {Node | UnusedScanLog | null} ref the node or log it is of
* @returns {number} its index
*/
const emit = (op, ref) => {
const index = recordCount++;
recordOps[index] = op;
recordRefs[index] = ref;
return index;
};
/**
* @returns {void}
*/
const releaseScans = () => {
for (let i = 0; i < loggedScopes.length; i++) {
loggedScopes[i].unusedScan = null;
}
loggedScopes.length = 0;
};
const recorder = {
/**
* Records a node and what it holds, as the scans would read it.
* @param {Node} node the node visited
* @param {((node: Node, visitor: Visitor) => void)=} descend walks its children
* @returns {boolean} false, as nothing stops the walk
*/
_visit(node, descend) {
const kind = node[kindBitsOf];
if (kind & UNUSED_REFERENCE) {
emit(SCAN_REFERENCE, node);
return false;
}
if (descend === undefined) return false;
if ((kind & UNUSED_SCANNED) === 0 || kind & UNUSED_STATIC_BLOCK) {
descend(node, recorder);
return false;
}
if (kind & (UNUSED_ASSIGN | UNUSED_UNARY)) {
const symbol = recordKeepsAssign
? undefined
: writeOnlyTarget(node, recordCompressor);
if (symbol !== undefined && symbol[kindBitsOf] & UNUSED_REFERENCE) {
let op = SCAN_ASSIGN;
if (kind & UNUSED_ASSIGN) {
op |= SCAN_ASSIGNMENT;
if (isRefOf(node.left, isSymbolBlockDeclarationNode)) {
op |= SCAN_BLOCK_NAME;
}
}
// The target's reference comes first; a right side follows it.
emit(op, node);
descend(node, recorder);
return false;
}
descend(node, recorder);
return false;
}
if (kind & UNUSED_CLASS) {
recordStrict++;
let op = SCAN_CLASS;
if (kind & UNUSED_DEF_CLASS) op |= SCAN_DECLARATION;
let runsParts = false;
let runsHeritage = false;
if (ast.hasSideEffects(node, recordCompressor)) {
op |= SCAN_SIDE_EFFECTS;
if (ast.runsOnlyHeritage(node, recordCompressor)) runsHeritage = true;
else if (ast.isSelfReferential(node)) op |= SCAN_SELF_REFERENTIAL;
else runsParts = true;
}
const start = emit(op, node);
if (runsParts) ast.visitNondeferredClassParts(node, recorder);
else if (runsHeritage) walkNode(node.superClass, recorder);
const deferred = emit(SCAN_CLASS_DEFERRED, null);
recordOps[start] += deferred * SCAN_END_UNIT;
descend(node, recorder);
emit(SCAN_END, null);
recordOps[deferred] += recordCount * SCAN_END_UNIT;
recordStrict--;
return false;
}
// A function: a class and a static block went above.
const blockScope = node.block_scope;
const log = blockScope === undefined ? null : blockScope.unusedScan;
if (log !== null) {
emit(recordStrict > 0 ? SCAN_INCLUDE | SCAN_STRICT : SCAN_INCLUDE, log);
return false;
}
const strict = headsWithUseStrict(node);
if (strict) recordStrict++;
let op = SCAN_SCOPE;
if (recordStrict > 0) op |= SCAN_STRICT;
if (
kind & UNUSED_LAMBDA &&
blockScope !== undefined &&
blockScope.usesArguments
) {
op |= SCAN_USES_ARGUMENTS;
}
if (kind & UNUSED_DEFUN) op |= SCAN_DECLARATION;
const start = emit(op, node);
descend(node, recorder);
emit(SCAN_END, null);
const end = recordCount;
recordOps[start] += end * SCAN_END_UNIT;
if (strict) recordStrict--;
if (blockScope !== undefined) {
const log = /** @type {UnusedScanLog} */ ({
ops: recordOps,
refs: recordRefs,
start,
end
});
blockScope.unusedScan = log;
recordLogs.push(log);
loggedScopes.push(blockScope);
}
return false;
},
push: noop,
pop: noop
};
/**
* Records the scan of a function nested in the scope being dropped from,
* whose log its scope then holds.
* @param {Node} node the function, which has a scope
* @param {Compressor} compressor the compressor
* @param {boolean} keepsAssign whether `unused` keeps assignments, so the scan reads none
* @returns {UnusedScanLog} its log
*/
const recordScan = (node, compressor, keepsAssign) => {
recordCount = 0;
recordStrict = 0;
recordCompressor = compressor;
recordKeepsAssign = keepsAssign;
walkNode(node, recorder);
const ops = recordOps.slice(0, recordCount);
const refs = recordRefs.slice(0, recordCount);
for (let i = 0; i < recordLogs.length; i++) {
const log = recordLogs[i];
log.ops = ops;
log.refs = refs;
}
recordLogs.length = 0;
recordRefs.fill(null, 0, recordCount);
recordCompressor = /** @type {Compressor} */ (
/** @type {Compressor | null} */ (null)
);
return /** @type {UnusedScanLog} */ (node.block_scope.unusedScan);
};
/**
* @param {Node} self the node of the scope whose unused declarations are dropped
* @param {Compressor} compressor the compressor
* @returns {void}
*/
function dropUnused(self, compressor) {
if (!isToplevelNode(self)) {
dropUnusedIn(self, compressor);
return;
}
// The top level's run is the last: no scope around it replays a log.
try {
dropUnusedIn(self, compressor);
} finally {
releaseScans();
}
}
/**
* @param {Node} self the node of the scope whose unused declarations are dropped
* @param {Compressor} compressor the compressor
* @returns {void}
*/
function dropUnusedIn(self, compressor) {
if (!compressor.option("unused")) return;
if (compressor.has_directive("use asm")) return;
// Not really a scope, such as a class.
const selfScope = self.block_scope;
if (!selfScope) return;
if (selfScope.isPinned()) return;
const dropFunctions = !isToplevelNode(self) || compressor.toplevel.funcs;
const dropVariables = !isToplevelNode(self) || compressor.toplevel.vars;
const assignAsUnused = /keep_assign/.test(
/** @type {string} */ (compressor.option("unused"))
)
? returnFalse
: (/** @type {Node} */ node) => writeOnlyTarget(node, compressor);
// In use: a definition stamped with this run, listed in the order found.
const run = ++dropUnusedRuns;
/** @type {Definition[]} */
const inUse = [];
/**
* @param {Definition} definition a definition
* @returns {boolean} whether this run found it in use
*/
const isInUse = (definition) => definition.inUseRun === run;
/**
* @param {Definition} definition a definition found in use
* @returns {void}
*/
const markInUse = (definition) => {
if (definition.inUseRun === run) return;
definition.inUseRun = run;
inUse.push(definition);
};
/**
* What replaces an expression dropped for having no effect: nothing in a
* list, but `0` where a position in it is read, as in an array.
* @param {Node} node the expression
* @param {Node} parent the node holding it
* @param {boolean=} inList whether it sits in a list
* @returns {Node | SkipMarker} what replaces it
*/
const dropped = (node, parent, inList) => {
if (
inList &&
(isSequenceNode(parent) ||
hooks.keepsListPosition === null ||
!hooks.keepsListPosition(compressor))
) {
return MAP.skip;
}
const zero = makeNode(NumberNode, node, { value: 0 });
if (
isSimpleStatementNode(parent) &&
hooks.dropsEmptiedStatement !== null &&
hooks.dropsEmptiedStatement(compressor)
) {
emptied.add(zero);
}
return zero;
};
/** @type {Set<Node>} */
const emptied = new Set();
/** @type {Map<number, Node>} */
const fixedIds = new Map();
if (isToplevelNode(self) && compressor.top_retain) {
for (const definition of selfScope.variables) {
if (compressor.top_retain(definition)) {
markInUse(definition);
}
}
}
/** @type {Map<number, Node[]>} */
const varDefsById = new Map();
/** @type {Map<number, Node[]>} */
const initializations = new Map();
// The `let` bindings of this scope whose declaration pass 1 has walked
// past, outside a `switch` case, which a later write cannot precede.
/** @type {Set<Definition>} */
const initializedLets = new Set();
/**
* @param {Definition} definition a `let` whose declaration ran before
* @returns {boolean} whether a write to it may go with it: one of a block, whose
* name no other script shares, or anywhere the scope drops its variables
*/
const dropsWriteOf = (definition) =>
initializedLets.has(definition) &&
(dropVariables || definition.scope !== selfScope);
// Pass 1: find which symbols this scope, not a nested one, uses directly.
let scope = self;
// Pass 3 skips a nested scope passes 1 and 2 walked whole unless it holds
// an assignment it may drop or a part they did not walk.
/** @type {Map<Node, Node>} */
const walkedScopes = new Map();
/** @type {Set<Node>} */
const neededScopes = new Set();
/** @type {Map<Node, Node>} */
const defunScopes = new Map();
/** @type {Node[]} */
const nestedAssigns = [];
/**
* @param {Node} from a scope nested in this one, or this one
* @returns {void}
*/
const markNeeded = (from) => {
let current = from;
while (current !== self && !neededScopes.has(current)) {
neededScopes.add(current);
current = /** @type {Node} */ (walkedScopes.get(current));
}
};
// The walk of passes 1 and 2 keeps only the nodes it is in, which give
// the one parent and the rare directive pass 1 reads.
/** @type {Node[]} */
const descending = [];
/** @type {(((node: Node, visitor: Visitor) => void) | undefined)[]} */
const descends = [];
const descendCurrent = () => {
/** @type {(node: Node, visitor: Visitor) => void} */ (
descends[descends.length - 1]
)(descending[descending.length - 1], tw);
};
const tw = {
/**
* @param {Node} node the node visited
* @param {((node: Node, visitor: Visitor) => void)=} descend walks its children
* @returns {boolean} false, as nothing stops the walk
*/
_visit(node, descend) {
descending.push(node);
descends.push(descend);
if (descend === undefined) {
scan(node, noop);
} else if (!scan(node, descendCurrent)) {
descend(node, tw);
}
descending.pop();
descends.pop();
return false;
},
/**
* @param {Node} node a node the walk is in, though it does not visit it
* @returns {void}
*/
push(node) {
descending.push(node);
descends.push(undefined);
},
pop() {
descending.pop();
descends.pop();
}
};
/**
* The full walker's `has_directive("use strict")` at the node visited: a
* class it is in, or a directive heading the body it is in or its own.
* @returns {boolean} whether the node visited is in strict code
*/
const inStrictCode = () => {
for (let i = descending.length; --i >= 0;) {
const node = descending[i];
if (headsWithUseStrict(node) || node[kindBitsOf] & UNUSED_CLASS) {
return true;
}
}
return false;
};
/**
* `inStrictCode` above the function visited, whose own strictness its
* log says.
* @returns {boolean} whether the scope around the function is in strict code
*/
const inStrictCodeAbove = () => {
for (let i = descending.length - 1; --i >= 0;) {
const node = descending[i];
if (headsWithUseStrict(node) || node[kindBitsOf] & UNUSED_CLASS) {
return true;
}
}
return false;
};
let secondPass = false;
/**
* Replays a function's log as walking it would read it; the first pass
* reads what the second does not.
* @param {UnusedScanLog} log the log
* @param {number} from the first event replayed
* @param {number} to the index after the last
* @param {boolean} strictAbove whether the function sits in strict code
* @returns {void}
*/
const replay = (log, from, to, strictAbove) => {
const { ops, refs } = log;
let i = from;
while (i < to) {
const op = ops[i];
switch (op & SCAN_KIND) {
case SCAN_REFERENCE: {
const definition = /** @type {Node} */ (refs[i]).definition;
if (!isInUse(definition)) {
markInUse(definition);
if (isCatchDefinition(definition)) {
const redefined = redefinedByCatch(definition);
if (redefined) markInUse(redefined);
}
}
i++;
break;
}
case SCAN_SCOPE: {
const node = /** @type {Node} */ (refs[i]);
if (!secondPass) {
if (
op & SCAN_USES_ARGUMENTS &&
!strictAbove &&
(op & SCAN_STRICT) === 0
) {
for (const argname of node.params) {
if (!isSymbolDeclarationNode(argname)) continue;
markInUse(argname.definition);
}
}
if (op & SCAN_DECLARATION) {
mapAdd(initializations, node.id.definition.id, node);
defunScopes.set(node, scope);
i = Math.floor(op / SCAN_END_UNIT);
break;
}
}
walkedScopes.set(node, scope);
replayScopes[replayDepth++] = scope;
scope = node;
i++;
break;
}
case SCAN_END:
scope = replayScopes[--replayDepth];
i++;
break;
case SCAN_CLASS: {
const node = /** @type {Node} */ (refs[i]);
const deferred = Math.floor(op / SCAN_END_UNIT);
const end = Math.floor(ops[deferred] / SCAN_END_UNIT);
if (secondPass) {
replay(log, deferred + 1, end - 1, strictAbove);
} else {
const runs = (op & SCAN_SIDE_EFFECTS) !== 0;
const selfReferential = (op & SCAN_SELF_REFERENTIAL) !== 0;
if (runs) {
if (selfReferential) {
replay(log, deferred + 1, end - 1, strictAbove);
} else {
markNeeded(scope);
replay(log, i + 1, deferred, strictAbove);
}
}
if (op & SCAN_DECLARATION) {
mapAdd(initializations, node.id.definition.id, node);
defunScopes.set(node, scope);
} else if (!(runs && selfReferential)) {
replay(log, deferred + 1, end - 1, strictAbove);
}
}
i = end;
break;
}
case SCAN_ASSIGN: {
const node = /** @type {Node} */ (refs[i]);
const symbol = op & SCAN_ASSIGNMENT ? node.left : node.argument;
nestedAssigns.push(symbol, scope);
const definition = symbol.definition;
if (
(op & SCAN_BLOCK_NAME) === 0 &&
selfScope.getBinding(symbol.name) === definition
) {
if (
op & SCAN_ASSIGNMENT &&
!definition.chained &&
ast.fixedValue(node.left) === node.right
) {
fixedIds.set(definition.id, node);
}
// Past the target's reference, to the right side.
i += 2;
} else {
i++;
}
break;
}
default: {
const inner = /** @type {UnusedScanLog} */ (refs[i]);
replay(
inner,
inner.start,
inner.end,
strictAbove || (op & SCAN_STRICT) !== 0
);
i++;
}
}
}
};
/** @type {(node: Node, descend: () => void) => boolean | undefined} */
let scan = (node, descend) => {
const kind = node[kindBitsOf];
if ((kind & UNUSED_SCANNED_FIRST) === 0) return;
if (
kind & UNUSED_LAMBDA &&
node.block_scope.usesArguments &&
!inStrictCode()
) {
for (const argname of node.params) {
if (!isSymbolDeclarationNode(argname)) continue;
const definition = argname.definition;
markInUse(definition);
}
}
if (node === self) return;
if (kind & UNUSED_CLASS && ast.hasSideEffects(node, compressor)) {
if (ast.runsOnlyHeritage(node, compressor)) {
markNeeded(scope);
walkNode(node.superClass, tw);
} else if (ast.isSelfReferential(node)) {
descend();
} else {
markNeeded(scope);
ast.visitNondeferredClassParts(node, tw);
}
}
if (kind & (UNUSED_DEFUN | UNUSED_DEF_CLASS)) {
const nodeDefinition = node.id.definition;
const inExport = isExportNode(descending[descending.length - 2]);
if (
(inExport || (!dropFunctions && scope === self)) &&
nodeDefinition.global
) {
markInUse(nodeDefinition);
}
mapAdd(initializations, nodeDefinition.id, node);
defunScopes.set(node, scope);
// Nested scopes are not gone into.
return true;
}
// The root scope drops; nested scopes only have their uses read.
const inRootScope = scope === self;
if (kind & UNUSED_FUNARG && inRootScope) {
mapAdd(varDefsById, node.definition.id, node);
}
if (kind & UNUSED_DEFINITIONS && inRootScope) {
const inExport = isExportNode(descending[descending.length - 2]);
for (const definition of node.declarations) {
if (isSymbolVarNode(definition.id)) {
mapAdd(varDefsById, definition.id.definition.id, definition);
}
if (inExport || !dropVariables) {
walk(definition.id, (/** @type {Node} */ inner) => {
if (isSymbolDeclarationNode(inner)) {
const declared = inner.definition;
if (declared.global) markInUse(declared);
}
});
}
if (isDestructuringNode(definition.id)) {
walkNode(definition, tw);
}
if (isSymbolDeclarationNode(definition.id) && definition.init) {
const nodeDefinition = definition.id.definition;
mapAdd(initializations, nodeDefinition.id, definition.init);
if (
!nodeDefinition.chained &&
ast.fixedValue(definition.id) === definition.init
) {
fixedIds.set(nodeDefinition.id, definition);
}
if (ast.hasSideEffects(definition.init, compressor)) {
walkNode(definition.init, tw);
}
}
}
if (
hooks.dropsLetWrite !== null &&
isLetNode(node) &&
!isSwitchBranchNode(descending[descending.length - 2]) &&
hooks.dropsLetWrite(compressor)
) {
for (const definition of node.declarations) {
if (isSymbolLetNode(definition.id)) {
initializedLets.add(definition.id.definition);
}
}
}
return true;
}
return scanReferenceScoped(node, descend);
};
walkNode(self, tw);
// Pass 2: walk each used symbol's initialization, for the symbols it uses.
scan = scanReferenceScoped;
secondPass = true;
// Indexed, as what this finds in use is appended while it runs.
for (let i = 0; i < inUse.length; i++) {
const definition = inUse[i];
const inits = initializations.get(definition.id);
if (inits) {
for (const init of inits) {
scope = defunScopes.get(init) || self;
walkNode(init, tw);
}
}
}
scope = self;
for (const [defun, parent] of defunScopes) {
if (!isInUse(defun.id.definition)) markNeeded(parent);
}
if (dropVariables) {
for (let i = 0; i < nestedAssigns.length; i += 2) {
const definition = nestedAssigns[i].definition;
if (!isInUse(definition) || fixedIds.has(definition.id)) {
markNeeded(nestedAssigns[i + 1]);
}
}
}
/**
* Counts a name a pattern declares as dropped where nothing reads it.
* @param {Node} symbol a name a pattern declares
* @returns {boolean} whether it was, and the pattern may lose it
*/
const dropName = (symbol) => {
const definition = symbol.definition;
if (
isInUse(definition) ||
(definition.global && !dropVariables) ||
isCatchDefinition(definition)
) {
return false;
}
definition.eliminated++;
return true;
};
// Pass 3: drop the declarations not in use. It reads no directive, only
// parents, so its transformer keeps nothing but the stack.
const tt = createStackTransformer(
/**
* @this {Walker} the transformer
* @param {Node} node the node visited
* @param {(node: Node, tw: Walker) => void} descend transforms its children
* @param {boolean=} inList whether it sits in a list
* @returns {Node | Splice<Node> | SkipMarker | undefined} what replaces it, where anything does
*/
function before(node, descend, inList) {
const kind = node[kindBitsOf];
if ((kind & UNUSED_DROPPED) === 0) {
// A sequence left with one expression or none, its children done
// here so the transformer needs no `after` for every other node.
if ((kind & UNUSED_SEQUENCE) === 0) return;
descend(node, this);
switch (node.expressions.length) {
case 0:
return dropped(node, tt.parent(), inList);
case 1:
return node.expressions[0];
}
return node;
}
const parent = tt.parent();
const symbol = assignAsUnused(node);
if (
symbol &&
symbol[kindBitsOf] & UNUSED_REFERENCE &&
(dropVariables || dropsWriteOf(symbol.definition))
) {
const definition = symbol.definition;
const inUse = isInUse(definition);
if (kind & UNUSED_ASSIGN) {
if (
!inUse ||
(fixedIds.has(definition.id) &&
fixedIds.get(definition.id) !== node)
) {
const assignee = transformNode(node.right, tt);
if (
!inUse &&
!ast.hasSideEffects(assignee, compressor) &&
!isUsedInExpression(tt)
) {
return dropped(node, parent, inList);
}
return maintainThisBinding(parent, node, assignee);
}
} else if (!inUse) {
return dropped(node, parent, inList);
}
}
if (
kind & UNUSED_SCOPE &&
!(kind & UNUSED_STATIC_BLOCK) &&
scope !== self
) {
if (walkedScopes.has(node) && !neededScopes.has(node)) return node;
// Entered, however deep, so changed inside: the scopes around it
// scan it afresh.
if (node.block_scope !== undefined) {
node.block_scope.unusedScan = null;
}
}
if (scope !== self) return;
if (
kind & UNUSED_CATCH &&
node.param !== null &&
isSymbolCatchNode(node.param) &&
hooks.dropsCatchBinding !== null &&
!isInUse(node.param.definition) &&
// A `var` of the same name in the block would reach the outer one.
node.param.definition.identifiers.length === 1 &&
!node.param.definition.scope
.getDefunScope()
.getBinding(node.param.definition.name) &&
hooks.dropsCatchBinding(compressor)
) {
node.param = /** @type {Node} */ (/** @type {Node | null} */ (null));
}
let nameDefinition;
if (
node.id &&
((kind & UNUSED_CLASS_EXPRESSION &&
!keepName(
compressor.option("keep_classnames"),
(nameDefinition = node.id.definition).name
)) ||
(kind & UNUSED_FUNCTION &&
!keepName(
compressor.option("keep_fnames"),
(nameDefinition = node.id.definition).name
))) && // A declaration of the same name overshadows it, so it is never read.
(!isInUse(nameDefinition) || nameDefinition.identifiers.length > 1)
) {
node.id = /** @type {Node} */ (/** @type {Node | null} */ (null));
}
if (kind & UNUSED_LAMBDA && !(kind & UNUSED_ACCESSOR)) {
let trim =
!compressor.option("keep_fargs") ||
// An IIFE that does not refer to its name.
(isCallNode(parent) &&
parent.callee === node &&
!ast.isPinned(node) &&
(!node.id || ast.isUnreferenced(node.id)));
const argnames = node.params;
const { keepsParameter, trimsUncountedParameter } = hooks;
// A default or rest parameter counts in no `length` to keep.
let tail = !trim && trimsUncountedParameter !== null;
for (let i = argnames.length; --i >= 0;) {
let symbol = argnames[i];
const written = symbol;
if (
trim &&
keepsParameter !== null &&
keepsParameter(node, symbol, compressor)
) {
trim = false;
}
if (isExpansionNode(symbol)) symbol = symbol.argument;
if (isDefaultAssignNode(symbol)) symbol = symbol.left;
// A destructured argument is kept: it asserts a shape.
if (!isDestructuringNode(symbol) && !isInUse(symbol.definition)) {
symbol.flags |= UNUSED;
if (trim) {
argnames.pop();
} else if (
tail &&
/** @type {NonNullable<UnusedHooks["trimsUncountedParameter"]>} */ (
trimsUncountedParameter
)(node, written, compressor)
) {
argnames.pop();
} else {
tail = false;
}
} else {
tail = false;
if (isDestructuringNode(symbol) && hooks.prunePattern !== null) {
hooks.prunePattern(symbol, dropName, compressor, null);
}
trim = false;
}
}
}
if (kind & UNUSED_DEF_CLASS && node !== self) {
const definition = node.id.definition;
descend(node, this);
const keepClass =
(definition.global && !dropFunctions) || isInUse(definition);
if (!keepClass) {
const kept = ast.dropSideEffectFree(node, compressor);
if (kept === null || kept === undefined) {
definition.eliminated++;
return inList ? MAP.skip : makeNode(EmptyStatementNode, node);
}
return kept;
}
return node;
}
if (kind & UNUSED_DEFUN && node !== self) {
const definition = node.id.definition;
const keep =
(definition.global && !dropFunctions) || isInUse(definition);
if (!keep) {
definition.eliminated++;
return inList ? MAP.skip : makeNode(EmptyStatementNode, node);
}
}
if (
kind & UNUSED_DEFINITIONS &&
!(isForInNode(parent) && parent.left === node)
) {
return dropDefinitions(node, parent, inList);
}
// `var`s moved out of a `for` head, where an unused name with a side
// effect left them, would make an invalid tree.
if (kind & UNUSED_FOR) {
descend(node, this);
let block;
if (isBlockStatementNode(node.init)) {
block = node.init;
node.init = /** @type {Node} */ (
/** @type {Node[]} */ (block.body).pop()
);
/** @type {Node[]} */ (block.body).push(node);
}
if (isSimpleStatementNode(node.init)) {
node.init = node.init.expression;
} else if (isEmpty(node.init)) {
node.init = /** @type {Node} */ (/** @type {Node | null} */ (null));
}
return !block
? node
: inList
? MAP.splice(/** @type {Node[]} */ (block.body))
: block;
}
if (kind & UNUSED_LABELED && isForNode(node.body)) {
descend(node, this);
if (isBlockStatementNode(node.body)) {
const block = /** @type {Node} */ (node.body);
node.body = /** @type {Node} */ (
/** @type {Node[]} */ (block.body).pop()
);
/** @type {Node[]} */ (block.body).push(node);
return inList
? MAP.splice(/** @type {Node[]} */ (block.body))
: block;
}
return node;
}
if (kind & UNUSED_BLOCK_STATEMENT) {
descend(node, this);
if (
inList &&
/** @type {Node[]} */ (node.body).every(canBeEvictedFromBlock)
) {
return MAP.splice(/** @type {Node[]} */ (node.body));
}
return node;
}
if (kind & UNUSED_SCOPE && !(kind & UNUSED_STATIC_BLOCK)) {
if (walkedScopes.has(node) && !neededScopes.has(node)) return node;
// Changed inside, so the scopes around it scan it afresh.
if (node.block_scope !== undefined) {
node.block_scope.unusedScan = null;
}
const saveScope = scope;
scope = node;
descend(node, this);
scope = saveScope;
return node;
}
}
);
/**
* A declaration list with its unused names dropped, their initializers'
* side effects kept in order.
* @param {Node} node the declarations
* @param {Node} parent the node holding them
* @param {boolean=} inList whether they sit in a list
* @returns {Node | Splice<Node> | SkipMarker} what replaces them
*/
const dropDefinitions = (node, parent, inList) => {
const dropBlock = !isToplevelNode(parent) && !isVarNode(node);
// Effects left as statements would move a `for` head's `let` out of
// the loop, where its closures share one binding.
const keepsEffects =
hooks.keepsLoopBinding !== null &&
!isVarNode(node) &&
isForNode(parent) &&
parent.init === node &&
hooks.keepsLoopBinding(compressor);
// Uninitialized names go first.
/** @type {Node[]} */
const body = [];
/** @type {Node[]} */
const head = [];
/** @type {Node[]} */
const tail = [];
// Side effects of unused names' initializers, cascaded into the next.
/** @type {Node[]} */
let sideEffects = [];
for (const definition of node.declarations) {
if (definition.init) {
definition.init = transformNode(definition.init, tt);
}
const isDestructure = isDestructuringNode(definition.id);
if (
isDestructure &&
hooks.prunePattern !== null &&
(dropVariables || dropBlock)
) {
hooks.prunePattern(
definition.id,
dropName,
compressor,
definition.init
);
}
const symbol = isDestructure
? createCompressorDefinition(
"<destructure>",
null,
0,
{ name: "<destructure>" },
undefined
)
: definition.id.definition;
if (dropBlock && symbol.global) {
tail.push(definition);
continue;
}
if (
!(dropVariables || dropBlock) ||
(isDestructure &&
(ast.namesOf(definition.id).length ||
definition.id.is_array ||
// Loose, as terser's: an option of `1` reads as `true`.
// eslint-disable-next-line eqeqeq
compressor.option("pure_getters") != true) &&
!(
hooks.dropsPattern !== null &&
hooks.dropsPattern(definition, compressor)
)) ||
isInUse(symbol)
) {
if (
definition.init &&
fixedIds.has(symbol.id) &&
fixedIds.get(symbol.id) !== definition
) {
definition.init = /** @type {Node} */ (
ast.dropSideEffectFree(definition.init, compressor)
);
}
if (isSymbolVarNode(definition.id)) {
const varDefs = /** @type {Node[]} */ (varDefsById.get(symbol.id));
if (
varDefs.length > 1 &&
(!definition.init ||
symbol.identifiers.indexOf(definition.id) > symbol.eliminated)
) {
if (definition.init) {
const reference = makeNode(
SymbolRefNode,
definition.id,
definition.id
);
symbol.references.push(reference);
const assign = makeNode(AssignNode, definition, {
operator: "=",
logical: false,
left: reference,
right: definition.init
});
if (fixedIds.get(symbol.id) === definition) {
fixedIds.set(symbol.id, assign);
}
sideEffects.push(transformNode(assign, tt));
}
remove(varDefs, definition);
symbol.eliminated++;
continue;
}
}
if (definition.init) {
if (sideEffects.length > 0) {
if (
tail.length > 0 ||
// Before the declaration, a `let` it names is in its TDZ.
(head.length > 0 &&
!isVarNode(node) &&
modules.corrections !== undefined &&
modules.corrections.enabled)
) {
sideEffects.push(definition.init);
definition.init = makeSequence(definition.init, sideEffects);
} else {
body.push(
makeNode(SimpleStatementNode, node, {
expression: makeSequence(node, sideEffects)
})
);
}
sideEffects = [];
}
tail.push(definition);
} else {
head.push(definition);
}
} else if (isCatchDefinition(symbol)) {
const value =
definition.init &&
ast.dropSideEffectFree(definition.init, compressor);
if (value) sideEffects.push(value);
definition.init = /** @type {Node} */ (
/** @type {Node | null} */ (null)
);
head.push(definition);
} else {
const value =
definition.init &&
ast.dropSideEffectFree(definition.init, compressor);
if (value && keepsEffects) {
definition.init = value;
tail.push(definition);
continue;
}
if (value) sideEffects.push(value);
symbol.eliminated++;
}
}
if (head.length > 0 || tail.length > 0) {
node.declarations = [...head, ...tail];
body.push(node);
}
if (sideEffects.length > 0) {
body.push(
makeNode(SimpleStatementNode, node, {
expression: makeSequence(node, sideEffects)
})
);
}
switch (body.length) {
case 0:
return inList ? MAP.skip : makeNode(EmptyStatementNode, node);
case 1:
return body[0];
default:
return inList
? MAP.splice(body)
: makeNode(BlockStatementNode, node, { body });
}
};
transformNode(self, tt);
if (
dropVariables &&
isToplevelNode(self) &&
hooks.dropsUnusedImport !== null &&
hooks.dropsUnusedImport(compressor)
) {
dropUnusedImports(/** @type {Node[]} */ (self.body), isInUse);
}
// A function optimized before its writes went is not visited again.
if (emptied.size > 0) {
/** @type {[Node[], Node][]} */
const found = [];
const walker = modules.ast.createWalker((/** @type {Node} */ node) => {
if (isSimpleStatementNode(node) && emptied.has(node.expression)) {
const list = modules.ast.bodyOf(walker.parent(0));
if (Array.isArray(list)) found.push([list, node]);
}
});
walkNode(self, walker);
for (const [list, statement] of found) {
list.splice(list.indexOf(statement), 1);
}
}
/**
* @param {Node} node a node
* @param {() => void} descend walks its children
* @returns {boolean | undefined} true where its children are not walked
*/
function scanReferenceScoped(node, descend) {
if ((node[kindBitsOf] & UNUSED_SCANNED) === 0) {
return;
}
let nodeDefinition;
const symbol = assignAsUnused(node);
if (scope !== self && symbol && symbol[kindBitsOf] & UNUSED_REFERENCE) {
nestedAssigns.push(symbol, scope);
}
if (
symbol &&
symbol[kindBitsOf] & UNUSED_REFERENCE &&
(!isRefOf(node.left, isSymbolBlockDeclarationNode) ||
(!secondPass &&
scope === self &&
initializedLets.has(symbol.definition))) &&
(selfScope.getBinding(symbol.name) ===
(nodeDefinition = symbol.definition) ||
// A block's own `let` in a function, whose declaration ran before.
(!secondPass && scope === self && dropsWriteOf(nodeDefinition)))
) {
if (node[kindBitsOf] & UNUSED_ASSIGN) {
walkNode(node.right, tw);
if (
!nodeDefinition.chained &&
ast.fixedValue(node.left) === node.right
) {
fixedIds.set(nodeDefinition.id, node);
}
}
return true;
}
const kind = node[kindBitsOf];
if (kind & UNUSED_REFERENCE) {
nodeDefinition = node.definition;
if (!isInUse(nodeDefinition)) {
markInUse(nodeDefinition);
if (isCatchDefinition(nodeDefinition)) {
const redefined = redefinedByCatch(nodeDefinition);
if (redefined) markInUse(redefined);
}
}
return true;
}
if (kind & UNUSED_CLASS) {
descend();
return true;
}
if (kind & UNUSED_SCOPE && !(kind & UNUSED_STATIC_BLOCK)) {
const blockScope = node.block_scope;
if (blockScope !== undefined) {
let log = blockScope.unusedScan;
if (log === null) {
log = recordScan(node, compressor, assignAsUnused === returnFalse);
}
replay(log, log.start, log.end, inStrictCodeAbove());
return true;
}
const saveScope = scope;
walkedScopes.set(node, scope);
scope = node;
descend();
scope = saveScope;
return true;
}
}
}
ast.dropUnused = dropUnused;
};
// What the reduce phase's hoisting check tests a node for, one bit a class.
const REDUCE_SYMBOL = 1;
const REDUCE_STATEMENT = 1 << 1;
const REDUCE_DEFUN = 1 << 2;
const REDUCE_OPAQUE = 1 << 3;
/**
* Installs webpack's `reduce_vars`, terser's flow analysis, and the walk that
* runs it. It is terser's, except that the functions a hoisted function reads
* are ordered by one pass over them rather than one pass per function read.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installReduce = (modules) => {
const { pushChildrenBackwards, walkNode } = modules.ast;
const { ast, common, flags, inference } = modules;
const {
isArrayNode,
isAssignNode,
isAwaitNode,
BinaryNode,
isBinaryNode,
isCallNode,
isClassNode,
isClassExpressionNode,
isConditionalNode,
isDefunNode,
isDestructuringNode,
isExitNode,
isExpansionNode,
isForInNode,
isForOfNode,
isLambdaNode,
isNewNode,
isSyntaxNode,
NumberNode,
isObjectKeyValNode,
isPropAccessNode,
isScopeNode,
isSequenceNode,
isSimpleStatementNode,
isSymbolNode,
isSymbolConstNode,
isSymbolDeclarationNode,
isSymbolDefunNode,
isSymbolFunargNode,
isSymbolLambdaNode,
isSymbolLetNode,
isSymbolRefNode,
isThisNode,
UnaryPrefixNode,
isVarDefLikeNode,
isYieldNode,
createWalker,
walk,
walk_body: walkBody
} = ast;
/**
* @param {Node} orig where its position comes from
* @returns {Node} `void 0`, which no binding named `undefined` shadows
*/
const makeVoid0 = (orig) =>
makeNode(UnaryPrefixNode, orig, {
operator: "void",
argument: makeNode(NumberNode, orig, { value: 0 })
});
const {
lazy_op: lazyOperators,
is_modified: isModified,
is_lhs: isLhs
} = inference;
const {
read_property: readProperty,
has_break_or_continue: hasBreakOrContinue,
is_recursive_ref: isRecursiveRef
} = common;
const {
INLINED,
TOP,
CLEAR_BETWEEN_PASSES,
clear_flag: clearFlag,
set_flag: setFlag
} = flags;
// Which of the kinds the reduce phase tests a node is, read off its kind
// once rather than by a chain of tests per visited node.
const kindBitsOf = ast.kindBits([
["Symbol", REDUCE_SYMBOL],
["Statement", REDUCE_STATEMENT],
["Defun", REDUCE_DEFUN],
["Scope", REDUCE_OPAQUE],
["SimpleStatement", REDUCE_OPAQUE]
]);
/**
* Whether a definition is a sloppy function declared in a block, which Annex B
* makes a `var` holding `undefined` until its statement runs.
* @param {Definition} definition the definition
* @param {Compressor} compressor the compressor
* @returns {boolean} true for one terser takes as set from the scope's start
*/
const isBlockFunction = (definition, compressor) => {
if (
modules.corrections === undefined ||
!modules.corrections.enabled ||
compressor.option("module")
) {
return false;
}
const statements = ast.bodyOf(definition.scope.node);
if (!Array.isArray(statements)) return false;
// Any of its declarations may be the block's, with a `var`, a parameter
// or a function of the scope's own holding the name before it runs.
const { identifiers, init } = definition;
let inBlock = false;
for (let i = 0; i < identifiers.length && !inBlock; i++) {
const symbol = identifiers[i];
inBlock =
isSymbolDefunNode(symbol) &&
(init && init.id === symbol
? !statements.includes(init)
: !statements.some(
(/** @type {Node} */ statement) => statement.id === symbol
));
}
if (!inBlock) return false;
// A module is strict, scoping the function to its block, which terser
// does for strict code it can see but not for a module it was not told of.
let top = definition.scope;
while (top.upper !== null) top = top.upper;
return !(
/** @type {Node[]} */ (ast.bodyOf(top.node)).some(
(/** @type {Node} */ statement) =>
ast.isExportNode(statement) || ast.isImportNode(statement)
)
);
};
// The definitions `isBlockFunction` holds for, worked out once a pass.
/** @type {WeakSet<Definition>} */
const blockFunctions = new WeakSet();
/**
* Whether a reference to a block's function reads its block's own binding,
* set on the block's entry, rather than the `var` Annex B sets later.
* @param {ReduceWalker} walker the walker marking
* @param {Definition} definition the function's definition
* @returns {boolean} true inside the block declaring it
*/
const readsBlockBinding = (walker, definition) => {
const declaration = definition.init;
const scope = definition.scope.node;
for (let i = 0, node; (node = walker.parent(i)) && node !== scope; i++) {
const body = ast.bodyOf(node);
if (
Array.isArray(body) &&
body.includes(/** @type {Node} */ (declaration))
) {
return true;
}
}
return false;
};
/**
* Whether a module's own binding is reached from outside only through its
* export, which importers read but cannot write.
* @param {Compressor} compressor the compressor
* @param {Definition} definition a variable
* @returns {boolean} true when the improve phase reads it as the module does
*/
const onlyExported = (compressor, definition) => {
// A function or class read elsewhere would be copied, the export kept too.
if (
modules.improvements === undefined ||
!modules.improvements.enabled ||
!definition.export ||
isSymbolDefunNode(definition.identifiers[0]) ||
isLambdaNode(definition.init) ||
isClassNode(definition.init)
) {
return false;
}
const flag = definition.export;
definition.export = 0;
const exposed = compressor.exposed(definition);
definition.export = flag;
return !exposed;
};
/**
* Clears what the analysis records on a definition.
* @param {Compressor} compressor the compressor
* @param {Definition} definition the definition
* @returns {void}
*/
const resetDefinition = (compressor, definition) => {
definition.assignments = 0;
definition.patternWrites = 0;
definition.chained = false;
definition.direct_access = false;
definition.escaped = 0;
definition.leaked = false;
definition.recursive_refs = 0;
definition.references = [];
definition.single_use = undefined;
if (isBlockFunction(definition, compressor)) {
blockFunctions.add(definition);
} else {
blockFunctions.delete(definition);
}
definition.copyOf = undefined;
definition.readUnset = false;
definition.declaredWithValue = false;
if (
definition.scope.isPinned() ||
(isSymbolFunargNode(definition.identifiers[0]) &&
definition.scope.usesArguments)
) {
definition.fixed = false;
} else if (
isSymbolConstNode(definition.identifiers[0]) ||
!compressor.exposed(definition) ||
onlyExported(compressor, definition) ||
(modules.improvements !== undefined &&
modules.improvements.enabled &&
isSymbolLetNode(definition.identifiers[0]))
) {
// As swc and oxc assume, another script writes no top-level `let`.
definition.fixed = definition.init;
} else {
definition.fixed = false;
}
};
/**
* Whether a binding is a `let` without an initializer, which reads as
* `undefined` only once its declaration runs: before, a read throws.
* @param {Definition} definition a definition
* @returns {boolean} true for `let x;`
*/
const isUninitializedLet = (definition) =>
modules.corrections !== undefined &&
modules.corrections.enabled &&
ast.isSymbolLetNode(definition.identifiers[0]);
/**
* @param {ReduceWalker} walker the walker
* @param {Compressor} compressor the compressor
* @param {Node} node the node of the scope whose variables are reset
* @returns {void}
*/
const resetVariables = (walker, compressor, node) => {
const { variables } = node.block_scope;
for (let i = 0; i < variables.length; i++) {
const definition = variables[i];
resetDefinition(compressor, definition);
if (definition.fixed === null) {
walker.defs_to_safe_ids.set(definition.id, walker.safe_ids);
mark(walker, definition, !isUninitializedLet(definition));
} else if (definition.fixed) {
walker.loop_ids.set(definition.id, walker.in_loop);
mark(walker, definition, true);
}
}
};
/**
* @param {Compressor} compressor the compressor
* @param {Node} node a node that may carry a block scope
* @returns {void}
*/
const resetBlockVariables = (compressor, node) => {
// A function's, class's or program's node holds its own scope, not a block's.
if (node.block_scope && !isScopeNode(node)) {
const { variables } = node.block_scope;
for (let i = 0; i < variables.length; i++) {
resetDefinition(compressor, variables[i]);
}
}
};
/**
* @param {ReduceWalker} walker the walker
* @returns {void}
*/
const push = (walker) => {
walker.safe_ids = Object.create(walker.safe_ids);
};
/**
* @param {ReduceWalker} walker the walker
* @returns {void}
*/
const pop = (walker) => {
walker.safe_ids = Object.getPrototypeOf(walker.safe_ids);
};
/**
* @param {ReduceWalker} walker the walker
* @param {Definition} definition the definition
* @param {boolean} safe whether it is safe to read
* @returns {void}
*/
const mark = (walker, definition, safe) => {
walker.safe_ids[definition.id] = safe;
};
/**
* @param {ReduceWalker} walker the walker
* @param {Definition} definition the definition
* @returns {boolean} whether its fixed value can be read here
*/
const safeToRead = (walker, definition) => {
if (definition.single_use === "m") return false;
if (walker.safe_ids[definition.id]) {
if (definition.fixed === null || definition.fixed === undefined) {
const orig = definition.identifiers[0];
if (isSymbolFunargNode(orig) || orig.name === "arguments") {
return false;
}
definition.fixed = makeVoid0(orig);
}
return true;
}
return isDefunNode(definition.fixed);
};
/**
* @param {ReduceWalker} walker the walker
* @param {Definition} definition the definition
* @param {Scope} scope the scope assigning it
* @param {Node | true} value what is assigned, true for an update
* @returns {boolean} whether the assignment can become its fixed value
*/
const safeToAssign = (walker, definition, scope, value) => {
if (definition.fixed === undefined) return true;
let definitionSafeIds;
if (
definition.fixed === null &&
(definitionSafeIds = walker.defs_to_safe_ids.get(definition.id))
) {
definitionSafeIds[definition.id] = false;
walker.defs_to_safe_ids.delete(definition.id);
return true;
}
if (!hasOwn(walker.safe_ids, definition.id)) return false;
if (!safeToRead(walker, definition)) return false;
if (definition.fixed === false) return false;
if (
definition.fixed !== null &&
definition.fixed !== undefined &&
(!value || definition.references.length > definition.assignments)
) {
return false;
}
if (isDefunNode(definition.fixed)) {
return (
isSyntaxNode(value) &&
/** @type {Node} */ (definition.fixed).block_scope.upper === scope
);
}
return definition.identifiers.every(
(/** @type {Node} */ symbol) =>
!(
isSymbolConstNode(symbol) ||
isSymbolDefunNode(symbol) ||
isSymbolLambdaNode(symbol)
)
);
};
/**
* @param {ReduceWalker} walker the walker
* @param {Compressor} compressor the compressor
* @param {Definition} definition the definition
* @returns {boolean} whether it is read once, outside any loop it was set in
*/
const referencedOnce = (walker, compressor, definition) =>
compressor.option("unused") &&
!definition.scope.isPinned() &&
definition.references.length - definition.recursive_refs === 1 &&
walker.loop_ids.get(definition.id) === walker.in_loop;
/**
* @param {Node | false | null | undefined} value a value
* @returns {boolean} whether reading it cannot change it
*/
const isImmutable = (value) => {
if (!value) return false;
return ast.isConstant(value) || isLambdaNode(value) || isThisNode(value);
};
/**
* Records how far a definition's value leaves where it is read: `escaped` is
* how many properties are read off it before it does.
* @param {ReduceWalker} walker the walker
* @param {Definition} definition the definition
* @param {Scope} scope the scope reading it
* @param {Node} node the node reading it
* @param {Node | null | undefined} value its value there
* @param {number} level how far up the node is from the walker's current one
* @param {number} depth how many properties were read off it
* @returns {void}
*/
const markEscaped = (
walker,
definition,
scope,
node,
value,
level,
depth
) => {
const parent = walker.parent(level);
if (value) {
if (ast.isConstant(value)) return;
if (isClassExpressionNode(value)) return;
}
if (
(isAssignNode(parent) &&
(parent.operator === "=" || parent.logical) &&
node === parent.right) ||
(isCallNode(parent) && (node !== parent.callee || isNewNode(parent))) ||
(isExitNode(parent) &&
node === parent.argument &&
node.scope !== definition.scope) ||
(isVarDefLikeNode(parent) && node === parent.init) ||
(isYieldNode(parent) &&
node === parent.value &&
node.scope !== definition.scope)
) {
if (depth === 1) definition.leaked = true;
if (depth > 1 && !(value && ast.isConstantExpression(value, scope))) {
depth = 1;
}
if (!definition.escaped || definition.escaped > depth) {
definition.escaped = depth;
}
return;
} else if (
isArrayNode(parent) ||
isAwaitNode(parent) ||
(isBinaryNode(parent) && lazyOperators.has(parent.operator)) ||
(isConditionalNode(parent) && node !== parent.test) ||
isExpansionNode(parent) ||
(isSequenceNode(parent) && node === ast.tailNode(parent))
) {
markEscaped(walker, definition, scope, parent, parent, level + 1, depth);
} else if (isObjectKeyValNode(parent) && node === parent.value) {
const object = walker.parent(level + 1);
markEscaped(walker, definition, scope, object, object, level + 2, depth);
} else if (isPropAccessNode(parent) && node === parent.object) {
value = readProperty(
/** @type {Node} */ (value),
ast.terserProperty(parent)
);
// A hole, null only there, is a constant, as terser's hole node was.
if (value === null) return;
markEscaped(
walker,
definition,
scope,
parent,
value,
level + 1,
depth + 1
);
if (value) return;
}
if (level > 0) return;
if (isSequenceNode(parent) && node !== ast.tailNode(parent)) return;
if (isSimpleStatementNode(parent)) return;
definition.direct_access = true;
};
/**
* @returns {boolean} whether a pattern's computed keys are read as reads,
* not given up on as the names it writes
*/
const readsPatternKeys = () =>
modules.improvements !== undefined && modules.improvements.enabled;
/**
* Reads the computed keys of a pattern, which a loop head reads each pass.
* @param {Node} node a pattern or a declaration of one
* @param {ReduceWalker} walker the walker marking
* @returns {void}
*/
const readPatternKeys = (node, walker) => {
if (!readsPatternKeys()) return;
walk(node, (/** @type {Node} */ child) => {
if (isObjectKeyValNode(child) && child.computed) {
walkNode(child.key, walker);
readPatternKeys(/** @type {Node} */ (child.value), walker);
return true;
}
});
};
/**
* Gives up on every definition a pattern or loop head names. A computed key
* is read, not written, so the walker reads it instead.
* @param {Node} node the node
* @returns {void}
*/
const suppress = (node) => {
const improved = readsPatternKeys();
/**
* @param {Node} child a node of the pattern
* @returns {true | undefined} true where its children are walked already
*/
const write = (child) => {
if (improved && isObjectKeyValNode(child) && child.computed) {
walk(/** @type {Node} */ (child.value), write);
return true;
}
if (!isSymbolNode(child)) return;
const definition = child.definition;
if (!definition) return;
if (isSymbolRefNode(child)) {
definition.references.push(child);
definition.patternWrites++;
}
definition.fixed = false;
definition.copyOf = undefined;
};
walk(node, write);
};
/**
* terser's `reduce_vars` of an accessor.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceAccessor = (self, walker, descend, compressor) => {
push(walker);
resetVariables(walker, compressor, self);
descend();
pop(walker);
return true;
};
/**
* terser's `reduce_vars` of an assign.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceAssign = (self, walker, descend, compressor) => {
const node = self;
if (isDestructuringNode(node.left)) {
suppress(node.left);
return;
}
/**
* @returns {true | undefined} whether the walk is done
*/
const finishWalk = () => {
if (node.logical) {
walkNode(node.left, walker);
push(walker);
walkNode(node.right, walker);
pop(walker);
return true;
}
};
const symbol = node.left;
if (!isSymbolRefNode(symbol)) return finishWalk();
const definition = symbol.definition;
const safe = safeToAssign(walker, definition, symbol.scope, node.right);
definition.assignments++;
if (!safe) return finishWalk();
const fixed = definition.fixed;
if (!fixed && node.operator !== "=" && !node.logical) return finishWalk();
const isPlain = node.operator === "=";
const value = isPlain ? node.right : node;
if (isModified(compressor, walker, node, value, 0)) return finishWalk();
definition.references.push(symbol);
if (!node.logical) {
if (!isPlain) definition.chained = true;
definition.fixed = isPlain
? () => node.right
: () =>
makeNode(BinaryNode, node, {
operator: node.operator.slice(0, -1),
left: isSyntaxNode(fixed)
? fixed
: /** @type {() => Node} */ (fixed)(),
right: node.right
});
}
if (node.logical) {
mark(walker, definition, false);
push(walker);
walkNode(node.right, walker);
pop(walker);
return true;
}
mark(walker, definition, false);
walkNode(node.right, walker);
mark(walker, definition, true);
markEscaped(walker, definition, symbol.scope, node, value, 0, 1);
return true;
};
/**
* terser's `reduce_vars` of a binary.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @returns {boolean | void} true where its children are walked already
*/
const reduceBinary = (self, walker) => {
if (!lazyOperators.has(self.operator)) return;
walkNode(self.left, walker);
push(walker);
walkNode(self.right, walker);
pop(walker);
return true;
};
/**
* terser's `reduce_vars` of a block.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceBlock = (self, walker, descend, compressor) => {
resetBlockVariables(compressor, self);
if (
!self.block_scope ||
isScopeNode(self) ||
modules.improvements === undefined ||
!modules.improvements.enabled
) {
return;
}
// terser only resets a block's names: marked as a function's are, a block's
// `const` or `let` inlines as theirs do, unless `eval` or `with` pins the
// function; a write past it ends what is known.
const { variables } = self.block_scope;
for (let i = 0; i < variables.length; i++) {
const definition = variables[i];
if (
definition.fixed === null &&
(isSymbolConstNode(definition.identifiers[0]) ||
ast.isSymbolLetNode(definition.identifiers[0])) &&
!definition.scope.getDefunScope().isPinned()
) {
walker.defs_to_safe_ids.set(definition.id, walker.safe_ids);
mark(
walker,
definition,
isSymbolConstNode(definition.identifiers[0]) ||
!isUninitializedLet(definition)
);
}
}
};
/**
* terser's `reduce_vars` of a case.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @returns {boolean | void} true where its children are walked already
*/
const reduceCase = (self, walker) => {
push(walker);
walkNode(self.test, walker);
pop(walker);
push(walker);
walkBody(/** @type {Node[]} */ (self.consequent), walker);
pop(walker);
return true;
};
/**
* terser's `reduce_vars` of a class.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @returns {boolean | void} true where its children are walked already
*/
const reduceClass = (self, walker, descend) => {
clearFlag(self, INLINED);
push(walker);
descend();
pop(walker);
return true;
};
/**
* terser's `reduce_vars` of a class static block.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceClassStaticBlock = (self, walker, descend, compressor) => {
resetBlockVariables(compressor, self);
};
/**
* terser's `reduce_vars` of a conditional.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @returns {boolean | void} true where its children are walked already
*/
const reduceConditional = (self, walker) => {
walkNode(self.test, walker);
push(walker);
walkNode(/** @type {Node} */ (self.consequent), walker);
pop(walker);
push(walker);
walkNode(self.alternate, walker);
pop(walker);
return true;
};
// An optional call or property access pushes and never pops: a chain's
// conditions hold cumulatively, so the chain restores them all at its end.
/**
* terser's `reduce_vars` of a chain.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @returns {boolean | void} true where its children are walked already
*/
const reduceChain = (self, walker, descend) => {
const safeIds = walker.safe_ids;
descend();
walker.safe_ids = safeIds;
return true;
};
/**
* terser's `reduce_vars` of a call.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @returns {boolean | void} true where its children are walked already
*/
const reduceCall = (self, walker) => {
walkNode(self.callee, walker);
if (self.optional) push(walker);
for (const argument of self.arguments) walkNode(argument, walker);
return true;
};
/**
* terser's `reduce_vars` of a prop access.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @returns {boolean | void} true where its children are walked already
*/
const reducePropAccess = (self, walker) => {
if (!self.optional) return;
walkNode(self.object, walker);
push(walker);
if (self.computed) walkNode(self.property, walker);
return true;
};
/**
* terser's `reduce_vars` of a default.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @returns {boolean | void} true where its children are walked already
*/
const reduceDefault = (self, walker, descend) => {
push(walker);
descend();
pop(walker);
return true;
};
/**
* terser's `reduce_vars` of a lambda.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceLambda = (self, walker, descend, compressor) => {
clearFlag(self, INLINED);
push(walker);
resetVariables(walker, compressor, self);
let call;
const scope = self.block_scope;
if (
!self.id &&
!scope.usesArguments &&
!scope.isPinned() &&
isCallNode((call = walker.parent())) &&
call.callee === self &&
!call.arguments.some((/** @type {Node} */ argument) =>
isExpansionNode(argument)
) &&
self.params.every((/** @type {Node} */ name) => isSymbolNode(name))
) {
// An IIFE's parameters are read as variables the call's arguments set.
const iife = call;
for (const [i, name] of self.params.entries()) {
if (!name.definition) continue;
const definition = name.definition;
if (definition.identifiers.length > 1) continue;
// The test above has already found `arguments` unread, as terser asks.
if (definition.fixed === undefined) {
definition.fixed = () => iife.arguments[i] || makeVoid0(iife);
walker.loop_ids.set(definition.id, walker.in_loop);
mark(walker, definition, true);
} else {
definition.fixed = false;
}
}
}
descend();
pop(walker);
handleDefinedAfterHoist(self);
return true;
};
// Every symbol of the tree last indexed, in terser's walk order: the id of
// its definition, and whether it is written. Let go of after each pass.
let hoistOrder = new Int32Array(0);
let hoistWritten = new Uint8Array(0);
let hoistCount = 0;
/**
* Indexes every symbol under `root` in terser's walk order, once per pass,
* and records on each scope the range its node's symbols span, so
* `handleDefinedAfterHoist` scans that instead of walking each function's
* subtree again. Each pass analyzes the scopes afresh, so no two nodes share
* a scope here.
* @param {Node} root the tree walked
* @returns {void}
*/
const indexHoistOrder = (root) => {
let order = new Int32Array(Math.max(1024, hoistCount));
let written = new Uint8Array(order.length);
let count = 0;
/** @type {(Node | null)[]} */
const pending = [root];
/** @type {(Node | undefined)[]} */
const parents = [undefined];
/** @type {Node} */
let current;
/**
* @param {Node} child a child of the current node
* @returns {void}
*/
const pushChild = (child) => {
pending.push(child);
parents.push(current);
};
while (pending.length !== 0) {
const node = /** @type {Node | null} */ (pending.pop());
const holder = parents.pop();
if (node === null) {
// The end of a scope's subtree, which `holder` is the node of.
/** @type {Node} */ (holder).block_scope.hoistEnd = count;
continue;
}
current = node;
const kind = node[kindBitsOf];
if (kind & REDUCE_SYMBOL && node.definition) {
if (count === order.length) {
const grownOrder = new Int32Array(count * 2);
grownOrder.set(order);
order = grownOrder;
const grownWritten = new Uint8Array(count * 2);
grownWritten.set(written);
written = grownWritten;
}
order[count] = node.definition.id;
written[count] =
isSymbolDeclarationNode(node) || isLhs(node, holder) ? 1 : 0;
count++;
}
if (kind & REDUCE_OPAQUE) {
const scope = node.block_scope;
if (scope !== undefined) {
scope.hoistStart = count;
pending.push(null);
parents.push(node);
}
}
pushChildrenBackwards(node, pushChild);
}
hoistOrder = order;
hoistWritten = written;
hoistCount = count;
};
/**
* Lets go of the symbol order once a pass has read it.
* @returns {void}
*/
const releaseHoistOrder = () => {
hoistOrder = new Int32Array(0);
hoistWritten = new Uint8Array(0);
};
/**
* Stops inlining a variable a hoisted function reads where the function can
* be called before the variable is written. The functions a hoisted function
* calls are ordered by the earliest read of any function calling them.
* @param {Node} parent the function or top level whose functions are checked
* @returns {void}
*/
const handleDefinedAfterHoist = (parent) => {
/** @type {Node[]} */
const defuns = [];
// Only a statement holds a function declaration outside a nested scope,
// so the walk does not enter an expression, as terser's did.
walk(parent, (/** @type {Node} */ node) => {
if (node === parent) return;
const kind = node[kindBitsOf];
if (kind & REDUCE_DEFUN) {
defuns.push(node);
return true;
}
return (kind & REDUCE_OPAQUE) !== 0 || (kind & REDUCE_STATEMENT) === 0;
});
/** @type {Map<number, number[]>} */
const defunDependencies = new Map();
/** @type {Map<number, Definition[]>} */
const dependencies = new Map();
/** @type {Set<number>} */
const symbolsOfInterest = new Set();
/** @type {Set<number>} */
const defunsOfInterest = new Set();
for (const defun of defuns) {
const nameDefinition = defun.id.definition;
/** @type {Definition[]} */
const enclosingDefinitions = [];
for (const definition of defun.block_scope.enclosed) {
if (
definition.fixed === false ||
definition === nameDefinition ||
definition.scope.getDefunScope() !== parent.block_scope
) {
continue;
}
symbolsOfInterest.add(definition.id);
if (
definition.assignments === 0 &&
definition.identifiers.length === 1 &&
isSymbolDefunNode(definition.identifiers[0])
) {
defunsOfInterest.add(definition.id);
symbolsOfInterest.add(definition.id);
defunsOfInterest.add(nameDefinition.id);
symbolsOfInterest.add(nameDefinition.id);
let called = defunDependencies.get(nameDefinition.id);
if (called === undefined) {
called = [];
defunDependencies.set(nameDefinition.id, called);
}
called.push(definition.id);
continue;
}
enclosingDefinitions.push(definition);
}
if (enclosingDefinitions.length !== 0) {
dependencies.set(nameDefinition.id, enclosingDefinitions);
defunsOfInterest.add(nameDefinition.id);
symbolsOfInterest.add(nameDefinition.id);
}
}
if (dependencies.size === 0) return;
// Symbols of interest counted in walk order, so reads and writes compare.
/** @type {Map<number, number>} */
const defunFirstRead = new Map();
/** @type {Map<number, number>} */
const symbolLastWrite = new Map();
const { hoistStart, hoistEnd } = parent.block_scope;
for (let i = hoistStart; i < hoistEnd; i++) {
const id = hoistOrder[i];
if (hoistWritten[i] === 1 && symbolsOfInterest.has(id)) {
symbolLastWrite.set(id, i + 1);
}
if (defunsOfInterest.has(id) && !defunFirstRead.has(id)) {
defunFirstRead.set(id, i + 1);
}
}
const firstReadOfCaller = earliestCallerRead(
defunFirstRead,
defunDependencies
);
for (const [defun, definitions] of dependencies) {
const firstRead = firstReadOfCaller.get(defun);
if (firstRead === undefined) continue;
for (const definition of definitions) {
if (definition.fixed === false) continue;
const lastWrite = symbolLastWrite.get(definition.id) || 0;
if (firstRead < lastWrite) definition.fixed = false;
}
}
};
/**
* For each function, the earliest first read of itself or of any function
* calling it, directly or not: what terser's per-function propagation settles
* on, reached here visiting each function once, earliest reads first.
* @param {Map<number, number>} firstRead each function's first read
* @param {Map<number, number[]>} calls the functions each function calls
* @returns {Map<number, number>} the earliest read reaching each function
*/
const earliestCallerRead = (firstRead, calls) => {
/** @type {Map<number, number>} */
const earliest = new Map();
const roots = [...firstRead].sort((a, b) => a[1] - b[1]);
/** @type {number[]} */
const pending = [];
for (const [root, read] of roots) {
if (earliest.has(root)) continue;
earliest.set(root, read);
const called = calls.get(root);
if (called === undefined) continue;
for (const id of called) pending.push(id);
while (pending.length !== 0) {
const id = /** @type {number} */ (pending.pop());
if (earliest.has(id)) continue;
earliest.set(id, read);
const next = calls.get(id);
if (next !== undefined) for (const callee of next) pending.push(callee);
}
}
return earliest;
};
/**
* terser's `reduce_vars` of a do.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceDo = (self, walker, descend, compressor) => {
resetBlockVariables(compressor, self);
const savedLoop = walker.in_loop;
walker.in_loop = self;
push(walker);
walkNode(/** @type {Node} */ (self.body), walker);
if (hasBreakOrContinue(self)) {
pop(walker);
push(walker);
}
walkNode(self.test, walker);
pop(walker);
walker.in_loop = savedLoop;
return true;
};
/**
* terser's `reduce_vars` of a for.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceFor = (self, walker, descend, compressor) => {
resetBlockVariables(compressor, self);
if (self.init) walkNode(self.init, walker);
const savedLoop = walker.in_loop;
walker.in_loop = self;
push(walker);
if (self.test) walkNode(self.test, walker);
walkNode(/** @type {Node} */ (self.body), walker);
if (self.update) {
if (hasBreakOrContinue(self)) {
pop(walker);
push(walker);
}
walkNode(self.update, walker);
}
pop(walker);
walker.in_loop = savedLoop;
return true;
};
/**
* terser's `reduce_vars` of a for in.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceForIn = (self, walker, descend, compressor) => {
resetBlockVariables(compressor, self);
suppress(self.left);
walkNode(self.right, walker);
const savedLoop = walker.in_loop;
walker.in_loop = self;
push(walker);
// Its keys are read again on each pass.
readPatternKeys(self.left, walker);
walkNode(/** @type {Node} */ (self.body), walker);
pop(walker);
walker.in_loop = savedLoop;
return true;
};
/**
* terser's `reduce_vars` of an if.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @returns {boolean | void} true where its children are walked already
*/
const reduceIf = (self, walker) => {
walkNode(self.test, walker);
push(walker);
walkNode(/** @type {Node} */ (self.consequent), walker);
pop(walker);
if (self.alternate) {
push(walker);
walkNode(self.alternate, walker);
pop(walker);
}
return true;
};
/**
* terser's `reduce_vars` of a labeled statement.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @returns {boolean | void} true where its children are walked already
*/
const reduceLabeledStatement = (self, walker) => {
push(walker);
walkNode(/** @type {Node} */ (self.body), walker);
pop(walker);
return true;
};
/**
* terser's `reduce_vars` of a symbol catch.
* @param {Node} self the node
* @returns {boolean | void} true where its children are walked already
*/
const reduceSymbolCatch = (self) => {
self.definition.fixed = false;
};
/**
* Whether a name is read in the object a `for…in` or `for…of` declaring
* its own names iterates, which runs once in the scope around the loop.
* @param {ReduceWalker} walker the walker marking
* @param {Definition} definition the name's binding
* @param {Node} self the read
* @returns {boolean} true when the loop's scope opens right in the binding's
*/
const readsInLoopHead = (walker, definition, self) => {
if (modules.improvements === undefined || !modules.improvements.enabled) {
return false;
}
const { stack } = walker;
for (let i = stack.length - 1; i > 0; i--) {
const node = stack[i - 1];
if (isForInNode(node) || isForOfNode(node)) {
return (
node.right === stack[i] &&
node.block_scope === self.scope &&
self.scope.upper === definition.scope
);
}
if (node.block_scope !== undefined) return false;
}
return false;
};
/**
* terser's `reduce_vars` of a symbol ref.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceSymbolRef = (self, walker, descend, compressor) => {
const definition = self.definition;
// Outside its block, Annex B's `var` holds the function only once its
// statement ran, which terser takes as from the scope's start.
if (
definition.fixed &&
blockFunctions.has(definition) &&
!readsBlockBinding(walker, definition)
) {
definition.fixed = false;
}
definition.references.push(self);
if (
definition.references.length === 1 &&
!definition.fixed &&
isSymbolDefunNode(definition.identifiers[0])
) {
walker.loop_ids.set(definition.id, walker.in_loop);
}
let fixedValue;
if (definition.fixed === undefined || !safeToRead(walker, definition)) {
definition.fixed = false;
definition.copyOf = undefined;
definition.readUnset = true;
} else if (definition.fixed) {
fixedValue = ast.fixedValue(self);
if (isLambdaNode(fixedValue) && isRecursiveRef(walker, definition)) {
definition.recursive_refs++;
} else if (
fixedValue &&
!compressor.exposed(definition) &&
referencedOnce(walker, compressor, definition)
) {
definition.single_use =
(isLambdaNode(fixedValue) && !ast.isPinned(fixedValue)) ||
isClassNode(fixedValue) ||
((definition.scope === self.scope ||
readsInLoopHead(walker, definition, self)) &&
ast.isConstantExpression(fixedValue));
} else {
definition.single_use = false;
}
if (
isModified(
compressor,
walker,
self,
fixedValue,
0,
isImmutable(fixedValue)
)
) {
if (definition.single_use) {
definition.single_use = "m";
} else {
// A write through a copy still reads the name it copies.
if (isSymbolRefNode(fixedValue)) {
definition.copyOf = /** @type {Node | undefined} */ (fixedValue);
}
definition.fixed = false;
}
}
}
markEscaped(
walker,
definition,
self.scope,
self,
/** @type {Node | undefined} */ (fixedValue),
0,
1
);
};
/**
* terser's `reduce_vars` of a toplevel.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceToplevel = (self, walker, descend, compressor) => {
for (const definition of self.globals.values()) {
resetDefinition(compressor, definition);
}
resetVariables(walker, compressor, self);
descend();
handleDefinedAfterHoist(self);
return true;
};
/**
* terser's `reduce_vars` of a try.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceTry = (self, walker, descend, compressor) => {
resetBlockVariables(compressor, self);
push(walker);
walkNode(self.block, walker);
pop(walker);
if (self.handler) {
push(walker);
walkNode(self.handler, walker);
pop(walker);
}
if (self.finalizer) walkNode(self.finalizer, walker);
return true;
};
/**
* terser's `reduce_vars` of a unary.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @returns {boolean | void} true where its children are walked already
*/
const reduceUnary = (self, walker) => {
const node = self;
if (node.operator !== "++" && node.operator !== "--") return;
const expression = node.argument;
if (!isSymbolRefNode(expression)) return;
const definition = expression.definition;
const safe = safeToAssign(walker, definition, expression.scope, true);
definition.assignments++;
if (!safe) return;
const fixed = definition.fixed;
if (!fixed) return;
definition.references.push(expression);
definition.chained = true;
definition.fixed = () =>
makeNode(BinaryNode, node, {
operator: node.operator.slice(0, -1),
left: makeNode(UnaryPrefixNode, node, {
operator: "+",
argument: isSyntaxNode(fixed) ? fixed : fixed()
}),
right: makeNode(NumberNode, node, {
value: 1
})
});
mark(walker, definition, true);
return true;
};
/**
* terser's `reduce_vars` of a var def.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceVarDef = (self, walker, descend, compressor) => {
const node = self;
if (isDestructuringNode(node.id)) {
suppress(node.id);
return;
}
const definition = node.id.definition;
// A top-level `let` the program keeps for other scripts holds only what
// it starts with: one starting empty reads no shorter as `void 0`.
if (
!node.init &&
isSymbolLetNode(node.id) &&
compressor.exposed(definition)
) {
definition.fixed = false;
return;
}
if (!node.init && isUninitializedLet(definition)) {
mark(walker, definition, true);
}
if (node.init) {
definition.declaredWithValue = true;
// An exported binding holding a function or class stays for
// importers, so a copy of its value only adds to it; terser copies
// a `const`'s, so only the improvements keep that out.
if (
definition.export &&
(isLambdaNode(node.init) || isClassNode(node.init)) &&
(!isSymbolConstNode(node.id) ||
(modules.improvements !== undefined && modules.improvements.enabled))
) {
definition.fixed = false;
return;
}
if (safeToAssign(walker, definition, node.id.scope, node.init)) {
definition.fixed = () => node.init;
walker.loop_ids.set(definition.id, walker.in_loop);
mark(walker, definition, false);
descend();
mark(walker, definition, true);
return true;
}
definition.fixed = false;
}
};
/**
* terser's `reduce_vars` of a using declarator.
* @param {Node} self the node
* @returns {boolean | void} true where its children are walked already
*/
const reduceUsingDef = (self) => {
suppress(self.id);
};
/**
* terser's `reduce_vars` of a while.
* @param {Node} self the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceWhile = (self, walker, descend, compressor) => {
resetBlockVariables(compressor, self);
const savedLoop = walker.in_loop;
walker.in_loop = self;
push(walker);
descend();
pop(walker);
walker.in_loop = savedLoop;
return true;
};
// Each kind's `reduce_vars`, which marks what a variable is known to hold as
// the walk reaches it, by terser's class; a kind absent walks its children.
const REDUCE_VARS = kindValues({
Accessor: reduceAccessor,
Assign: reduceAssign,
Binary: reduceBinary,
Block: reduceBlock,
Call: reduceCall,
Case: reduceCase,
Chain: reduceChain,
Class: reduceClass,
ClassStaticBlock: reduceClassStaticBlock,
Conditional: reduceConditional,
Default: reduceDefault,
Do: reduceDo,
For: reduceFor,
ForIn: reduceForIn,
If: reduceIf,
LabeledStatement: reduceLabeledStatement,
Lambda: reduceLambda,
PropAccess: reducePropAccess,
SymbolCatch: reduceSymbolCatch,
SymbolRef: reduceSymbolRef,
Toplevel: reduceToplevel,
Try: reduceTry,
Unary: reduceUnary,
UsingDef: reduceUsingDef,
VarDef: reduceVarDef,
While: reduceWhile
});
/**
* @typedef {(node: Node, walker: ReduceWalker, descend: () => void, compressor: Compressor) => boolean | void} ReduceVariables
*/
/**
* terser's `reduce_vars` of a node.
* @param {Node} node the node
* @param {ReduceWalker} walker the walker marking
* @param {() => void} descend walks its children
* @param {Compressor} compressor the compressor
* @returns {boolean | void} true where its children are walked already
*/
const reduceVariables = (node, walker, descend, compressor) => {
const reduce = /** @type {ReduceVariables | undefined} */ (
/** @type {number | ReduceVariables | undefined} */ (node[REDUCE_VARS])
);
return reduce === undefined
? undefined
: reduce(node, walker, descend, compressor);
};
/**
* terser's `reset_opt_flags`: each node's flags cleared for a pass, and what
* `reduce_vars` knows of each variable worked out again.
* @param {Node} self the top level
* @param {Compressor} compressor the compressor
* @returns {void}
*/
ast.resetOptimizerFlags = function resetOptimizerFlags(self, compressor) {
const reduceVars = compressor.option("reduce_vars");
const preparation = /** @type {ReduceWalker} */ (
createWalker(
(/** @type {Node} */ node, /** @type {() => void} */ descend) => {
clearFlag(node, CLEAR_BETWEEN_PASSES);
if (reduceVars) {
if (
compressor.top_retain &&
isDefunNode(node) &&
preparation.parent() === self
) {
setFlag(node, TOP);
}
return reduceVariables(node, preparation, descend, compressor);
}
}
)
);
// Whether a definition is assigned before it is read, one layer a branch.
preparation.safe_ids = Object.create(null);
preparation.in_loop = null;
preparation.loop_ids = new Map();
preparation.defs_to_safe_ids = new Map();
if (reduceVars) indexHoistOrder(self);
walkNode(self, preparation);
if (reduceVars) releaseHoistOrder();
};
};
/**
* @typedef {object} SourceMapOptions
* @property {string | null | undefined} file the minified file's name
* @property {string | null | undefined} root the sources' root
* @property {string | RawSourceMap | DecodedSourceMap | IndexSourceMap | null} orig the map the input already carries, or its JSON
* @property {Record<string, string> | null} files each source's content, where the map includes it
*/
/**
* @typedef {object} SourceMapBuilder
* @property {(source: string, generatedLine: number, generatedColumn: number, originalLine: number, originalColumn: number, name: string | undefined) => void} add records one mapping
* @property {(generatedLine: number, columns: number[], deltas: number[]) => void} shiftColumns moves a line's mappings past each column, in order, by its delta, as text there changed length; a column of `n - 0.5` moves the mapping at `n` too
* @property {() => BuiltSourceMap} getDecoded the map, decoded
* @property {() => EncodedSourceMap} getEncoded the map, encoded
* @property {() => void} destroy frees the input's map
*/
/** @typedef {import("../util/createMappings").DecodedSegment} DecodedSegment */
/** @typedef {import("webpack-sources").RawSourceMap} RawSourceMap */
/** @typedef {Omit<RawSourceMap, "mappings"> & { mappings: DecodedSegment[][] }} DecodedSourceMap */
/** @typedef {{ version: number, file?: string, sections: { offset: { line: number, column: number }, map: string | RawSourceMap | DecodedSourceMap | IndexSourceMap }[] }} IndexSourceMap a map made of sections, each a map of its own, or its JSON */
/**
* The map a minify writes, its mappings decoded, less the keys that say nothing.
* @typedef {{ version: number, file?: string, names: string[], sourceRoot?: string, sources: string[], sourcesContent?: (string | null | undefined)[], mappings: DecodedSegment[][], ignoreList: number[] }} BuiltSourceMap
*/
/** @typedef {Omit<BuiltSourceMap, "mappings"> & { mappings: string }} EncodedSourceMap the map a minify writes, its mappings encoded */
/**
* @typedef {object} SourceMapReader
* @property {string[]} sources the sources, normalized as readers resolve them
* @property {(string | null)[] | undefined} sourcesContent their content
* @property {string[]} names the names
* @property {DecodedSegment[][]} lines the segments of each generated line
*/
/**
* Reads a source map, flattening an index map's sections into one map, as
* `@jridgewell/trace-mapping`'s `AnyMap` reads it.
* @param {string | RawSourceMap | DecodedSourceMap | IndexSourceMap} input the map, or its JSON
* @returns {SourceMapReader} the map, decoded
*/
const readSourceMapInput = (input) => {
const { decodeMappings } = require("../util/createMappings");
const { normalizeSourceMapUrl } = require("../util/identifier");
/**
* @param {string | RawSourceMap | DecodedSourceMap | IndexSourceMap} map a map, or its JSON
* @returns {RawSourceMap | DecodedSourceMap | IndexSourceMap} it, parsed
*/
const parseMap = (map) => (typeof map === "string" ? JSON.parse(map) : map);
/**
* @param {RawSourceMap | DecodedSourceMap} parsed a map without sections
* @param {boolean} owned whether its decoded lines may be sorted in place
* @returns {SourceMapReader} it, decoded
*/
const readPlain = (parsed, owned) => {
const prefix = parsed.sourceRoot ? `${parsed.sourceRoot}/` : "";
const { mappings } = parsed;
/** @type {DecodedSegment[][]} */
let lines;
if (typeof mappings === "string") {
lines = decodeMappings(mappings);
} else if (Array.isArray(mappings)) {
lines = mappings.map((/** @type {DecodedSegment[]} */ line) => {
for (let i = 1; i < line.length; i++) {
if (line[i][0] < line[i - 1][0]) {
return (owned ? line : [...line]).sort((a, b) => a[0] - b[0]);
}
}
return line;
});
} else {
throw new Error(`invalid source map: ${JSON.stringify(parsed)}`);
}
return {
sources: parsed.sources.map((/** @type {string | null} */ source) =>
normalizeSourceMapUrl(prefix + (source || ""))
),
sourcesContent: parsed.sourcesContent,
names: parsed.names || [],
lines
};
};
const parsed = parseMap(input);
if (!("sections" in parsed)) {
return readPlain(parsed, typeof input === "string");
}
/** @type {SourceMapReader} */
const joined = { sources: [], sourcesContent: [], names: [], lines: [] };
/**
* @param {IndexSourceMap} map an index map
* @param {number} lineOffset the line its sections are offset by
* @param {number} columnOffset the column its sections are offset by
* @param {number} stopLine the line the next section starts at
* @param {number} stopColumn the column the next section starts at
*/
const addSections = (map, lineOffset, columnOffset, stopLine, stopColumn) => {
const { sections } = map;
for (let i = 0; i < sections.length; i++) {
const { offset } = sections[i];
let sectionStopLine = stopLine;
let sectionStopColumn = stopColumn;
if (i + 1 < sections.length) {
const next = sections[i + 1].offset;
sectionStopLine = Math.min(stopLine, lineOffset + next.line);
if (sectionStopLine === stopLine) {
sectionStopColumn = Math.min(stopColumn, columnOffset + next.column);
} else if (sectionStopLine < stopLine) {
sectionStopColumn = columnOffset + next.column;
}
}
const section = parseMap(sections[i].map);
if ("sections" in section) {
addSections(
section,
lineOffset + offset.line,
columnOffset + offset.column,
sectionStopLine,
sectionStopColumn
);
continue;
}
addSection(
readPlain(section, false),
lineOffset + offset.line,
columnOffset + offset.column,
sectionStopLine,
sectionStopColumn
);
}
};
/**
* @param {SourceMapReader} map a section's map
* @param {number} lineOffset its line
* @param {number} columnOffset its column
* @param {number} stopLine the line the next section starts at
* @param {number} stopColumn the column the next section starts at
*/
const addSection = (map, lineOffset, columnOffset, stopLine, stopColumn) => {
const sourcesOffset = joined.sources.length;
const namesOffset = joined.names.length;
const contents = /** @type {(string | null)[]} */ (joined.sourcesContent);
joined.sources.push(...map.sources);
joined.names.push(...map.names);
if (map.sourcesContent) contents.push(...map.sourcesContent);
else for (let i = 0; i < map.sources.length; i++) contents.push(null);
for (let i = 0; i < map.lines.length; i++) {
const lineIndex = lineOffset + i;
if (lineIndex > stopLine) return;
for (let j = joined.lines.length; j <= lineIndex; j++) {
joined.lines[j] = [];
}
const out = joined.lines[lineIndex];
const columnShift = i === 0 ? columnOffset : 0;
for (const segment of map.lines[i]) {
const column = columnShift + segment[0];
if (lineIndex === stopLine && column >= stopColumn) return;
if (segment.length === 1) {
out.push([column]);
} else if (segment.length === 4) {
out.push([
column,
sourcesOffset + segment[1],
segment[2],
segment[3]
]);
} else {
out.push([
column,
sourcesOffset + segment[1],
segment[2],
segment[3],
namesOffset + segment[4]
]);
}
}
}
};
addSections(parsed, 0, 0, Infinity, Infinity);
joined.sources = joined.sources.map((source) =>
normalizeSourceMapUrl(source || "")
);
return joined;
};
/**
* The original position a generated one maps to: the last segment at or
* before its column, as `originalPositionFor` finds it.
* @param {SourceMapReader} map the map
* @param {number} line the generated line, 1-based
* @param {number} column the generated column
* @returns {{ source: string | null, line: number, column: number, name: string | null }} the original position; `source` is null where none is mapped
*/
const findOriginalPosition = (map, line, column) => {
const none = { source: null, line: 0, column: 0, name: null };
if (line - 1 >= map.lines.length) return none;
const segments = map.lines[line - 1];
let low = 0;
let high = segments.length - 1;
let index = -1;
while (low <= high) {
const middle = low + ((high - low) >> 1);
const difference = segments[middle][0] - column;
if (difference === 0) {
index = middle;
break;
}
if (difference < 0) low = middle + 1;
else high = middle - 1;
}
if (index === -1) {
index = low - 1;
} else {
while (index > 0 && segments[index - 1][0] === column) index--;
}
if (index === -1) return none;
const segment = segments[index];
if (segment.length === 1) return none;
return {
source: map.sources[segment[1]],
line: segment[2] + 1,
column: segment[3],
name: segment.length === 5 ? map.names[segment[4]] : null
};
};
/**
* terser's `SourceMap`: builds the output's map as `@jridgewell/gen-mapping`
* does, mapping back through the input's own map, but setting a source's
* content once rather than with every mapping into it.
* @param {SourceMapOptions} options what to build the map from
* @param {boolean} corrected whether a node without a source, as the `enclose` wrapper, adds no entry to `sources`
* @returns {Generator<SourceMapReader, SourceMapBuilder, SourceMapReader | undefined>} the builder, once the input's map is read
*/
function* createSourceMap(options, corrected) {
const { encodeDecodedMappings } = require("../util/createMappings");
/** @type {Map<string, number>} */
const sourceIndexes = new Map();
/** @type {string[]} */
const sources = [];
/** @type {(string | null | undefined)[]} */
const builtContent = [];
/** @type {Map<string, number>} */
const nameIndexes = new Map();
/** @type {string[]} */
const names = [];
/** @type {DecodedSegment[][]} */
const lines = [];
/** @type {Record<string, string | null | undefined>} */
const sourcesContent = Object.create(null);
const { files } = options;
for (const name in files) {
if (Object.prototype.hasOwnProperty.call(files, name)) {
sourcesContent[name] = files[name];
}
}
/** @type {SourceMapReader | undefined} */
let originalMap;
if (options.orig) {
originalMap = yield readSourceMapInput(options.orig);
const { sourcesContent: contents } = /** @type {SourceMapReader} */ (
originalMap
);
if (contents) {
for (const [i, source] of /** @type {SourceMapReader} */ (
originalMap
).sources.entries()) {
const content = contents[i];
if (content) sourcesContent[source] = content;
}
}
}
/** @type {Set<string>} */
const withContent = new Set();
/**
* @param {string} source a source
* @returns {number} its index, added when new
*/
const sourceIndexOf = (source) => {
let index = sourceIndexes.get(source);
if (index === undefined) {
index = sources.push(source) - 1;
sourceIndexes.set(source, index);
}
return index;
};
/**
* Adds a segment, unless the one before it on its line already says the same.
* @param {number} generatedLine its line, 1-based
* @param {number} generatedColumn its column
* @param {string | null} source its source, null for an unmapped one
* @param {number} originalLine its original line, 1-based
* @param {number} originalColumn its original column
* @param {string | null | undefined} name its name
*/
const addSegment = (
generatedLine,
generatedColumn,
source,
originalLine,
originalColumn,
name
) => {
for (let i = lines.length; i < generatedLine; i++) lines[i] = [];
const line = lines[generatedLine - 1];
let index = line.length;
while (index > 0 && generatedColumn < line[index - 1][0]) index--;
const previous = index > 0 ? line[index - 1] : undefined;
/** @type {DecodedSegment} */
let segment;
if (!source) {
if (!previous || previous.length === 1) return;
segment = [generatedColumn];
} else {
const sourceIndex = sourceIndexOf(source);
let nameIndex = -1;
if (name) {
const known = nameIndexes.get(name);
if (known === undefined) {
nameIndex = names.push(name) - 1;
nameIndexes.set(name, nameIndex);
} else {
nameIndex = known;
}
}
if (sourceIndex === builtContent.length) builtContent[sourceIndex] = null;
if (
previous &&
previous.length !== 1 &&
sourceIndex === previous[1] &&
originalLine - 1 === previous[2] &&
originalColumn === previous[3] &&
nameIndex === (previous.length === 5 ? previous[4] : -1)
) {
return;
}
segment = name
? [
generatedColumn,
sourceIndex,
originalLine - 1,
originalColumn,
nameIndex
]
: [generatedColumn, sourceIndex, originalLine - 1, originalColumn];
}
line.splice(index, 0, segment);
};
/**
* @returns {BuiltSourceMap} the map, its mappings decoded, less the keys that say nothing
*/
const build = () => {
let length = lines.length;
while (length > 0 && lines[length - 1].length === 0) length--;
lines.length = length;
/** @type {BuiltSourceMap} */
const built = {
version: 3,
file: options.file || undefined,
names,
sourceRoot: options.root || undefined,
sources,
sourcesContent: builtContent,
mappings: lines,
ignoreList: []
};
if (
builtContent.every((content) => content === null || content === undefined)
) {
delete built.sourcesContent;
}
if (built.file === undefined) delete built.file;
if (built.sourceRoot === undefined) delete built.sourceRoot;
return built;
};
return {
add(
source,
generatedLine,
generatedColumn,
originalLine,
originalColumn,
name
) {
if (originalMap) {
const info = findOriginalPosition(
originalMap,
originalLine,
originalColumn
);
if (info.source === null) {
addSegment(generatedLine, generatedColumn, null, 0, 0, null);
return;
}
source = info.source;
originalLine = info.line;
originalColumn = info.column;
name = info.name || name;
}
addSegment(
generatedLine,
generatedColumn,
source,
originalLine,
originalColumn,
name
);
if (!withContent.has(source) && (source || !corrected)) {
withContent.add(source);
builtContent[sourceIndexOf(source)] = sourcesContent[source];
}
},
shiftColumns(generatedLine, columns, deltas) {
const line = lines[generatedLine - 1];
if (line === undefined) return;
let next = 0;
let delta = 0;
for (let i = 0; i < line.length; i++) {
const segment = line[i];
while (next < columns.length && columns[next] < segment[0]) {
delta += deltas[next++];
}
segment[0] += delta;
}
},
getDecoded() {
return build();
},
getEncoded() {
const built = build();
return { ...built, mappings: encodeDecodedMappings(built.mappings) };
},
destroy() {}
};
}
/**
* What property mangling asks of its options, as both of its walks read it.
* @typedef {object} PropertyNames
* @property {(name: string) => void} add records a name a walk finds, before any is renamed
* @property {(name: string) => string} mangle the name a property is renamed to, itself where kept
* @property {boolean} keepQuoted whether a name written quoted is kept
* @property {boolean} undeclared whether a name read off an undeclared global is found too
* @property {{ get: (index: number) => string }} identifiers the identifier source it names from
*/
/**
* terser's `propmangle.js`, what of it reads terser's tree before compressing:
* the ESTree mangler renames. It builds the builtin names a property keeps once.
* @param {TerserModules} modules terser's modules, with `domprops` among them
* @returns {{ reserveQuotedKeys: (toplevel: Node, reserved: string[]) => void, findAnnotatedProperties: (toplevel: Node) => Set<string>, propertyNames: (given: GivenPropertyMangleOptions, annotatedNames: Set<string>) => PropertyNames }} what property mangling reads and asks
*/
const createPropertyMangler = (modules) => {
const { walkNode } = modules.ast;
const { ast, domprops } = modules;
const {
isClassPrivatePropertyNode,
isConditionalNode,
isDotNode,
isDotHashNode,
isObjectKeyValNode,
isObjectPropertyNode,
isPrivateGetterNode,
isPrivateMethodNode,
isPrivateSetterNode,
isSequenceNode,
isStringNode,
isSubNode,
createWalker,
walk
} = ast;
const { base54 } = modules.scope;
const NUMERIC = /^-?[0-9]+(\.[0-9]+)?(e[+-][0-9]+)?$/;
/** @type {Set<string> | undefined} */
let builtins;
/**
* terser's `find_builtins`: the DOM's property names and every standard
* global's, which a property keeps unless `builtins` is set.
* @returns {Set<string>} the names
*/
const builtinNames = () => {
if (builtins !== undefined) return builtins;
builtins = new Set(domprops.domprops);
for (const name of [
"null",
"true",
"false",
"NaN",
"Infinity",
"-Infinity",
"undefined"
]) {
builtins.add(name);
}
// A global some engines lack reads as a function with no names of its own.
const optional = (/** @type {string} */ name) =>
/** @type {Record<string, { prototype?: EXPECTED_OBJECT } | undefined>} */ (
/** @type {typeof globalThis | Record<string, { prototype?: EXPECTED_OBJECT } | undefined>} */ (
global
)
)[name] || function missing() {};
for (const ctor of /** @type {{ prototype?: EXPECTED_OBJECT }[]} */ ([
Object,
Array,
Function,
Number,
String,
Boolean,
Error,
Math,
Date,
RegExp,
optional("Symbol"),
ArrayBuffer,
DataView,
decodeURI,
decodeURIComponent,
encodeURI,
encodeURIComponent,
// eslint-disable-next-line no-eval
eval,
EvalError,
Float32Array,
Float64Array,
Int8Array,
Int16Array,
Int32Array,
// The same names as the globals: a function's own are `length` and `name`.
Number.isFinite,
Number.isNaN,
JSON,
optional("Map"),
Number.parseFloat,
Number.parseInt,
optional("Promise"),
optional("Proxy"),
RangeError,
ReferenceError,
optional("Reflect"),
optional("Set"),
SyntaxError,
TypeError,
Uint8Array,
Uint8ClampedArray,
Uint16Array,
Uint32Array,
URIError,
optional("WeakMap"),
optional("WeakSet")
])) {
for (const name of Object.getOwnPropertyNames(ctor)) builtins.add(name);
if (ctor.prototype) {
for (const name of Object.getOwnPropertyNames(ctor.prototype)) {
builtins.add(name);
}
}
}
return builtins;
};
/**
* @param {Node} node a node a private name belongs to
* @returns {boolean} whether `#private` mangling renames it instead
*/
const isPrivate = (node) =>
isClassPrivatePropertyNode(node) ||
isPrivateMethodNode(node) ||
isPrivateGetterNode(node) ||
isPrivateSetterNode(node) ||
isDotHashNode(node);
/**
* Every string a property key may be read from.
* @param {Node} node where the key is computed
* @param {(name: string) => void} add receives each
* @returns {void}
*/
const addStrings = (node, add) => {
walkNode(
node,
createWalker((/** @type {Node} */ inner) => {
if (isSequenceNode(inner)) {
addStrings(ast.tailNode(inner), add);
} else if (isStringNode(inner)) {
add(/** @type {string} */ (inner.value));
} else if (isConditionalNode(inner)) {
addStrings(/** @type {Node} */ (inner.consequent), add);
addStrings(inner.alternate, add);
}
return true;
})
);
};
/**
* terser's `find_annotated_props`: the names marked for mangling.
* @param {Node} toplevel the tree
* @returns {Set<string>} the names
*/
const findAnnotated = (toplevel) => {
/** @type {Set<string>} */
const annotated = new Set();
walk(toplevel, (/** @type {Node} */ node) => {
if (isPrivate(node)) return;
if (isObjectKeyValNode(node)) {
if (!node.computed && node._annotations & MANGLE_PROP) {
annotated.add(node.key.name);
}
} else if (isObjectPropertyNode(node)) {
if (node._annotations & MANGLE_PROP) annotated.add(node.key.name);
} else if (isDotNode(node)) {
if (node._annotations & MANGLE_PROP) annotated.add(node.property.name);
} else if (
isSubNode(node) &&
isStringNode(node.property) &&
node._annotations & MANGLE_PROP
) {
annotated.add(/** @type {string} */ (node.property.value));
}
});
return annotated;
};
/**
* terser's `mangle_properties` options read into what both of its walks
* ask: which names a walk finds, and what each is renamed to.
* @param {GivenPropertyMangleOptions} given the `mangle.properties` options
* @param {Set<string>} annotatedNames the names marked for mangling
* @returns {PropertyNames} the names
*/
const propertyNames = (given, annotatedNames) => {
const options = /** @type {PropertyMangleOptions} */ (
/** @type {Omit<PropertyMangleOptions, "reserved"> & { reserved: string[] | string | null }} */ (
defaults(
given,
{
builtins: false,
cache: null,
debug: false,
keep_quoted: false,
nth_identifier: base54,
only_cache: false,
regex: null,
reserved: null,
undeclared: false,
only_annotated: false
},
true
)
)
);
const identifiers = options.nth_identifier;
const reservedOption = Array.isArray(options.reserved)
? options.reserved
: [options.reserved];
/** @type {Set<string>} */
const reserved = options.builtins
? new Set(reservedOption)
: new Set([...reservedOption, ...builtinNames()]);
let counter = -1;
/** @type {Map<string, string>} */
const cache = options.cache ? options.cache.props : new Map();
const onlyAnnotated = options.only_annotated;
const regex = options.regex && new RegExp(options.regex);
// `debug` is false, or the suffix a debug name ends in: `true` is none.
const debug = options.debug !== false;
const debugSuffix = options.debug === true ? "" : options.debug;
/** @type {Set<string>} */
const toMangle = new Set();
/** @type {Set<string>} */
const unmangleable = new Set();
// A name already handed out is not handed out again.
const handedOut = new Set(cache.values());
for (const mangled of handedOut) unmangleable.add(mangled);
const keepQuoted = Boolean(options.keep_quoted);
/**
* @param {string} name a property name
* @returns {boolean} whether it may be renamed
*/
const canMangle = (name) => {
if (unmangleable.has(name)) return false;
if (reserved.has(name)) return false;
if (options.only_cache) return cache.has(name);
return !NUMERIC.test(name);
};
/**
* @param {string} name a property name
* @returns {boolean} whether it is renamed
*/
const shouldMangle = (name) => {
if (onlyAnnotated && !annotatedNames.has(name)) return false;
if (regex && !regex.test(name)) return annotatedNames.has(name);
if (reserved.has(name)) return false;
return cache.has(name) || toMangle.has(name);
};
/**
* @param {string} name a property name found
* @returns {void}
*/
const add = (name) => {
if (canMangle(name)) {
toMangle.add(name);
} else if (
modules.corrections !== undefined &&
modules.corrections.enabled &&
handedOut.has(name) &&
!reserved.has(name) &&
!options.only_cache
) {
// A name the cache hands another property is renamed too, or the two collide.
toMangle.add(name);
}
if (!shouldMangle(name)) unmangleable.add(name);
};
/**
* @param {string} name a property name
* @returns {string} what it is renamed to
*/
const mangle = (name) => {
if (!shouldMangle(name)) return name;
let mangled = cache.get(name);
if (!mangled) {
if (debug) {
const debugName = `_$${name}$${debugSuffix}_`;
if (canMangle(debugName)) mangled = debugName;
}
if (!mangled) {
do {
mangled = identifiers.get(++counter);
} while (!canMangle(mangled));
}
cache.set(name, mangled);
}
return mangled;
};
return {
add,
mangle,
keepQuoted,
undeclared: Boolean(options.undeclared),
identifiers
};
};
return {
reserveQuotedKeys(toplevel, reserved) {
/**
* @param {string} name a quoted key
* @returns {void}
*/
const add = (name) => pushUnique(reserved, name);
walkNode(
toplevel,
createWalker((/** @type {Node} */ node) => {
if (isObjectKeyValNode(node) && node.quote) {
add(node.key.name);
} else if (isObjectPropertyNode(node) && node.quote) {
add(node.key.name);
} else if (isSubNode(node)) {
addStrings(node.property, add);
}
})
);
},
findAnnotatedProperties: (toplevel) => findAnnotated(toplevel),
propertyNames
};
};
/**
* Names held across minifies, which a minify reads as a map and writes back
* as an object.
* @internal
* @typedef {{ props?: Map<string, string> | Record<string, string> }} NameCacheEntry
*/
/**
* terser's `mangle.properties` options, as given.
* @internal
* @typedef {Omit<Partial<PropertyMangleOptions>, "cache"> & { cache?: NameCacheEntry | null }} GivenPropertyMangleOptions
*/
/**
* terser's `mangle` options, as `minifySyncOrAsync` defaults them.
* @internal
* @typedef {object} DriverMangleOptions
* @property {NameCacheEntry | null} cache the names given so far, where names are cached
* @property {boolean} eval whether names a scope reading `eval` sees are mangled
* @property {boolean} ie8 whether the output works around that engine
* @property {boolean | RegExp} keep_classnames which class names survive
* @property {boolean | RegExp} keep_fnames which function names survive
* @property {boolean} module whether the source is a module
* @property {IdentifierSource} nth_identifier where mangled names come from
* @property {false | GivenPropertyMangleOptions} properties how property names are mangled, false where they are kept
* @property {string[]} reserved the names never given
* @property {boolean} safari10 whether the output works around that engine
* @property {boolean} toplevel whether top-level names are mangled
*/
/**
* terser's `sourceMap` options, as `minifySyncOrAsync` defaults them.
* @internal
* @typedef {object} DriverSourceMapOptions
* @property {boolean} asObject whether `map` is answered as an object rather than as JSON
* @property {string | RawSourceMap | DecodedSourceMap | IndexSourceMap | null} content the input's map or its JSON, `"inline"` for the one the source ends in
* @property {string | null} filename the minified file's name
* @property {boolean} includeSources whether the map holds the sources' content
* @property {string | null} root the sources' root
* @property {string | null} url where the code says its map is, `"inline"` for the map itself
*/
/**
* terser's `format` options as given, and the keys asking for a tree back.
* @internal
* @typedef {Partial<FormatSettings> & { ast?: boolean, code?: boolean, spidermonkey?: boolean }} DriverFormatOptions
*/
/**
* terser's `minify` options, as `minifySyncOrAsync` defaults them before it
* reads them.
* @internal
* @typedef {object} DriverOptions
* @property {boolean | Partial<CompressOptions>} compress what the compressor does, or false to leave the tree as read
* @property {number | undefined} ecma which edition the output may use
* @property {boolean | string} enclose the parameters and arguments of a function the code is wrapped in, false for none
* @property {boolean} ie8 whether the output works around that engine
* @property {boolean | RegExp | undefined} keep_classnames which class names survive
* @property {boolean | RegExp} keep_fnames which function names survive
* @property {false | DriverMangleOptions} mangle which names are renamed, false to keep them
* @property {boolean} module whether the source is a module
* @property {{ vars?: NameCacheEntry, props?: NameCacheEntry } | null} nameCache names held across minifies, which this writes into
* @property {DriverFormatOptions | null} output the older spelling of `format`
* @property {DriverFormatOptions} format how the output is written, `output` where only it was given
* @property {ParseOptions & { spidermonkey?: boolean }} parse how the source is read, the tree read written into it
* @property {boolean | DriverMangleOptions | undefined} rename whether names are renamed
* @property {boolean} safari10 whether the output works around that engine
* @property {false | DriverSourceMapOptions} sourceMap how a source map is written, false for none
* @property {boolean} spidermonkey whether the sources are ESTree trees
* @property {boolean} timings whether how long each step took is answered
* @property {boolean} toplevel whether top-level names may be renamed or dropped
* @property {boolean | "verbose"} warnings whether warnings are reported
* @property {string | false} wrap the name the code is wrapped as a CommonJS module under, false for none
*/
/**
* ESTree programs a minify with `parse.spidermonkey` reads: one, a list, or
* each under its name.
* @internal
* @typedef {NodeOf<"Program"> | NodeOf<"Program">[] | ProgramsByName} EstreePrograms
*/
/**
* @internal
* @typedef {Record<string, NodeOf<"Program">>} ProgramsByName ESTree programs, each under its index in a list or its name
*/
/**
* @internal
* @typedef {Record<string, DriverOptions[keyof DriverOptions]>} DriverOptionGroup the options, or one group of them, read by name
*/
/** @typedef {MinifyOutput & { timings?: Record<string, number> }} DriverOutput what a minify answers with, and how long each step took where asked */
/**
* Installs webpack's `minify`, terser's driver phase by phase. It skips the
* walk renaming `#private` members where the input names none.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installMinify = (modules) => {
const { ast, parse: parseModule, output } = modules;
const { isToplevelNode, walk } = ast;
const { createCompressor } = modules.compress;
const { findAnnotatedProperties, propertyNames, reserveQuotedKeys } =
createPropertyMangler(modules);
/**
* @param {SourceMapOptions} options what to build the map from
* @returns {Generator<SourceMapReader, SourceMapBuilder, SourceMapReader | undefined>} the builder
*/
const SourceMap = (options) =>
createSourceMap(
options,
modules.corrections !== undefined && modules.corrections.enabled
);
const { base54 } = modules.scope;
/**
* Moves a comment whose code the compressor dropped onto the next node kept,
* where the printer's own filter then decides it; terser drops it.
* @param {Node} toplevel the compressed tree
* @param {boolean} some whether only a license or `@preserve` comment is kept
* @returns {void}
*/
const keepImportantComments = (toplevel, some) => {
/** @type {TokenTables | null} */
const tables = toplevel.tokenTables;
if (!tables) return;
// A node's tokens are numbers, but for a token handed in whole.
/** @type {Set<NodeToken | number>} */
const starts = new Set();
/** @type {Set<NodeToken | number>} */
const ends = new Set();
walk(toplevel, (/** @type {Node} */ node) => {
starts.add(node.startToken);
ends.add(node.endToken);
});
const sorted = [...starts]
.filter((token) => typeof token === "number")
.sort((a, b) => a - b);
tables.forEachCommentGap((token, comments) => {
// A gap a kept node starts or ends at prints its comments.
if (starts.has(token) || ends.has(token) || ends.has(token - 1)) return;
const important = comments.filter(
(/** @type {Comment} */ comment) =>
(comment.type === "comment1" || comment.type === "comment2") &&
(!some || IMPORTANT_COMMENT.test(comment.value))
);
if (important.length === 0) return;
let low = 0;
let high = sorted.length;
while (low < high) {
const middle = (low + high) >> 1;
if (sorted[middle] > token) high = middle;
else low = middle + 1;
}
if (low === sorted.length) {
// Past every node kept, the program's last token prints it after.
const end = /** @type {NodeToken} */ (tables.of(toplevel.endToken));
end.comments_after = [...important, ...end.comments_after];
} else {
const next = /** @type {NodeToken} */ (tables.of(sorted[low]));
next.comments_before = [...important, ...next.comments_before];
// The line break after them moves too, as in `keepCommentsOf`.
if (/** @type {NodeToken} */ (tables.of(token)).nlb) next.nlb = true;
}
for (const comment of important) {
comments.splice(comments.indexOf(comment), 1);
}
});
};
/**
* Drops what only scope analysis read from a tree given to `minify`, as
* terser does under `_destroy_ast` before printing.
* @param {Node} toplevel the tree
* @returns {void}
*/
const dropScopes = (toplevel) => {
walk(toplevel, (/** @type {Node} */ node) => {
if (node.block_scope) {
node.block_scope = /** @type {Scope} */ (
/** @type {Scope | undefined} */ (undefined)
);
}
});
};
/**
* terser's `log_input`: the options and sources written to a file in
* `TERSER_DEBUG_DIR`, before they are parsed.
* @param {MinifySources} files the sources
* @param {MinifyOptions | undefined} options the options
* @param {Partial<Pick<typeof import("fs"), "mkdirSync" | "writeFileSync">>} fs the file system written to
* @param {string} debugFolder the folder
* @returns {void}
*/
const logInput = (files, options, fs, debugFolder) => {
if (!(fs && fs.writeFileSync && fs.mkdirSync)) return;
try {
fs.mkdirSync(debugFolder);
} catch (err) {
if (/** @type {NodeJS.ErrnoException} */ (err).code !== "EEXIST") {
throw err;
}
}
const logPath = `${debugFolder}/terser-debug-${(Math.random() * 9999999) | 0}.log`;
options = options || {};
const optionsText = JSON.stringify(
options,
(_key, value) => {
if (typeof value === "function") {
return `[Function ${value.toString()}]`;
}
if (value instanceof RegExp) return `[RegExp ${value.toString()}]`;
return value;
},
4
);
/**
* @param {MinifySources} file a source, or the sources by name
* @returns {string} it as the log writes it
*/
const filesText = (file) => {
if (
typeof file === "object" &&
options.parse &&
options.parse.spidermonkey
) {
return JSON.stringify(file, null, 2);
} else if (typeof file === "object") {
return Object.keys(file)
.map(
(key) =>
`${key}: ${filesText(/** @type {Record<string, MinifySources>} */ (file)[key])}`
)
.join("\n\n");
} else if (typeof file === "string") {
return `\`\`\`\n${file}\n\`\`\``;
}
return file;
};
fs.writeFileSync(
logPath,
`Options: \n${optionsText}\n\nInput files:\n\n${filesText(files)}\n`
);
};
/**
* @param {string} code a source ending in an inline source map
* @returns {string | null} the map's JSON, or null where there is none
*/
const readSourceMap = (code) => {
const match =
/(?:^|[^.])\/\/# sourceMappingURL=data:application\/json(;[\w=-]*)?;base64,([+/0-9A-Za-z]*=*)\s*$/.exec(
code
);
if (!match) {
// eslint-disable-next-line no-console
console.warn("inline source map not found");
return null;
}
return Buffer.from(match[2], "base64").toString();
};
/**
* @param {keyof DriverOptions} name a top-level option
* @param {DriverOptionGroup} options the options
* @param {("compress" | "mangle" | "parse" | "format")[]} keys the option groups it also sets
* @returns {void}
*/
const setShorthand = (name, options, keys) => {
if (options[name]) {
for (const key of keys) {
if (options[key]) {
if (typeof options[key] !== "object") options[key] = {};
if (!(name in /** @type {DriverOptionGroup} */ (options[key]))) {
/** @type {DriverOptionGroup} */ (options[key])[name] =
options[name];
}
}
}
}
};
/**
* @param {NameCacheEntry | null | undefined} cache a name cache
* @returns {void}
*/
const initCache = (cache) => {
if (!cache) return;
if (!("props" in cache)) {
cache.props = new Map();
} else if (!(cache.props instanceof Map)) {
cache.props = mapFromObject(
/** @type {Record<string, string>} */ (cache.props)
);
}
};
/**
* @param {MinifySources} files what `minify` was given
* @returns {boolean} whether no source there can name a `#private` member
*/
const namesNoPrivateMember = (files) => {
// A tree is read as naming one: only a source can be scanned for it, and
// `format.ast` hands a tree back that may have gained one since.
if (isToplevelNode(files)) return false;
if (typeof files === "string") return !files.includes("#");
if (!files || typeof files !== "object" || Array.isArray(files)) {
return false;
}
for (const name of Object.keys(files)) {
if (typeof files[name] !== "string" || files[name].includes("#")) {
return false;
}
}
return true;
};
/**
* @param {DriverMangleOptions} mangle the mangle options, defaulted
* @returns {boolean} whether the tree printer names as they ask: no property, cache, old engine or `keep_*` regexp answering per call
*/
const manglesTree = (mangle) =>
!mangle.properties &&
!mangle.cache &&
!mangle.ie8 &&
!mangle.safari10 &&
!answersPerCall(mangle.keep_fnames) &&
!answersPerCall(mangle.keep_classnames);
/**
* The one source a minify prints as it reads it, which the tree printer
* prints from a `SyntaxTree` of it: nothing compressed or wrapped, names
* mangled only as `manglesTree` allows, and only the options it writes as
* `printEstree` does.
* @param {MinifySources} files the sources, or a tree
* @param {DriverOptions} options the options, defaulted
* @returns {{ name: string, text: string } | null} the source and the name it is read under, null where terser's tree is printed
*/
const sourcePrintedFromTree = (files, options) => {
const { corrections, improvements, treePrinting } = modules;
const { format, parse } = options;
if (
treePrinting === undefined ||
!treePrinting.enabled ||
options.compress !== false ||
(options.mangle !== false && !manglesTree(options.mangle)) ||
options.wrap ||
options.enclose ||
!parse ||
parse.spidermonkey ||
format.ast ||
format.spidermonkey ||
(hasOwn(format, "code") && !format.code) ||
// A callback is handed the node it reads a comment off.
typeof format.comments === "function" ||
corrections === undefined ||
!corrections.enabled ||
improvements === undefined ||
!improvements.enabled
) {
return null;
}
if (typeof files === "string") return { name: "0", text: files };
if (!files || typeof files !== "object" || isToplevelNode(files)) {
return null;
}
const names = Object.keys(files);
if (
names.length !== 1 ||
typeof (
/** @type {Record<string, MinifySources>} */ (files)[names[0]]
) !== "string"
) {
return null;
}
return {
name: names[0],
text: /** @type {Record<string, string>} */ (files)[names[0]]
};
};
/**
* terser's `minify_sync_or_async`: it yields where a source map may be read
* asynchronously, and returns the result.
* @param {MinifySources | EstreePrograms} files the sources, or a tree, made a list in place; with `parse.spidermonkey`, ESTree programs, which `MinifySources` leaves out
* @param {DriverOptions} options the options, as given: they are defaulted in place
* @returns {Generator<SourceMapReader, DriverOutput, SourceMapReader | undefined>} the driver
*/
function* minifySyncOrAsync(files, options) {
options = /** @type {DriverOptions} */ (
/** @type {Omit<DriverOptions, "format"> & { format: DriverFormatOptions | null }} */ (
defaults(
options,
{
compress: {},
ecma: undefined,
enclose: false,
ie8: false,
keep_classnames: undefined,
keep_fnames: false,
mangle: {},
module: false,
nameCache: null,
output: null,
format: null,
parse: {},
rename: undefined,
safari10: false,
sourceMap: false,
spidermonkey: false,
timings: false,
toplevel: false,
warnings: false,
wrap: false
},
true
)
)
);
const timings =
options.timings &&
/** @type {Record<string, number>} */ ({ start: Date.now() });
// Parsing writes its tree into `parse`: a copy keeps the caller's options free of it.
if (
options.parse &&
modules.corrections !== undefined &&
modules.corrections.enabled
) {
options.parse = { ...options.parse };
}
if (options.keep_classnames === undefined) {
options.keep_classnames = options.keep_fnames;
}
if (options.rename === undefined) {
options.rename = options.compress && options.mangle;
}
if (options.output && options.format) {
throw new Error(
"Please only specify either output or format option, preferrably format."
);
}
options.format = options.format || options.output || {};
setShorthand("ecma", options, ["parse", "compress", "format"]);
setShorthand("ie8", options, ["compress", "mangle", "format"]);
setShorthand("keep_classnames", options, ["compress", "mangle"]);
setShorthand("keep_fnames", options, ["compress", "mangle"]);
setShorthand("module", options, ["parse", "compress", "mangle"]);
setShorthand("safari10", options, ["mangle", "format"]);
setShorthand("toplevel", options, ["compress", "mangle"]);
setShorthand("warnings", options, ["compress"]);
/** @type {string[] | undefined} */
let quotedProperties;
if (options.mangle) {
options.mangle = /** @type {DriverMangleOptions} */ (
defaults(
options.mangle,
{
cache: options.nameCache && (options.nameCache.vars || {}),
eval: false,
ie8: false,
keep_classnames: false,
keep_fnames: false,
module: false,
nth_identifier: base54,
properties: false,
reserved: [],
safari10: false,
toplevel: false
},
true
)
);
if (options.mangle.properties) {
if (typeof options.mangle.properties !== "object") {
options.mangle.properties = {};
}
if (options.mangle.properties.keep_quoted) {
quotedProperties = options.mangle.properties.reserved;
if (!Array.isArray(quotedProperties)) quotedProperties = [];
options.mangle.properties.reserved = quotedProperties;
}
if (options.nameCache && !("cache" in options.mangle.properties)) {
options.mangle.properties.cache = options.nameCache.props || {};
}
}
initCache(options.mangle.cache);
initCache(
/** @type {GivenPropertyMangleOptions} */ (options.mangle.properties)
.cache
);
}
if (options.sourceMap) {
options.sourceMap = defaults(
options.sourceMap,
{
asObject: false,
content: null,
filename: null,
includeSources: false,
root: null,
url: null
},
true
);
}
const withoutPrivateNames = namesNoPrivateMember(
/** @type {MinifySources} */ (files)
);
// Parse.
if (timings) timings.parse = Date.now();
/** @type {Node} */
let toplevel;
// A source printed as read is printed from a `SyntaxTree` of it.
const treeSource = sourcePrintedFromTree(
/** @type {MinifySources} */ (files),
options
);
/** @type {ParsedTree | null} */
let parsedTree = null;
if (treeSource !== null) {
// A source map's sources read the sources as a list, as parsing has them.
if (typeof files === "string") files = [files];
options.parse.toplevel = null;
delete options.parse.spidermonkey;
options.parse.filename = treeSource.name;
parsedTree = parseModule.parseTree(
treeSource.text,
options.parse,
modules.readTreeTokens
);
if (options.sourceMap && options.sourceMap.content === "inline") {
options.sourceMap.content = readSourceMap(treeSource.text);
}
// Nothing reads terser's tree of it: it is neither built nor printed.
toplevel = /** @type {Node} */ (/** @type {Node | null} */ (null));
} else if (isToplevelNode(files)) {
toplevel = /** @type {Node} */ (/** @type {Node | NodeShape} */ (files));
} else {
if (
typeof files === "string" ||
(options.parse.spidermonkey && !Array.isArray(files))
) {
files = /** @type {string[] | NodeOf<"Program">[]} */ ([files]);
}
options.parse = options.parse || {};
options.parse.toplevel = null;
if (options.parse.spidermonkey) {
options.parse.toplevel = ast.adoptEstree(
/** @type {NodeOf<"Program">} */ (
Object.keys(files).reduce(
(/** @type {NodeOf<"Program"> | null} */ merged, name) => {
if (!merged) return /** @type {ProgramsByName} */ (files)[name];
merged.body = [
...merged.body,
.../** @type {ProgramsByName} */ (files)[name].body
];
return merged;
},
null
)
)
);
} else {
delete options.parse.spidermonkey;
for (const name in /** @type {Record<string, string>} */ (files)) {
if (!hasOwn(/** @type {Record<string, string>} */ (files), name)) {
continue;
}
options.parse.filename = name;
options.parse.toplevel = parseModule.parse(
/** @type {Record<string, string>} */ (files)[name],
options.parse
);
if (options.sourceMap && options.sourceMap.content === "inline") {
if (Object.keys(files).length > 1) {
throw new Error(
"inline source map only works with singular input"
);
}
options.sourceMap.content = readSourceMap(
/** @type {Record<string, string>} */ (files)[name]
);
}
}
}
if (options.parse.toplevel === null) {
throw new Error("no source file given");
}
toplevel = options.parse.toplevel;
}
if (
quotedProperties &&
/** @type {GivenPropertyMangleOptions} */ (
/** @type {DriverMangleOptions} */ (options.mangle).properties
).keep_quoted !== "strict"
) {
reserveQuotedKeys(toplevel, quotedProperties);
}
let annotatedProperties;
if (options.mangle && options.mangle.properties) {
annotatedProperties = findAnnotatedProperties(toplevel);
}
if (options.wrap) toplevel = ast.wrapCommonjs(toplevel, options.wrap);
if (options.enclose) toplevel = ast.wrapEnclose(toplevel, options.enclose);
if (
!options.wrap &&
!options.enclose &&
modules.improvements !== undefined &&
modules.improvements.enabled &&
(toplevel
? /** @type {Node[]} */ (toplevel.body).some(
(statement) =>
ast.isImportNode(statement) || ast.isExportNode(statement)
)
: parsedTree !== null && parsedTree.module)
) {
// Only a module imports or exports, its top-level names private to it.
if (options.compress) {
options.compress = {
.../** @type {Partial<CompressOptions>} */ (options.compress),
module: true
};
}
if (options.mangle) {
options.mangle = { ...options.mangle, module: true, toplevel: true };
}
}
if (timings) timings.rename = Date.now();
// Compress.
if (timings) timings.compress = Date.now();
if (options.compress) {
const compressor = createCompressor(
/** @type {Partial<CompressOptions>} */ (options.compress),
{
mangle_options: options.mangle
}
);
assignNativeLookups(compressor, modules.nativeObjects);
toplevel = compressor.compress(toplevel);
// Unset, the printer keeps what "some" does.
const comments = hasOwn(options.format, "comments")
? options.format.comments
: "some";
if (
comments &&
modules.corrections !== undefined &&
modules.corrections.enabled
) {
keepImportantComments(toplevel, comments === "some");
}
}
// Mangle.
if (timings) timings.scope = Date.now();
const printsCode = !hasOwn(options.format, "code") || options.format.code;
// terser's `_destroy_ast`: the tree is dropped once printed.
const destroysTree = printsCode && !options.format.ast;
const handsTreeBack = options.format.ast || options.format.spidermonkey;
const givenTree = isToplevelNode(files);
// What only the compressor read is dropped from a tree nobody keeps.
const releases = destroysTree && !handsTreeBack && !givenTree;
// A tree kept keeps no mapping name set for its one print.
/** @type {(PrintProperty | PrintPropertyDefinition)[] | null} */
const setMapNames = releases ? null : [];
if (timings) timings.mangle = timings.properties = Date.now();
/** @type {DriverFormatOptions | undefined} */
let formatOptions;
/** @type {OutputStream | undefined} */
let stream;
if (printsCode) {
formatOptions = { ...options.format };
if (destroysTree) formatOptions._destroy_ast = true;
if (options.sourceMap) {
if (options.sourceMap.includeSources && isToplevelNode(files)) {
throw new Error("original source content unavailable");
}
formatOptions.source_map = yield* SourceMap({
file: options.sourceMap.filename,
orig: options.sourceMap.content,
root: options.sourceMap.root,
files: options.sourceMap.includeSources
? /** @type {Record<string, string>} */ (files)
: null
});
}
delete formatOptions.ast;
delete formatOptions.code;
delete formatOptions.spidermonkey;
stream = output.OutputStream(formatOptions);
}
// A print made before the names are chosen, which they are then written
// into: the characters it counts are the ones a print of its own would.
/** @type {MangleRecording | null} */
let recorded = null;
let recordedText = "";
/** @type {TreeNames | null} */
let treeNames = null;
if (options.mangle && parsedTree !== null) {
treeNames = modules.mangleTree(
parsedTree,
/** @type {OutputStream} */ (stream),
/** @type {{ text: string }} */ (treeSource).text,
formatMangleOptions(options.mangle, base54),
!withoutPrivateNames
);
} else if (options.mangle) {
const mangleOptions = formatMangleOptions(options.mangle, base54);
const tree = /** @type {PrintProgram} */ (toplevel);
// Written now rather than once named, so one print serves both the
// count and the result; the count puts back what they changed.
const letCounts =
modules.writePendingLets === undefined
? null
: modules.writePendingLets(toplevel);
let analysis = /** @type {MinifierScopeAnalysis | undefined} */ (
modules.analyzeManglingScopes(
tree,
mangleOptions,
releases,
stream !== undefined && stream.options.shorthand
)
);
if (timings) timings.mangle = Date.now();
/** @type {IdentifierSource} */
const identifiers = mangleOptions.nth_identifier;
let namesPrivate = false;
if (
stream !== undefined &&
!givenTree &&
!options.mangle.properties &&
!(/** @type {MinifierScopeAnalysis} */ (analysis).namesPrivate) &&
modules.countsFrequency(identifiers) &&
printsAsString(stream.options)
) {
// Kept whole through this print: a comment written sends the count
// back to a print of its own.
stream.options._destroy_ast = false;
const printed = modules.printRecording(tree, stream, mangleOptions);
recorded = printed;
recordedText = stream.get();
if (stream.wroteComments) {
modules.computeEstreeCharFrequency(tree, mangleOptions, letCounts);
} else {
countRecording(identifiers, recordedText, printed);
if (letCounts !== null) countLetsBack(identifiers, letCounts);
identifiers.sort();
}
} else {
namesPrivate = modules.computeEstreeCharFrequency(
tree,
mangleOptions,
letCounts
);
}
modules.mangleManglingScopes(
/** @type {MinifierScopeAnalysis} */ (analysis),
mangleOptions
);
// terser renames the private names a direct `eval` reads, and its output
// no longer parses; the correct phase keeps them.
const { corrections } = modules;
const kept =
!withoutPrivateNames && corrections && corrections.enabled
? modules.privateNamesReadByEval(
/** @type {MinifierScopeAnalysis} */ (analysis)
)
: null;
analysis = undefined;
if (timings) timings.properties = Date.now();
modules.renameMangledEstree(
tree,
mangleOptions,
options.mangle.properties
? propertyNames(
options.mangle.properties,
/** @type {Set<string>} */ (annotatedProperties)
)
: null,
!withoutPrivateNames &&
namesPrivate &&
// A private method's `name` is its private name, which
// `keep_fnames` keeps as it keeps the other functions' names.
!(
corrections &&
corrections.enabled &&
mangleOptions.keep_fnames === true
),
setMapNames,
kept
);
}
// Format.
if (timings) timings.format = Date.now();
/** @type {DriverOutput} */
const result = {};
if (options.format.ast) {
result.ast = /** @type {NodeShape} */ (
/** @type {NodeShape | Node} */ (toplevel)
);
}
if (options.format.spidermonkey) {
result.ast = /** @type {NodeShape} */ (ast.toEstree(toplevel));
}
if (printsCode) {
// Dropped as terser drops it; a tree nobody was given is garbage.
if (destroysTree && givenTree) dropScopes(toplevel);
if (treeNames !== null && treeNames.recorded !== null) {
result.code = modules.patchRecording(
/** @type {OutputStream} */ (stream),
treeNames.text,
treeNames.recorded
);
} else if (parsedTree !== null) {
modules.printTree(
parsedTree,
/** @type {OutputStream} */ (stream),
/** @type {{ text: string }} */ (treeSource).text,
treeNames
);
result.code = /** @type {OutputStream} */ (stream).get();
} else if (recorded === null) {
modules.printProgram(
/** @type {PrintProgram} */ (toplevel),
/** @type {OutputStream} */ (stream),
releases && !options.mangle
);
result.code = /** @type {OutputStream} */ (stream).get();
} else {
result.code = modules.patchRecording(
/** @type {OutputStream} */ (stream),
recordedText,
recorded
);
}
if (options.sourceMap) {
const { sourceMap } = options;
Object.defineProperty(result, "map", {
configurable: true,
enumerable: true,
get() {
const map = /** @type {SourceMapBuilder} */ (
/** @type {DriverFormatOptions} */ (formatOptions).source_map
).getEncoded();
return (result.map = sourceMap.asObject
? map
: JSON.stringify(map));
},
set(value) {
Object.defineProperty(result, "map", { value, writable: true });
}
});
result.decoded_map = /** @type {SourceMapBuilder} */ (
/** @type {DriverFormatOptions} */ (formatOptions).source_map
).getDecoded();
if (sourceMap.url === "inline") {
const map =
typeof result.map === "object"
? JSON.stringify(result.map)
: result.map;
result.code += `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${Buffer.from(
/** @type {string} */ (map)
).toString("base64")}`;
} else if (sourceMap.url) {
result.code += `\n//# sourceMappingURL=${sourceMap.url}`;
}
}
}
if (setMapNames !== null) {
for (let i = 0; i < setMapNames.length; i++) {
setMapNames[i].mapName = undefined;
}
}
if (options.nameCache && options.mangle) {
if (options.mangle.cache) {
options.nameCache.vars = {
props: mapToObject(
/** @type {Map<string, string>} */ (options.mangle.cache.props)
)
};
}
if (options.mangle.properties && options.mangle.properties.cache) {
options.nameCache.props = {
props: mapToObject(
/** @type {Map<string, string>} */ (
options.mangle.properties.cache.props
)
)
};
}
}
if (formatOptions && formatOptions.source_map) {
formatOptions.source_map.destroy();
}
if (timings) {
timings.end = Date.now();
result.timings = {
parse: 1e-3 * (timings.rename - timings.parse),
rename: 1e-3 * (timings.compress - timings.rename),
compress: 1e-3 * (timings.scope - timings.compress),
scope: 1e-3 * (timings.mangle - timings.scope),
mangle: 1e-3 * (timings.properties - timings.mangle),
properties: 1e-3 * (timings.format - timings.properties),
format: 1e-3 * (timings.end - timings.format),
total: 1e-3 * (timings.end - timings.start)
};
}
return result;
}
/**
* @param {MinifySources} files the sources, or a tree
* @param {MinifyOptions=} options the options
* @param {typeof import("fs")=} fsModule where terser's debug log is written
* @returns {Promise<DriverOutput>} the result
*/
modules.minify = async (files, options, fsModule) => {
if (
fsModule &&
typeof process === "object" &&
process.env &&
typeof process.env.TERSER_DEBUG_DIR === "string"
) {
logInput(files, options, fsModule, process.env.TERSER_DEBUG_DIR);
}
const driver = minifySyncOrAsync(
files,
/** @type {DriverOptions} */ (options)
);
let yielded;
let step;
do {
step = driver.next(
/** @type {SourceMapReader | undefined} */ (await yielded)
);
yielded = step.value;
} while (!step.done);
return step.value;
};
/**
* terser's `minify_sync`: the same minify, for a caller handing over no
* source map that reads asynchronously.
* @param {MinifySources} files the sources, or a tree
* @param {MinifyOptions=} options the options
* @returns {DriverOutput} the result
*/
modules.minifySync = (files, options) => {
const driver = minifySyncOrAsync(
files,
/** @type {DriverOptions} */ (options)
);
let step = driver.next();
while (!step.done) step = driver.next(step.value);
return step.value;
};
};
const KEY = 0b00001000;
const MANGLE_PROP = 0b00010000;
// A token's flags, as terser's `ast.js` numbers them.
const TOKEN_NEWLINE_BEFORE = 0b0001;
const TOKEN_QUOTE_SINGLE = 0b0010;
const TOKEN_QUOTE_EXISTS = 0b0100;
const TOKEN_TEMPLATE_END = 0b1000;
/**
* terser's `ParsedToken`: a token, sealed, its booleans packed into `flags`.
*/
class ParsedToken {
/**
* @param {string} type the token's type
* @param {string} value its value
* @param {number} line its line, from 1
* @param {number} col its column, from 0
* @param {number} pos its offset
* @param {boolean} nlb whether a line break comes before it
* @param {Comment[]} commentsBefore the comments before it
* @param {Comment[]} commentsAfter the comments after it
* @param {(string | null)=} file its file
*/
constructor(
type,
value,
line,
col,
pos,
nlb,
commentsBefore,
commentsAfter,
file
) {
this.flags = nlb ? 1 : 0;
this.type = type;
this.value = value;
this.line = line;
this.col = col;
this.pos = pos;
this.comments_before = commentsBefore;
this.comments_after = commentsAfter;
this.file = file;
Object.seal(this);
}
/**
* @param {number} _depth how deep Node's inspector is
* @param {import("util").InspectContext} options its options
* @returns {string} the token, as Node's console shows it
*/
[Symbol.for("nodejs.util.inspect.custom")](_depth, options) {
/**
* @param {string} text text
* @returns {string} it, styled as special
*/
const special = (text) => options.stylize(text, "special");
const quote =
typeof this.value === "string" && this.value.includes("`") ? "'" : "`";
return `${special("[ParsedToken")} ${quote}${this.value}${quote} at ${this.line}:${this.col}${special("]")}`;
}
/**
* @returns {boolean} whether a line break comes before it
*/
get nlb() {
return Boolean(this.flags & TOKEN_NEWLINE_BEFORE);
}
/**
* @param {boolean} value whether a line break comes before it
*/
set nlb(value) {
this.flags = value
? this.flags | TOKEN_NEWLINE_BEFORE
: this.flags & ~TOKEN_NEWLINE_BEFORE;
}
/**
* @returns {string} the quote a string had, or ""
*/
get quote() {
if (!(this.flags & TOKEN_QUOTE_EXISTS)) return "";
return this.flags & TOKEN_QUOTE_SINGLE ? "'" : '"';
}
/**
* @param {string} quote the quote a string had, or ""
*/
set quote(quote) {
this.flags =
quote === "'"
? this.flags | TOKEN_QUOTE_SINGLE
: this.flags & ~TOKEN_QUOTE_SINGLE;
this.flags = quote
? this.flags | TOKEN_QUOTE_EXISTS
: this.flags & ~TOKEN_QUOTE_EXISTS;
}
/**
* @returns {boolean} whether it ends a template literal
*/
get template_end() {
return Boolean(this.flags & TOKEN_TEMPLATE_END);
}
/**
* @param {boolean} value whether it ends a template literal
*/
set template_end(value) {
this.flags = value
? this.flags | TOKEN_TEMPLATE_END
: this.flags & ~TOKEN_TEMPLATE_END;
}
}
// What each terser class of node says of its nodes, as data: the ESTree type
// they have, the fields a node of the kind is made from, and the values every
// node of it holds, which tell it apart from the other kinds of its type.
/** @typedef {{ kind: string, type: string, fields: string[], values: NodeProps, unguarded?: boolean, noFlags?: boolean, initializes?: boolean, holdsBlock?: boolean }} NodeKind */
/** @type {NodeKind[]} */
const NODE_KINDS = [
{
kind: "Debugger",
type: "DebuggerStatement",
fields: ["startToken", "endToken"],
values: {}
},
{
kind: "Directive",
type: "ExpressionStatement",
fields: ["directive", "expression", "startToken", "endToken"],
values: {}
},
{
kind: "SimpleStatement",
type: "ExpressionStatement",
fields: ["expression", "startToken", "endToken"],
values: {}
},
{
kind: "BlockStatement",
type: "BlockStatement",
fields: ["body", "block_scope", "startToken", "endToken"],
values: {}
},
{
kind: "TryBlock",
type: "BlockStatement",
fields: ["body", "block_scope", "startToken", "endToken"],
values: { role: "try" }
},
{
kind: "Finally",
type: "BlockStatement",
fields: ["body", "block_scope", "startToken", "endToken"],
values: { role: "finally" }
},
{
kind: "EmptyStatement",
type: "EmptyStatement",
fields: ["startToken", "endToken"],
values: {}
},
{
kind: "LabeledStatement",
type: "LabeledStatement",
fields: ["label", "body", "startToken", "endToken"],
values: {}
},
{
kind: "Do",
type: "DoWhileStatement",
fields: ["test", "block_scope", "body", "startToken", "endToken"],
values: {}
},
{
kind: "While",
type: "WhileStatement",
fields: ["test", "block_scope", "body", "startToken", "endToken"],
values: {}
},
{
kind: "For",
type: "ForStatement",
fields: [
"init",
"test",
"update",
"block_scope",
"body",
"startToken",
"endToken"
],
values: {}
},
{
kind: "ForIn",
type: "ForInStatement",
fields: ["left", "right", "block_scope", "body", "startToken", "endToken"],
values: {}
},
{
kind: "ForOf",
type: "ForOfStatement",
fields: [
"await",
"left",
"right",
"block_scope",
"body",
"startToken",
"endToken"
],
values: {}
},
{
kind: "With",
type: "WithStatement",
fields: ["object", "body", "startToken", "endToken"],
values: {}
},
{
kind: "Toplevel",
type: "Program",
fields: [
"globals",
"body",
"block_scope",
"startToken",
"endToken",
"tokenTables"
],
values: {}
},
{
kind: "Accessor",
type: "FunctionExpression",
fields: [
"id",
"params",
"generator",
"async",
"body",
"block_scope",
"startToken",
"endToken"
],
values: { role: "method" },
holdsBlock: true
},
{
kind: "Function",
type: "FunctionExpression",
fields: [
"id",
"params",
"generator",
"async",
"body",
"block_scope",
"startToken",
"endToken"
],
values: {},
holdsBlock: true
},
{
kind: "Arrow",
type: "ArrowFunctionExpression",
fields: [
"id",
"params",
"generator",
"async",
"body",
"block_scope",
"startToken",
"endToken"
],
values: {},
holdsBlock: true
},
{
kind: "Defun",
type: "FunctionDeclaration",
fields: [
"id",
"params",
"generator",
"async",
"body",
"block_scope",
"startToken",
"endToken"
],
values: {},
holdsBlock: true
},
{
kind: "PrefixedTemplateString",
type: "TaggedTemplateExpression",
fields: ["quasi", "tag", "_annotations", "startToken", "endToken"],
values: {},
initializes: true
},
{
kind: "TemplateString",
type: "TemplateLiteral",
fields: ["segments", "startToken", "endToken"],
values: {}
},
{
kind: "TemplateSegment",
type: "TemplateElement",
fields: ["value", "raw", "startToken", "endToken"],
values: {}
},
{
kind: "Return",
type: "ReturnStatement",
fields: ["argument", "startToken", "endToken"],
values: {}
},
{
kind: "Throw",
type: "ThrowStatement",
fields: ["argument", "startToken", "endToken"],
values: {}
},
{
kind: "Break",
type: "BreakStatement",
fields: ["label", "startToken", "endToken"],
values: {}
},
{
kind: "Continue",
type: "ContinueStatement",
fields: ["label", "startToken", "endToken"],
values: {}
},
{
kind: "Await",
type: "AwaitExpression",
fields: ["argument", "startToken", "endToken"],
values: {}
},
{
kind: "Yield",
type: "YieldExpression",
fields: ["argument", "delegate", "startToken", "endToken"],
values: {}
},
{
kind: "If",
type: "IfStatement",
fields: ["test", "alternate", "consequent", "startToken", "endToken"],
values: {}
},
{
kind: "Switch",
type: "SwitchStatement",
fields: ["discriminant", "cases", "block_scope", "startToken", "endToken"],
values: {}
},
{
kind: "Default",
type: "SwitchCase",
fields: ["consequent", "block_scope", "startToken", "endToken"],
values: {}
},
{
kind: "Case",
type: "SwitchCase",
fields: ["test", "consequent", "block_scope", "startToken", "endToken"],
values: {}
},
{
kind: "Try",
type: "TryStatement",
fields: ["block", "handler", "finalizer", "startToken", "endToken"],
values: {}
},
{
kind: "Catch",
type: "CatchClause",
fields: ["param", "body", "block_scope", "startToken", "endToken"],
values: {},
holdsBlock: true
},
{
kind: "Var",
type: "VariableDeclaration",
fields: ["declarations", "startToken", "endToken"],
values: { kind: "var" }
},
{
kind: "Let",
type: "VariableDeclaration",
fields: ["declarations", "startToken", "endToken"],
values: { kind: "let" }
},
{
kind: "Const",
type: "VariableDeclaration",
fields: ["declarations", "startToken", "endToken"],
values: { kind: "const" }
},
{
kind: "Using",
type: "VariableDeclaration",
fields: ["await", "declarations", "startToken", "endToken"],
values: { kind: "using" }
},
{
kind: "VarDef",
type: "VariableDeclarator",
fields: ["id", "init", "startToken", "endToken"],
values: {}
},
{
kind: "UsingDef",
type: "VariableDeclarator",
fields: ["id", "init", "startToken", "endToken"],
values: { role: "using" }
},
{
kind: "NameMapping",
type: "ExportSpecifier",
fields: ["imported", "exported", "local", "startToken", "endToken"],
values: {}
},
{
kind: "ExportSpecifier",
type: "ExportSpecifier",
fields: ["imported", "exported", "local", "startToken", "endToken"],
values: {}
},
{
kind: "ImportSpecifier",
type: "ImportSpecifier",
fields: ["imported", "exported", "local", "startToken", "endToken"],
values: {}
},
{
kind: "ImportNamespaceSpecifier",
type: "ImportNamespaceSpecifier",
fields: ["imported", "exported", "local", "startToken", "endToken"],
values: {}
},
{
kind: "Import",
type: "ImportDeclaration",
fields: [
"phase",
"imported_name",
"imported_names",
"source",
"attributes",
"startToken",
"endToken"
],
values: {}
},
{
kind: "ImportMeta",
type: "MetaProperty",
fields: ["startToken", "endToken"],
values: { role: "importMeta" }
},
{
kind: "NewTarget",
type: "MetaProperty",
fields: ["startToken", "endToken"],
values: { role: "newTarget" }
},
{
kind: "DynamicImport",
type: "ImportExpression",
fields: ["phase", "args", "startToken", "endToken"],
values: {}
},
{
kind: "CallExpression",
type: "CallExpression",
fields: [
"callee",
"arguments",
"optional",
"_annotations",
"startToken",
"endToken"
],
values: {},
initializes: true
},
{
kind: "New",
type: "NewExpression",
fields: [
"callee",
"arguments",
"optional",
"_annotations",
"startToken",
"endToken"
],
values: {},
initializes: true
},
{
kind: "Sequence",
type: "SequenceExpression",
fields: ["expressions", "startToken", "endToken"],
values: {}
},
{
kind: "Dot",
type: "MemberExpression",
fields: [
"quote",
"object",
"property",
"optional",
"_annotations",
"startToken",
"endToken"
],
values: { computed: false }
},
{
kind: "DotHash",
type: "MemberExpression",
fields: ["object", "property", "optional", "startToken", "endToken"],
values: { computed: false }
},
{
kind: "Sub",
type: "MemberExpression",
fields: [
"object",
"property",
"optional",
"_annotations",
"startToken",
"endToken"
],
values: { computed: true }
},
{
kind: "Chain",
type: "ChainExpression",
fields: ["expression", "startToken", "endToken"],
values: {}
},
{
kind: "UnaryPrefix",
type: "UnaryExpression",
fields: ["operator", "argument", "startToken", "endToken"],
values: { prefix: true }
},
{
kind: "UnaryPostfix",
type: "UpdateExpression",
fields: ["operator", "argument", "startToken", "endToken"],
values: { prefix: false }
},
{
kind: "UpdatePrefix",
type: "UpdateExpression",
fields: ["operator", "argument", "startToken", "endToken"],
values: { prefix: true }
},
{
kind: "Binary",
type: "BinaryExpression",
fields: ["operator", "left", "right", "startToken", "endToken"],
values: {}
},
{
kind: "PrivateIn",
type: "BinaryExpression",
fields: ["left", "right", "startToken", "endToken"],
values: { operator: "in", role: "privateIn" }
},
{
kind: "Logical",
type: "LogicalExpression",
fields: ["operator", "left", "right", "startToken", "endToken"],
values: {}
},
{
kind: "Conditional",
type: "ConditionalExpression",
fields: ["test", "consequent", "alternate", "startToken", "endToken"],
values: {}
},
{
kind: "Assign",
type: "AssignmentExpression",
fields: ["logical", "operator", "left", "right", "startToken", "endToken"],
values: {}
},
{
kind: "DefaultAssign",
type: "AssignmentPattern",
fields: ["operator", "left", "right", "startToken", "endToken"],
values: {}
},
{
kind: "Array",
type: "ArrayExpression",
fields: ["elements", "startToken", "endToken"],
values: {}
},
{
kind: "Object",
type: "ObjectExpression",
fields: ["properties", "startToken", "endToken"],
values: {}
},
{
kind: "ObjectKeyVal",
type: "Property",
fields: [
"quote",
"key",
"value",
"startToken",
"endToken",
"_annotations",
"computed",
"kind",
"method",
"shorthand"
],
values: {}
},
{
kind: "ObjectGetter",
type: "Property",
fields: [
"quote",
"static",
"key",
"value",
"startToken",
"endToken",
"_annotations",
"computed",
"method",
"shorthand"
],
values: { kind: "get" }
},
{
kind: "ObjectSetter",
type: "Property",
fields: [
"quote",
"static",
"key",
"value",
"startToken",
"endToken",
"_annotations",
"computed",
"method",
"shorthand"
],
values: { kind: "set" }
},
{
kind: "ConciseMethod",
type: "Property",
fields: [
"quote",
"static",
"key",
"value",
"startToken",
"endToken",
"_annotations",
"computed",
"kind",
"shorthand"
],
values: { method: true }
},
{
kind: "PrivateGetter",
type: "MethodDefinition",
fields: ["static", "key", "value", "startToken", "endToken", "computed"],
values: { kind: "get" }
},
{
kind: "PrivateSetter",
type: "MethodDefinition",
fields: ["static", "key", "value", "startToken", "endToken", "computed"],
values: { kind: "set" }
},
{
kind: "PrivateMethod",
type: "MethodDefinition",
fields: [
"static",
"key",
"value",
"startToken",
"endToken",
"computed",
"kind"
],
values: {}
},
{
kind: "ClassGetter",
type: "MethodDefinition",
fields: [
"quote",
"static",
"key",
"value",
"startToken",
"endToken",
"_annotations",
"computed",
"method",
"shorthand"
],
values: { kind: "get" }
},
{
kind: "ClassSetter",
type: "MethodDefinition",
fields: [
"quote",
"static",
"key",
"value",
"startToken",
"endToken",
"_annotations",
"computed",
"method",
"shorthand"
],
values: { kind: "set" }
},
{
kind: "ClassMethod",
type: "MethodDefinition",
fields: [
"quote",
"static",
"key",
"value",
"startToken",
"endToken",
"_annotations",
"computed",
"kind",
"method",
"shorthand"
],
values: {}
},
{
kind: "ClassProperty",
type: "PropertyDefinition",
fields: [
"static",
"quote",
"key",
"value",
"startToken",
"endToken",
"_annotations",
"computed"
],
values: {}
},
{
kind: "ClassPrivateProperty",
type: "PropertyDefinition",
fields: ["static", "key", "value", "startToken", "endToken", "computed"],
values: {}
},
{
kind: "DefClass",
type: "ClassDeclaration",
fields: [
"id",
"superClass",
"body",
"block_scope",
"startToken",
"endToken"
],
values: {},
holdsBlock: true
},
{
kind: "ClassExpression",
type: "ClassExpression",
fields: [
"id",
"superClass",
"body",
"block_scope",
"startToken",
"endToken"
],
values: {},
holdsBlock: true
},
{
kind: "ClassStaticBlock",
type: "StaticBlock",
fields: ["body", "block_scope", "startToken", "endToken"],
values: {},
unguarded: true,
noFlags: true
},
{
kind: "ClassBody",
type: "ClassBody",
fields: ["body", "startToken", "endToken"],
values: {}
},
{
kind: "SymbolVar",
type: "Identifier",
fields: ["init", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "var" }
},
{
kind: "SymbolFunarg",
type: "Identifier",
fields: ["init", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "funarg" }
},
{
kind: "SymbolConst",
type: "Identifier",
fields: ["init", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "const" }
},
{
kind: "SymbolUsing",
type: "Identifier",
fields: ["init", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "using" }
},
{
kind: "SymbolLet",
type: "Identifier",
fields: ["init", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "let" }
},
{
kind: "SymbolDefun",
type: "Identifier",
fields: ["init", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "defun" }
},
{
kind: "SymbolMethod",
type: "Identifier",
fields: ["scope", "name", "definition", "startToken", "endToken"],
values: { role: "method" }
},
{
kind: "SymbolClassProperty",
type: "Identifier",
fields: ["scope", "name", "definition", "startToken", "endToken"],
values: { role: "classProperty" }
},
{
kind: "SymbolLambda",
type: "Identifier",
fields: ["init", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "lambda" }
},
{
kind: "SymbolDefClass",
type: "Identifier",
fields: ["init", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "defClass" }
},
{
kind: "SymbolClass",
type: "Identifier",
fields: ["init", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "class" }
},
{
kind: "SymbolCatch",
type: "Identifier",
fields: ["init", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "catch" }
},
{
kind: "SymbolImport",
type: "Identifier",
fields: ["init", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "import" }
},
{
kind: "SymbolImportForeign",
type: "Identifier",
fields: ["quote", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "importForeign" }
},
{
kind: "Label",
type: "Identifier",
fields: [
"references",
"scope",
"name",
"definition",
"startToken",
"endToken"
],
values: { role: "label" },
initializes: true
},
{
kind: "SymbolRef",
type: "Identifier",
fields: ["scope", "name", "definition", "startToken", "endToken"],
values: { role: "reference" }
},
{
kind: "SymbolExport",
type: "Identifier",
fields: ["quote", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "export" }
},
{
kind: "SymbolExportForeign",
type: "Identifier",
fields: ["quote", "scope", "name", "definition", "startToken", "endToken"],
values: { role: "exportForeign" }
},
{
kind: "LabelRef",
type: "Identifier",
fields: ["scope", "name", "definition", "startToken", "endToken"],
values: { role: "labelReference" }
},
{
kind: "SymbolProperty",
type: "Identifier",
fields: ["name", "startToken", "endToken"],
values: { role: "property" }
},
{
kind: "SymbolPrivateProperty",
type: "PrivateIdentifier",
fields: ["scope", "name", "definition", "startToken", "endToken"],
values: { role: "privateProperty" }
},
{
kind: "NaN",
type: "Identifier",
fields: ["startToken", "endToken"],
values: { value: Number.NaN, name: "NaN", atom: true, definition: null }
},
{
kind: "Undefined",
type: "Identifier",
fields: ["startToken", "endToken"],
values: {
value: undefined,
name: "undefined",
atom: true,
definition: null
}
},
{
kind: "Infinity",
type: "Identifier",
fields: ["startToken", "endToken"],
values: { value: Infinity, name: "Infinity", atom: true, definition: null }
},
{
kind: "This",
type: "ThisExpression",
fields: ["scope", "name", "definition", "startToken", "endToken"],
values: {}
},
{
kind: "Super",
type: "Super",
fields: ["scope", "name", "definition", "startToken", "endToken"],
values: {}
},
{
kind: "String",
type: "Literal",
fields: ["value", "quote", "startToken", "endToken", "_annotations"],
values: {}
},
{
kind: "Number",
type: "Literal",
fields: ["value", "raw", "startToken", "endToken"],
values: {}
},
{
kind: "BigInt",
type: "Literal",
fields: ["value", "raw", "startToken", "endToken"],
values: { role: "bigint" }
},
{
kind: "RegExp",
type: "Literal",
fields: ["value", "startToken", "endToken"],
values: { role: "regexp" }
},
{
kind: "Null",
type: "Literal",
fields: ["startToken", "endToken"],
values: { value: null }
},
{
kind: "True",
type: "Literal",
fields: ["startToken", "endToken"],
values: { value: true }
},
{
kind: "False",
type: "Literal",
fields: ["startToken", "endToken"],
values: { value: false }
},
{
kind: "SymbolImportForeignLiteral",
type: "Literal",
fields: [
"quote",
"scope",
"name",
"definition",
"startToken",
"endToken",
"value"
],
values: { role: "importForeign" }
},
{
kind: "SymbolExportForeignLiteral",
type: "Literal",
fields: [
"quote",
"scope",
"name",
"definition",
"startToken",
"endToken",
"value"
],
values: { role: "exportForeign" }
},
{
kind: "SymbolExportLiteral",
type: "Literal",
fields: [
"quote",
"scope",
"name",
"definition",
"startToken",
"endToken",
"value"
],
values: { role: "export" }
},
{
kind: "ArrayPattern",
type: "ArrayPattern",
fields: ["elements", "properties", "is_array", "startToken", "endToken"],
values: {}
},
{
kind: "ObjectPattern",
type: "ObjectPattern",
fields: ["elements", "properties", "is_array", "startToken", "endToken"],
values: {}
},
{
kind: "ExportAllDeclaration",
type: "ExportAllDeclaration",
fields: [
"exported_definition",
"exported_value",
"is_default",
"exported_names",
"source",
"attributes",
"startToken",
"endToken"
],
values: {}
},
{
kind: "ExportDefaultDeclaration",
type: "ExportDefaultDeclaration",
fields: [
"exported_definition",
"exported_value",
"is_default",
"exported_names",
"source",
"attributes",
"startToken",
"endToken"
],
values: {}
},
{
kind: "ExportNamedDeclaration",
type: "ExportNamedDeclaration",
fields: [
"exported_definition",
"exported_value",
"is_default",
"exported_names",
"source",
"attributes",
"startToken",
"endToken"
],
values: {}
},
{
kind: "RestElement",
type: "RestElement",
fields: ["argument", "startToken", "endToken"],
values: {}
},
{
kind: "SpreadElement",
type: "SpreadElement",
fields: ["argument", "startToken", "endToken"],
values: {}
}
];
// The fields of the nodes of an ESTree type, in the one order its kinds take
// them in, with those an analysis writes; a block's scope has no type.
/** @type {Record<string, string[]>} */
const NODE_TYPE_FIELDS = {
DebuggerStatement: ["startToken", "endToken", "flags"],
ExpressionStatement: [
"directive",
"expression",
"startToken",
"endToken",
"flags"
],
BlockStatement: [
"body",
"block_scope",
"startToken",
"endToken",
"role",
"flags"
],
EmptyStatement: ["startToken", "endToken", "flags"],
LabeledStatement: ["label", "body", "startToken", "endToken", "flags"],
DoWhileStatement: [
"test",
"block_scope",
"body",
"startToken",
"endToken",
"flags"
],
WhileStatement: [
"test",
"block_scope",
"body",
"startToken",
"endToken",
"flags"
],
ForStatement: [
"init",
"test",
"update",
"block_scope",
"body",
"startToken",
"endToken",
"flags"
],
ForInStatement: [
"left",
"right",
"block_scope",
"body",
"startToken",
"endToken",
"flags"
],
ForOfStatement: [
"await",
"left",
"right",
"block_scope",
"body",
"startToken",
"endToken",
"flags"
],
WithStatement: ["object", "body", "startToken", "endToken", "flags"],
Program: [
"globals",
"body",
"block_scope",
"startToken",
"endToken",
"tokenTables",
"flags"
],
FunctionExpression: [
"role",
"id",
"params",
"generator",
"async",
"body",
"block_scope",
"startToken",
"endToken",
"flags"
],
ArrowFunctionExpression: [
"id",
"params",
"generator",
"async",
"body",
"block_scope",
"startToken",
"endToken",
"flags"
],
FunctionDeclaration: [
"id",
"params",
"generator",
"async",
"body",
"block_scope",
"startToken",
"endToken",
"flags"
],
TaggedTemplateExpression: [
"quasi",
"tag",
"_annotations",
"startToken",
"endToken",
"flags"
],
TemplateLiteral: ["segments", "startToken", "endToken", "flags"],
TemplateElement: ["value", "raw", "startToken", "endToken", "flags"],
ReturnStatement: ["argument", "startToken", "endToken", "flags"],
ThrowStatement: ["argument", "startToken", "endToken", "flags"],
BreakStatement: ["label", "startToken", "endToken", "flags"],
ContinueStatement: ["label", "startToken", "endToken", "flags"],
AwaitExpression: ["argument", "startToken", "endToken", "flags"],
YieldExpression: ["argument", "delegate", "startToken", "endToken", "flags"],
IfStatement: [
"test",
"alternate",
"consequent",
"startToken",
"endToken",
"flags"
],
SwitchStatement: [
"discriminant",
"cases",
"block_scope",
"startToken",
"endToken",
"flags"
],
SwitchCase: [
"consequent",
"block_scope",
"startToken",
"endToken",
"test",
"flags"
],
TryStatement: [
"block",
"handler",
"finalizer",
"startToken",
"endToken",
"flags"
],
CatchClause: [
"param",
"body",
"block_scope",
"startToken",
"endToken",
"flags"
],
VariableDeclaration: [
"kind",
"declarations",
"startToken",
"endToken",
"await",
"flags"
],
VariableDeclarator: ["id", "init", "startToken", "endToken", "role", "flags"],
ExportSpecifier: [
"imported",
"exported",
"local",
"startToken",
"endToken",
"flags"
],
ImportSpecifier: [
"imported",
"exported",
"local",
"startToken",
"endToken",
"flags"
],
ImportNamespaceSpecifier: [
"imported",
"exported",
"local",
"startToken",
"endToken",
"flags"
],
ImportDeclaration: [
"phase",
"imported_name",
"imported_names",
"source",
"attributes",
"startToken",
"endToken",
"flags"
],
MetaProperty: ["role", "startToken", "endToken", "flags"],
ImportExpression: ["phase", "args", "startToken", "endToken", "flags"],
CallExpression: [
"callee",
"arguments",
"optional",
"_annotations",
"startToken",
"endToken",
"flags"
],
NewExpression: [
"callee",
"arguments",
"optional",
"_annotations",
"startToken",
"endToken",
"flags"
],
SequenceExpression: ["expressions", "startToken", "endToken", "flags"],
MemberExpression: [
"computed",
"quote",
"object",
"property",
"optional",
"_annotations",
"startToken",
"endToken",
"flags"
],
ChainExpression: ["expression", "startToken", "endToken", "flags"],
UnaryExpression: [
"prefix",
"operator",
"argument",
"startToken",
"endToken",
"flags"
],
UpdateExpression: [
"prefix",
"operator",
"argument",
"startToken",
"endToken",
"flags"
],
BinaryExpression: [
"operator",
"left",
"right",
"startToken",
"endToken",
"role",
"flags"
],
LogicalExpression: [
"operator",
"left",
"right",
"startToken",
"endToken",
"flags"
],
ConditionalExpression: [
"test",
"consequent",
"alternate",
"startToken",
"endToken",
"flags"
],
AssignmentExpression: [
"logical",
"operator",
"left",
"right",
"startToken",
"endToken",
"flags"
],
AssignmentPattern: [
"operator",
"left",
"right",
"startToken",
"endToken",
"flags"
],
ArrayExpression: ["elements", "startToken", "endToken", "flags"],
ObjectExpression: ["properties", "startToken", "endToken", "flags"],
Property: [
"quote",
"key",
"value",
"startToken",
"endToken",
"_annotations",
"computed",
"kind",
"method",
"shorthand",
"mapName",
"static",
"flags"
],
MethodDefinition: [
"kind",
"static",
"key",
"value",
"startToken",
"endToken",
"computed",
"quote",
"_annotations",
"method",
"shorthand",
"flags"
],
PropertyDefinition: [
"static",
"quote",
"mapName",
"key",
"value",
"startToken",
"endToken",
"_annotations",
"computed",
"flags"
],
ClassDeclaration: [
"id",
"superClass",
"body",
"block_scope",
"startToken",
"endToken",
"flags"
],
ClassExpression: [
"id",
"superClass",
"body",
"block_scope",
"startToken",
"endToken",
"flags"
],
StaticBlock: ["body", "block_scope", "startToken", "endToken", "flags"],
ClassBody: ["body", "startToken", "endToken", "flags"],
Identifier: [
"role",
"init",
"scope",
"name",
"definition",
"startToken",
"endToken",
"quote",
"references",
"value",
"atom",
"flags"
],
ThisExpression: [
"scope",
"name",
"definition",
"startToken",
"endToken",
"flags"
],
Super: ["scope", "name", "definition", "startToken", "endToken", "flags"],
Literal: [
"value",
"quote",
"startToken",
"endToken",
"_annotations",
"raw",
"role",
"scope",
"name",
"definition",
"flags"
],
ArrayPattern: [
"elements",
"properties",
"is_array",
"startToken",
"endToken",
"flags"
],
ObjectPattern: [
"elements",
"properties",
"is_array",
"startToken",
"endToken",
"flags"
],
ExportAllDeclaration: [
"exported_definition",
"exported_value",
"is_default",
"exported_names",
"source",
"attributes",
"startToken",
"endToken",
"flags"
],
ExportDefaultDeclaration: [
"exported_definition",
"exported_value",
"is_default",
"exported_names",
"source",
"attributes",
"startToken",
"endToken",
"flags"
],
ExportNamedDeclaration: [
"exported_definition",
"exported_value",
"is_default",
"exported_names",
"source",
"attributes",
"startToken",
"endToken",
"flags"
],
RestElement: ["argument", "startToken", "endToken", "flags"],
SpreadElement: ["argument", "startToken", "endToken", "flags"],
PrivateIdentifier: [
"role",
"init",
"scope",
"name",
"definition",
"startToken",
"endToken",
"quote",
"references",
"value",
"atom",
"flags"
]
};
// Each kind and the kinds it is one of, terser's class and its bases.
/** @type {Record<string, string[]>} */
const NODE_KIND_ANCESTRY = {
Debugger: ["Debugger", "Statement", "Node"],
Directive: ["Directive", "Statement", "Node"],
SimpleStatement: ["SimpleStatement", "Statement", "Node"],
BlockStatement: ["BlockStatement", "Block", "Statement", "Node"],
TryBlock: ["TryBlock", "Block", "Statement", "Node"],
Finally: ["Finally", "Block", "Statement", "Node"],
EmptyStatement: ["EmptyStatement", "Statement", "Node"],
LabeledStatement: [
"LabeledStatement",
"StatementWithBody",
"Statement",
"Node"
],
Do: [
"Do",
"DWLoop",
"IterationStatement",
"StatementWithBody",
"Statement",
"Node"
],
While: [
"While",
"DWLoop",
"IterationStatement",
"StatementWithBody",
"Statement",
"Node"
],
For: ["For", "IterationStatement", "StatementWithBody", "Statement", "Node"],
ForIn: [
"ForIn",
"IterationStatement",
"StatementWithBody",
"Statement",
"Node"
],
ForOf: [
"ForOf",
"ForIn",
"IterationStatement",
"StatementWithBody",
"Statement",
"Node"
],
With: ["With", "StatementWithBody", "Statement", "Node"],
Toplevel: ["Toplevel", "Scope", "Block", "Statement", "Node"],
Accessor: ["Accessor", "Lambda", "Scope", "Block", "Statement", "Node"],
Function: ["Function", "Lambda", "Scope", "Block", "Statement", "Node"],
Arrow: ["Arrow", "Lambda", "Scope", "Block", "Statement", "Node"],
Defun: ["Defun", "Lambda", "Scope", "Block", "Statement", "Node"],
PrefixedTemplateString: ["PrefixedTemplateString", "Node"],
TemplateString: ["TemplateString", "Node"],
TemplateSegment: ["TemplateSegment", "Node"],
Return: ["Return", "Exit", "Jump", "Statement", "Node"],
Throw: ["Throw", "Exit", "Jump", "Statement", "Node"],
Break: ["Break", "LoopControl", "Jump", "Statement", "Node"],
Continue: ["Continue", "LoopControl", "Jump", "Statement", "Node"],
Await: ["Await", "Node"],
Yield: ["Yield", "Node"],
If: ["If", "StatementWithBody", "Statement", "Node"],
Switch: ["Switch", "Block", "Statement", "Node"],
Default: ["Default", "SwitchBranch", "Block", "Statement", "Node"],
Case: ["Case", "SwitchBranch", "Block", "Statement", "Node"],
Try: ["Try", "Statement", "Node"],
Catch: ["Catch", "Block", "Statement", "Node"],
Var: ["Var", "Definitions", "DefinitionsLike", "Statement", "Node"],
Let: ["Let", "Definitions", "DefinitionsLike", "Statement", "Node"],
Const: ["Const", "Definitions", "DefinitionsLike", "Statement", "Node"],
Using: ["Using", "DefinitionsLike", "Statement", "Node"],
VarDef: ["VarDef", "VarDefLike", "Node"],
UsingDef: ["UsingDef", "VarDefLike", "Node"],
NameMapping: ["NameMapping", "Node"],
ExportSpecifier: ["ExportSpecifier", "NameMapping", "Node"],
ImportSpecifier: ["ImportSpecifier", "NameMapping", "Node"],
ImportNamespaceSpecifier: ["ImportNamespaceSpecifier", "NameMapping", "Node"],
Import: ["Import", "Node"],
ImportMeta: ["ImportMeta", "Node"],
NewTarget: ["NewTarget", "Node"],
DynamicImport: ["DynamicImport", "Node"],
CallExpression: ["CallExpression", "Call", "Node"],
New: ["New", "Call", "Node"],
Sequence: ["Sequence", "Node"],
Dot: ["Dot", "PropAccess", "Node"],
DotHash: ["DotHash", "PropAccess", "Node"],
Sub: ["Sub", "PropAccess", "Node"],
Chain: ["Chain", "Node"],
UnaryPrefix: ["UnaryPrefix", "Unary", "Node"],
UnaryPostfix: ["UnaryPostfix", "Unary", "Node"],
UpdatePrefix: ["UpdatePrefix", "UnaryPrefix", "Unary", "Node"],
Binary: ["Binary", "Node"],
PrivateIn: ["PrivateIn", "Node"],
Logical: ["Logical", "Binary", "Node"],
Conditional: ["Conditional", "Node"],
Assign: ["Assign", "Binary", "Node"],
DefaultAssign: ["DefaultAssign", "Binary", "Node"],
Array: ["Array", "Node"],
Object: ["Object", "Node"],
ObjectKeyVal: ["ObjectKeyVal", "ObjectProperty", "Node"],
ObjectGetter: ["ObjectGetter", "ObjectProperty", "Node"],
ObjectSetter: ["ObjectSetter", "ObjectProperty", "Node"],
ConciseMethod: ["ConciseMethod", "ObjectProperty", "Node"],
PrivateGetter: ["PrivateGetter", "ObjectProperty", "Node"],
PrivateSetter: ["PrivateSetter", "ObjectProperty", "Node"],
PrivateMethod: ["PrivateMethod", "ObjectProperty", "Node"],
ClassGetter: ["ClassGetter", "ObjectGetter", "ObjectProperty", "Node"],
ClassSetter: ["ClassSetter", "ObjectSetter", "ObjectProperty", "Node"],
ClassMethod: ["ClassMethod", "ConciseMethod", "ObjectProperty", "Node"],
ClassProperty: ["ClassProperty", "ObjectProperty", "Node"],
ClassPrivateProperty: ["ClassPrivateProperty", "ObjectProperty", "Node"],
DefClass: ["DefClass", "Class", "Scope", "Block", "Statement", "Node"],
ClassExpression: [
"ClassExpression",
"Class",
"Scope",
"Block",
"Statement",
"Node"
],
ClassStaticBlock: ["ClassStaticBlock", "Scope", "Block", "Statement", "Node"],
ClassBody: ["ClassBody", "Node"],
SymbolVar: ["SymbolVar", "SymbolDeclaration", "Symbol", "Node"],
SymbolFunarg: [
"SymbolFunarg",
"SymbolVar",
"SymbolDeclaration",
"Symbol",
"Node"
],
SymbolConst: [
"SymbolConst",
"SymbolBlockDeclaration",
"SymbolDeclaration",
"Symbol",
"Node"
],
SymbolUsing: [
"SymbolUsing",
"SymbolBlockDeclaration",
"SymbolDeclaration",
"Symbol",
"Node"
],
SymbolLet: [
"SymbolLet",
"SymbolBlockDeclaration",
"SymbolDeclaration",
"Symbol",
"Node"
],
SymbolDefun: ["SymbolDefun", "SymbolDeclaration", "Symbol", "Node"],
SymbolMethod: ["SymbolMethod", "Symbol", "Node"],
SymbolClassProperty: ["SymbolClassProperty", "Symbol", "Node"],
SymbolLambda: ["SymbolLambda", "SymbolDeclaration", "Symbol", "Node"],
SymbolDefClass: [
"SymbolDefClass",
"SymbolBlockDeclaration",
"SymbolDeclaration",
"Symbol",
"Node"
],
SymbolClass: ["SymbolClass", "SymbolDeclaration", "Symbol", "Node"],
SymbolCatch: [
"SymbolCatch",
"SymbolBlockDeclaration",
"SymbolDeclaration",
"Symbol",
"Node"
],
SymbolImport: [
"SymbolImport",
"SymbolBlockDeclaration",
"SymbolDeclaration",
"Symbol",
"Node"
],
SymbolImportForeign: ["SymbolImportForeign", "Symbol", "Node"],
Label: ["Label", "Symbol", "Node"],
SymbolRef: ["SymbolRef", "Symbol", "Node"],
SymbolExport: ["SymbolExport", "SymbolRef", "Symbol", "Node"],
SymbolExportForeign: ["SymbolExportForeign", "Symbol", "Node"],
LabelRef: ["LabelRef", "Symbol", "Node"],
SymbolProperty: ["SymbolProperty", "Symbol", "Node"],
SymbolPrivateProperty: ["SymbolPrivateProperty", "Symbol", "Node"],
NaN: ["NaN", "Atom", "Constant", "Node"],
Undefined: ["Undefined", "Atom", "Constant", "Node"],
Infinity: ["Infinity", "Atom", "Constant", "Node"],
This: ["This", "Symbol", "Node"],
Super: ["Super", "This", "Symbol", "Node"],
String: ["String", "Constant", "Node"],
Number: ["Number", "Constant", "Node"],
BigInt: ["BigInt", "Constant", "Node"],
RegExp: ["RegExp", "Constant", "Node"],
Null: ["Null", "Atom", "Constant", "Node"],
True: ["True", "Boolean", "Atom", "Constant", "Node"],
False: ["False", "Boolean", "Atom", "Constant", "Node"],
SymbolImportForeignLiteral: [
"SymbolImportForeignLiteral",
"SymbolImportForeign",
"Symbol",
"Node"
],
SymbolExportForeignLiteral: [
"SymbolExportForeignLiteral",
"SymbolExportForeign",
"Symbol",
"Node"
],
SymbolExportLiteral: [
"SymbolExportLiteral",
"SymbolExport",
"SymbolRef",
"Symbol",
"Node"
],
ArrayPattern: ["ArrayPattern", "Destructuring", "Node"],
ObjectPattern: ["ObjectPattern", "Destructuring", "Node"],
ExportAllDeclaration: ["ExportAllDeclaration", "Export", "Statement", "Node"],
ExportDefaultDeclaration: [
"ExportDefaultDeclaration",
"Export",
"Statement",
"Node"
],
ExportNamedDeclaration: [
"ExportNamedDeclaration",
"Export",
"Statement",
"Node"
],
RestElement: ["RestElement", "Expansion", "Node"],
SpreadElement: ["SpreadElement", "Expansion", "Node"]
};
// The prototype every node inherits, no method on it: what tells a node from
// any other object, such as one an object literal evaluates to.
const NODE_PROTOTYPE = Object.freeze({});
// The kind a node was made as, and its index, read off its kind's prototype.
const NODE_KIND_NAME = Symbol("kind");
const NODE_KIND_INDEX = Symbol("kind index");
// The fields only the compressor reads that a kind's nodes hold.
const NODE_COMPRESSOR_FIELDS = Symbol("compressor fields");
// Each kind's index, in `NODE_KIND_ANCESTRY`'s order.
/** @type {Map<string, number>} */
const NODE_KIND_INDEXES = new Map(
Object.keys(NODE_KIND_ANCESTRY).map((kind, index) => [kind, index])
);
// The class each of terser's classes extends, read off the ancestries.
/** @type {Map<string, string>} */
const NODE_CLASS_PARENTS = new Map();
for (const ancestry of Object.values(NODE_KIND_ANCESTRY)) {
for (let i = 0; i < ancestry.length - 1; i++) {
NODE_CLASS_PARENTS.set(ancestry[i], ancestry[i + 1]);
}
}
// Each class's prototype: frozen, without methods, so a node is of a class
// where the prototype is in its chain, as `is<Class>Node` asks.
/** @type {Map<string, EXPECTED_OBJECT>} */
const NODE_CLASS_PROTOTYPES = new Map([["Node", NODE_PROTOTYPE]]);
/**
* @param {string} name one of terser's classes
* @returns {EXPECTED_OBJECT} its prototype
*/
const classPrototype = (name) => {
let prototype = NODE_CLASS_PROTOTYPES.get(name);
if (prototype === undefined) {
prototype = Object.freeze({
__proto__: classPrototype(
/** @type {string} */ (NODE_CLASS_PARENTS.get(name))
)
});
NODE_CLASS_PROTOTYPES.set(name, prototype);
}
return prototype;
};
// Each kind's own prototype, below its class's: its type, the values the kind
// fixes, and the kind; a value an optimizer assigns becomes the node's own.
/** @type {Map<string, { type: string }>} */
const NODE_KIND_PROTOTYPES = new Map();
/**
* A kind's prototype as it is made: its type, the values the kind fixes, its
* kind and index, and the fields only the compressor reads.
* @internal
* @typedef {{ type: string, [NODE_KIND_NAME]: string, [NODE_KIND_INDEX]: number | undefined, [NODE_COMPRESSOR_FIELDS]?: string[] } & NodeProps} KindPrototype
*/
/**
* @param {string} kind a kind of node
* @param {string} type the ESTree type its nodes have
* @param {NodeProps} constants the values the kind fixes
* @returns {KindPrototype} the prototype of its nodes
*/
const kindPrototype = (kind, type, constants) => {
const key = `${kind}|${type}`;
let prototype = /** @type {KindPrototype | undefined} */ (
NODE_KIND_PROTOTYPES.get(key)
);
if (prototype === undefined) {
prototype = /** @type {KindPrototype} */ ({
__proto__: classPrototype(kind),
type,
...constants,
[NODE_KIND_NAME]: kind,
[NODE_KIND_INDEX]: NODE_KIND_INDEXES.get(kind)
});
NODE_KIND_PROTOTYPES.set(key, prototype);
}
return prototype;
};
/**
* Whether a value is a node, one of the objects the factories make.
* @type {(value: unknown) => value is Node}
*/
const isNode = /** @type {(value: unknown) => value is Node} */ (
Object.prototype.isPrototypeOf.bind(NODE_PROTOTYPE)
);
/**
* Gives each kind's prototype, under a key of its own, the value of the
* nearest of its classes a table has, as a method of terser's classes was.
* @template T
* @param {Record<string, T>} table values by terser's class
* @returns {symbol} the key the values are read under
*/
const kindValues = (table) => {
const key = Symbol("kind value");
for (const [prototypeKey, prototype] of NODE_KIND_PROTOTYPES) {
const kind = prototypeKey.slice(0, prototypeKey.indexOf("|"));
const name = NODE_KIND_ANCESTRY[kind].find((name) => hasOwn(table, name));
/** @type {Record<symbol, T | undefined>} */ (prototype)[key] =
name === undefined ? undefined : table[name];
}
return key;
};
/**
* Installs the node factories, one per kind, `is<Kind>Node` predicates
* answering on a node's type and the values telling kinds apart, and `kindOf`.
* @param {TerserAst} ast the ast to install them into
* @returns {void}
*/
const installNodeKinds = (ast) => {
for (const entry of NODE_KINDS) {
ast[classNameOf(entry.kind)] = compileNodeFactory(ast, entry);
}
ast.isSyntaxNode = isNode;
// WHY: a bound `isPrototypeOf` is a builtin call where its caller is not yet
// optimized, and TurboFan lowers it to the prototype walk `instanceof` makes;
// a closure testing a node's data costs a frame per call.
for (const name of NODE_CLASS_PARENTS.keys()) {
ast[`is${classNameOf(name)}`] = /** @type {NodeTest} */ (
Object.prototype.isPrototypeOf.bind(classPrototype(name))
);
}
/**
* The kind of a node, terser's class, as it was made.
* @param {Node} n a node
* @returns {string | undefined} its kind
*/
ast.kindOf = (n) =>
/** @type {Node & { [NODE_KIND_NAME]: string | undefined }} */ (n)[
NODE_KIND_NAME
];
// Each kind's factory, by the kind's index.
/** @type {NodeFactory[]} */
const factories = Object.keys(NODE_KIND_ANCESTRY).map(
(kind) => ast[classNameOf(kind)]
);
/**
* The factory making nodes of a node's kind, which copies it.
* @param {Node} node a node
* @returns {NodeFactory} the factory
*/
ast.factoryOf = (node) => factories[node[NODE_KIND_INDEX]];
};
/** @typedef {((props?: NodeProps) => Node) & { LEAF?: ((props: NodeProps) => NodeFactory) | null }} NodeFactory a node kind's factory, and the kind its props choose where ESTree types its nodes apart */
// What a factory reads a field from where it is handed no properties.
const NO_PROPS = Object.freeze({});
/**
* A name a generated factory's source holds, which is only ever a plain
* identifier: anything else throws rather than reach `new Function`.
* @param {string} name a field or factory name
* @returns {string} the name
*/
const plainIdentifier = (name) => {
if (!/^[A-Za-z_$][\w$]*$/.test(name)) {
throw new Error(`Not a plain identifier: ${name}`);
}
return name;
};
/**
* Compiles the factory of one kind: a plain object holding every field of the
* kind's ESTree type, in that type's order, taken from the properties given
* where the kind has the field, and the kind's own values.
* @param {TerserAst} ast the ast, whose factories copy a function's or a class's block
* @param {NodeKind} entry the kind
* @returns {NodeFactory} the factory
*/
const compileNodeFactory = (ast, entry) => {
const { kind, type, fields, values } = entry;
// Each kind has a prototype, so a shape of its own: a field only other kinds
// of its type take is left out; one no kind takes, written by an analysis,
// stays.
/** @type {Set<string>} */
const otherKinds = new Set();
for (const other of NODE_KINDS) {
if (other.type !== type || other.kind === kind) continue;
for (const field of other.fields) otherKinds.add(field);
for (const field of Object.keys(other.values)) otherKinds.add(field);
}
const shape = NODE_TYPE_FIELDS[type].filter(
(field) =>
field === "flags" ||
fields.includes(field) ||
hasOwn(values, field) ||
!otherKinds.has(field)
);
// The values the kind fixes are read off its prototype.
const constants = values;
const prototype = kindPrototype(kind, type, constants);
// A declarator's or loop's `init` is a child the printer reads; a name's is
// what the compressor alone reads.
prototype[NODE_COMPRESSOR_FIELDS] = COMPRESSOR_ONLY_FIELDS.filter(
(field) =>
shape.includes(field) && (field !== "init" || type === "Identifier")
);
// A method's name is a private name in a private member.
const privatePrototype =
kind === "SymbolMethod"
? kindPrototype(kind, "PrivateIdentifier", constants)
: prototype;
privatePrototype[NODE_COMPRESSOR_FIELDS] = prototype[NODE_COMPRESSOR_FIELDS];
const props = entry.unguarded ? "props" : "p";
const parts = [];
for (const field of shape) {
plainIdentifier(field);
let value;
if (Object.prototype.hasOwnProperty.call(constants, field)) continue;
if (Object.prototype.hasOwnProperty.call(values, field)) {
value = `values.${field}`;
} else if (field === "flags") {
value = entry.noFlags ? "undefined" : "0";
} else if (!fields.includes(field)) {
value = "undefined";
} else if (entry.holdsBlock && field === "body") {
// A copy of another node takes a block of its own: one shared would see
// the other's statements replaced, where terser's array was replaced.
value = `${props}.type === undefined || ${props}.body === undefined ? ${props}.body : copyBlock(${props}.body)`;
} else if (entry.initializes && field === "_annotations") {
value = `${props}._annotations === null || ${props}._annotations === undefined ? 0 : ${props}._annotations`;
} else {
value = `${props}.${field}`;
}
parts.push(`this.${field} = ${value};`);
}
// A function, class or program made of another is a scope of its own, as
// terser's node was, that holds the other's definitions.
const label =
kind === "Label"
? "node.references = []; node.definition = node;"
: NODE_KIND_ANCESTRY[kind].includes("Scope")
? "if (node.block_scope !== undefined) node.block_scope = node.block_scope.share(node);"
: "";
// A method's name is a private name in a private member.
const made =
kind === "SymbolMethod"
? `${props}.type === "PrivateIdentifier" ? new PrivateKind(${props}) : new Kind(${props})`
: `new Kind(${props})`;
// One constructor per kind, as terser's were: it allocates the fields in
// place, where an object literal naming a prototype has it set once made.
// eslint-disable-next-line no-new-func
return new Function(
"values",
"copyBlock",
"NO_PROPS",
"PROTOTYPE",
"PRIVATE_PROTOTYPE",
`"use strict"; function Kind(${props}) { ${parts.join(" ")} } Kind.prototype = PROTOTYPE; function PrivateKind(${props}) { ${parts.join(" ")} } PrivateKind.prototype = PRIVATE_PROTOTYPE; return function ${plainIdentifier(classNameOf(kind))}(props) { ${entry.unguarded ? "" : "const p = props || NO_PROPS;"} const node = ${made}; ${label} return node; };`
)(
values,
(/** @type {Node} */ block) => ast.factoryOf(block)(block),
NO_PROPS,
prototype,
privatePrototype
);
};
/**
* What terser's `ast.js` exported, without its classes: a factory per kind of
* node, the predicates on nodes, the token, the walkers and the walk helpers.
* @returns {Ast} the ast
*/
const createAst = () => {
const ast = /** @type {TerserAst} */ ({ ParsedToken });
installNodeKinds(ast);
/**
* Gives each kind's prototype the bits a list of kinds and the bit each sets
* on the kinds that are one of it, read off a node as `node[key]`.
* @param {[string, number][]} table each kind and its bit
* @returns {symbol} the key the bits are read under
*/
ast.kindBits = (table) => {
const key = Symbol("kind bits");
for (const [prototypeKey, prototype] of NODE_KIND_PROTOTYPES) {
const kind = prototypeKey.slice(0, prototypeKey.indexOf("|"));
let kindBits = 0;
for (const [name, bit] of table) {
if (NODE_KIND_ANCESTRY[kind].includes(name)) kindBits |= bit;
}
/** @type {Record<symbol, number>} */ (prototype)[key] = kindBits;
}
return key;
};
// The kinds no node is made of, whose factory makes the leaf the
// properties choose.
for (const kind of ["Call", "Destructuring", "Export"]) {
/** @type {NodeFactory} */
const factory = (props) => {
const leaf = /** @type {(props: NodeProps) => NodeFactory} */ (
factory.LEAF
);
return leaf(/** @type {NodeProps} */ (props))(props);
};
ast[classNameOf(kind)] = factory;
}
// The leaf class a node of a class ESTree types apart by a field takes, as
// `makeNode` builds it: a call is never one of `Call` itself.
for (const type of [
"SymbolImportForeign",
"SymbolExportForeign",
"SymbolExport"
]) {
const Unquoted = ast[classNameOf(type)];
const Quoted = ast[classNameOf(`${type}Literal`)];
Unquoted.LEAF = (/** @type {NodeProps} */ props) =>
props.quote ? Quoted : Unquoted;
}
ast.DestructuringNode.LEAF = (props) =>
props.is_array ? ast.ArrayPatternNode : ast.ObjectPatternNode;
ast.ExportNode.LEAF = (props) =>
props.is_default
? ast.ExportDefaultDeclarationNode
: props.exported_names &&
props.exported_names.length > 0 &&
isUnquotedStar(props.exported_names[0].local)
? ast.ExportAllDeclarationNode
: ast.ExportNamedDeclarationNode;
// A mapping `export *` makes, its local the bare `*`, has no ESTree node.
ast.NameMappingNode.LEAF = (props) =>
props.imported
? isUnquotedStar(props.imported)
? ast.ImportNamespaceSpecifierNode
: ast.ImportSpecifierNode
: isUnquotedStar(/** @type {Node} */ (props.local))
? ast.NameMappingNode
: ast.ExportSpecifierNode;
ast.BinaryNode.LEAF = (props) =>
LAZY_OPERATORS.has(/** @type {string} */ (props.operator))
? ast.LogicalNode
: ast.BinaryNode;
ast.UnaryPrefixNode.LEAF = (props) =>
props.operator === "++" || props.operator === "--"
? ast.UpdatePrefixNode
: ast.UnaryPrefixNode;
ast.CallNode.LEAF = () => ast.CallExpressionNode;
installTraversal(ast);
markNodeKinds({ ast });
const KIND_BITS = ast.nodeKindBits;
const { pushChildrenBackwards, walkNode } = ast;
const walkAbort = Symbol("abort walk");
/**
* terser's `walk`: depth first, children in order; `callback` returns true
* to skip a node's children, `walk_abort` to stop.
* @param {Node} node the root
* @param {(node: Node, toVisit: Node[]) => WalkResult} callback what each node is handed to
* @param {Node[]=} toVisit the nodes left to visit
* @returns {boolean} whether the walk was stopped
*/
const walk = (node, callback, toVisit = [node]) => {
const push = toVisit.push.bind(toVisit);
while (toVisit.length) {
const current = toVisit.pop();
const result = callback(/** @type {Node} */ (current), toVisit);
if (result) {
if (result === walkAbort) return true;
continue;
}
pushChildrenBackwards(/** @type {Node} */ (current), push);
}
return false;
};
/**
* terser's `walk_parent`: `walk`, handing `callback` the node's parents.
* @param {Node} node the root
* @param {(node: Node, info: ParentStack) => WalkResult} callback what each node is handed to
* @param {Node[]=} initialStack the parents of the root
* @returns {boolean} whether the walk was stopped
*/
const walkParent = (node, callback, initialStack) => {
const toVisit = [node];
const push = toVisit.push.bind(toVisit);
const stack = initialStack ? [...initialStack] : [];
/** @type {number[]} */
const parentPopIndices = [];
/** @type {Node} */
let current;
const info = {
/**
* @param {number=} n how many parents up, -1 for the node itself
* @returns {Node} that parent, undefined past the root
*/
parent: (n = 0) => {
if (n === -1) return current;
if (initialStack && n >= stack.length) {
n -= stack.length;
return initialStack[initialStack.length - (n + 1)];
}
return stack[stack.length - (1 + n)];
}
};
while (toVisit.length) {
current = /** @type {Node} */ (toVisit.pop());
while (
parentPopIndices.length &&
toVisit.length === parentPopIndices[parentPopIndices.length - 1]
) {
stack.pop();
parentPopIndices.pop();
}
const result = callback(current, info);
if (result) {
if (result === walkAbort) return true;
continue;
}
const visitLength = toVisit.length;
pushChildrenBackwards(current, push);
// The node is a parent only where it has children to visit.
if (toVisit.length > visitLength) {
stack.push(current);
parentPopIndices.push(visitLength - 1);
}
}
return false;
};
/**
* terser's `walk_body`, handed the list rather than the node holding it.
* @param {Node[]} body a statement list
* @param {Visitor} visitor the walker
* @returns {void}
*/
const walkBody = (body, visitor) => {
for (let i = 0, len = body.length; i < len; i++) walkNode(body[i], visitor);
};
const {
isBreakNode,
isLambdaNode,
isSwitchNode,
isDirectiveNode,
isForInNode,
isForNode,
isForOfNode,
isIterationStatementNode,
isLabeledStatementNode,
isScopeNode
} = ast;
/**
* Made apart from `visitNode`, whose calls would otherwise each allocate the
* context this closure keeps the walker in.
* @param {Walker} walker the walker
* @returns {() => void} what walks the children of the node it visits
*/
const descendCurrentOf = (walker) => () => {
// The node visited is the top of the stack, as nothing a visitor
// walks before descending stays pushed.
/** @type {(node: Node, visitor: Walker) => void} */ (
walker.webpackDescend
)(walker.stack[walker.stack.length - 1], walker);
};
/**
* @this {Walker} the walker
* @param {Node} node the node visited
* @param {((node: Node, visitor: Visitor) => void)=} descend walks its children
* @returns {boolean | void} whether the visitor stopped the walk
*/
function visitNode(node, descend) {
this.push(node);
let stopped;
if (descend) {
if (this.webpackDescendCurrent === undefined) {
this.webpackDescendCurrent = descendCurrentOf(this);
}
const outer = this.webpackDescend;
this.webpackDescend = descend;
stopped = /** @type {WalkVisit} */ (this.visit)(
node,
this.webpackDescendCurrent
);
if (!stopped) descend(node, this);
this.webpackDescend = outer;
} else {
stopped = /** @type {WalkVisit} */ (this.visit)(node, noop);
}
this.pop();
return stopped;
}
// The function or class a layer of directives belongs to, which it is
// made for only once something is written into it.
const DIRECTIVES_OWNER = Symbol("directives owner");
/**
* @param {Walker} walker the walker
* @param {Node} owner the function or class the layer belongs to
* @returns {Directives} its own layer of directives
*/
const addDirectivesLayer = (walker, owner) => {
const layer = Object.create(walker.directives);
layer[DIRECTIVES_OWNER] = owner;
walker.directives = layer;
return layer;
};
/**
* @param {Walker} walker the walker
* @returns {Directives} the layer of directives of the function or class it is in
*/
const ownDirectives = (walker) => {
const { stack } = walker;
for (let i = stack.length; --i >= 0;) {
const node = stack[i];
if ((node[KIND_BITS] & (KIND_LAMBDA | KIND_CLASS)) !== 0) {
return walker.directives[DIRECTIVES_OWNER] === node
? walker.directives
: addDirectivesLayer(walker, node);
}
}
return walker.directives;
};
/**
* @this {Walker} the walker
* @param {Node} node the node entered
* @returns {void}
*/
function pushNode(node) {
const kind = node[KIND_BITS];
if ((kind & KIND_DIRECTIVE) !== 0) {
if (!this.directives[node.directive]) {
ownDirectives(this)[node.directive] = node;
}
} else if ((kind & KIND_CLASS) !== 0 && !this.directives["use strict"]) {
addDirectivesLayer(this, node)["use strict"] = node;
}
this.stack.push(node);
}
/**
* @this {Walker} the walker
* @returns {void}
*/
function popNode() {
const node = this.stack.pop();
if (
node !== undefined &&
(node[KIND_BITS] & (KIND_LAMBDA | KIND_CLASS)) !== 0 &&
this.directives[DIRECTIVES_OWNER] === node
) {
this.directives = Object.getPrototypeOf(this.directives);
}
}
/**
* @this {Walker} the walker
* @param {number=} n how many parents up
* @returns {Node | undefined} that parent of the node visited
*/
function parentNode(n) {
return this.stack[this.stack.length - 2 - (n || 0)];
}
/**
* @this {Walker} the walker
* @returns {Node | undefined} the node visited
*/
function selfNode() {
return this.stack[this.stack.length - 1];
}
/**
* @this {Walker} the walker
* @param {(node: Node) => boolean} test what the node is
* @returns {Node | undefined} the nearest node on the stack passing the test
*/
function findParent(test) {
const { stack } = this;
for (let i = stack.length; --i >= 0;) {
if (test(stack[i])) return stack[i];
}
}
/**
* @this {Walker} the walker
* @returns {boolean} whether the node visited can run more than once in its function
*/
function isWithinLoop() {
const { stack } = this;
let i = stack.length - 1;
let child = stack[i];
while (i--) {
const node = stack[i];
if (isLambdaNode(node)) return false;
if (
isIterationStatementNode(node) &&
// The parts of a `for` loop that run only once.
!(isForNode(node) && child === node.init) &&
!((isForInNode(node) || isForOfNode(node)) && child === node.right)
) {
return true;
}
child = node;
}
return false;
}
/**
* @this {Walker} the walker
* @returns {Scope | undefined} the scope the node visited declares into
*/
function findScope() {
const { stack } = this;
for (let i = stack.length; --i >= 0;) {
const node = stack[i];
const kind = node[KIND_BITS];
if ((kind & (KIND_TOPLEVEL | KIND_LAMBDA)) !== 0) return node.block_scope;
// A class's scope, or a static block's, takes no declaration.
if (node.block_scope && (kind & KIND_SCOPE) === 0) {
return node.block_scope;
}
}
}
/**
* @this {Walker} the walker
* @param {string} type a directive, such as `use strict`
* @returns {Node | true | undefined} the directive, where it is in force, true where an option sets it
*/
function hasDirective(type) {
const directive = this.directives[type];
if (directive) return directive;
const node = this.stack[this.stack.length - 1];
/** @type {Node[] | undefined} */
const body = /** @type {Node[] | undefined} */ (
isScopeNode(node) ? ast.bodyOf(node) : undefined
);
if (body) {
for (let i = 0; i < body.length; ++i) {
const statement = body[i];
if (!isDirectiveNode(statement)) break;
if (statement.directive === type) return statement;
}
}
}
/**
* @this {Walker} the walker
* @param {Node} node a `break` or `continue`
* @returns {Node | undefined} the statement it leaves or continues
*/
function loopControlTarget(node) {
const { stack } = this;
if (node.label) {
for (let i = stack.length; --i >= 0;) {
const parent = stack[i];
if (
isLabeledStatementNode(parent) &&
parent.label.name === node.label.name
) {
return /** @type {Node} */ (parent.body);
}
}
} else {
for (let i = stack.length; --i >= 0;) {
const parent = stack[i];
if (
isIterationStatementNode(parent) ||
(isBreakNode(node) && isSwitchNode(parent))
) {
return parent;
}
}
}
}
/**
* terser's `TreeWalker` and `TreeTransformer`, as a plain object: the stack
* of nodes a walk is inside, the directives in force there, and what each
* node is handed to, `before` and `after` replacing it where they return a node.
* @param {WalkVisit=} visit what a walk hands each node to
* @param {TransformBefore=} before what a transform hands each node to on entering it
* @param {TransformAfter=} after what a transform hands each node to on leaving it
* @returns {Walker} the walker
*/
const createWalker = (visit, before, after) =>
/** @type {Walker} */ ({
visit,
before,
after,
/** @type {Node[]} */
stack: [],
/** @type {Directives} */
directives: Object.create(null),
// The compressor's `before` returns a squeezed node as it is, having
// pushed and popped it; only a directive's push leaves anything behind.
webpackSkipsSqueezed: false,
webpackCopiesLists: false,
// The compressor's improvements transform a block's functions first.
webpackDefunsFirst: false,
webpackDescend: undefined,
webpackDescendCurrent: undefined,
// A function assigning `this` fields reads as a constructor.
_visit: /** @type {Walker["_visit"]} */ (
/** @type {Walker["_visit"] | typeof visitNode} */ (visitNode)
),
push: pushNode,
pop: popNode,
parent: parentNode,
self: selfNode,
find_parent: findParent,
is_within_loop: isWithinLoop,
find_scope: findScope,
has_directive: hasDirective,
loopcontrol_target: loopControlTarget
});
/**
* @param {TransformBefore=} before what each node is handed to on entering it
* @param {TransformAfter=} after what each node is handed to on leaving it
* @returns {Walker} the transformer
*/
const createTransformer = (before, after) =>
createWalker(undefined, before, after);
/**
* Pushes a node a walk enters, tracking no directive.
* @this {Walker} the walk
* @param {Node} node the node
* @returns {void}
*/
function pushOnStack(node) {
this.stack.push(node);
}
/**
* Pops the node a walk leaves.
* @this {Walker} the walk
* @returns {void}
*/
function popOffStack() {
this.stack.pop();
}
const STACK_ONLY = { push: pushOnStack, pop: popOffStack };
/**
* A transformer for a walk that reads no directive: it pushes and pops only
* the stack `parent` and `find_scope` read.
* @param {TransformBefore=} before what each node is handed to on entering it
* @param {TransformAfter=} after what each node is handed to on leaving it
* @returns {Walker} the transformer
*/
const createStackTransformer = (before, after) =>
Object.assign(createWalker(undefined, before, after), STACK_ONLY);
// Where terser's `expression` field went, by ESTree type: a call's callee,
// a property read's object, an operator's argument.
/** @type {Record<string, "callee" | "object" | "argument" | "expression" | "discriminant" | "test">} */
const EXPRESSION_FIELDS = {
CallExpression: "callee",
NewExpression: "callee",
MemberExpression: "object",
WithStatement: "object",
UnaryExpression: "argument",
UpdateExpression: "argument",
AwaitExpression: "argument",
YieldExpression: "argument",
RestElement: "argument",
SpreadElement: "argument",
ChainExpression: "expression",
SwitchStatement: "discriminant",
SwitchCase: "test"
};
/**
* What terser's `expression` field held.
* @param {Node} node any node
* @returns {Node | undefined} that child, undefined where the node had none
*/
const expressionOf = (node) => {
const field = EXPRESSION_FIELDS[node.type];
return field === undefined ? undefined : node[field];
};
/**
* What terser's `body` field held, which ESTree names per type: a
* statement's expression, a branch's consequent, a switch's cases.
* @param {Node} node any node
* @returns {Node | Node[] | undefined} that child or list, undefined where the node had none
*/
const bodyOf = (node) => {
switch (node.type) {
case "FunctionExpression":
case "ArrowFunctionExpression":
case "FunctionDeclaration":
case "CatchClause":
return /** @type {Node} */ (node.body).body;
case "ExpressionStatement":
return node.directive === undefined ? node.expression : undefined;
case "IfStatement":
case "SwitchCase":
return node.consequent;
case "SwitchStatement":
return node.cases;
case "TryStatement":
return node.block;
// A class's `body` holds its members, where terser's held nothing.
case "ClassDeclaration":
case "ClassExpression":
return undefined;
default:
return node.body;
}
};
/**
* A key as terser held it: a property's name not computed as its string,
* which ESTree holds as a node, any other key as itself.
* @param {Node} key a member's key or a property read's property
* @returns {Node | string} the string or node terser held
*/
const terserKey = (key) => (ast.isSymbolPropertyNode(key) ? key.name : key);
/**
* What terser's destructuring held as `names`: an array pattern's elements,
* holes null, or an object pattern's properties.
* @param {Node} node a destructuring
* @returns {(Node | null)[]} the list
*/
const namesOf = (node) => (node.is_array ? node.elements : node.properties);
/**
* A property read's property as terser held it: a name not computed as its
* string, which ESTree holds as a node, a computed one as itself.
* @param {Node} node any node
* @returns {Node | string | undefined} the string or node, undefined where it has none
*/
const terserProperty = (node) =>
node.type === "MemberExpression" && !node.computed
? node.property.name
: node.property;
ast.createWalker = createWalker;
ast.createTransformer = createTransformer;
ast.createStackTransformer = createStackTransformer;
ast.expressionOf = expressionOf;
ast.bodyOf = bodyOf;
ast.terserKey = terserKey;
ast.terserProperty = terserProperty;
ast.namesOf = namesOf;
ast.walk = walk;
ast.walk_parent = walkParent;
ast.walk_body = walkBody;
ast.walk_abort = walkAbort;
ast._PURE = 0b00000001;
ast._INLINE = 0b00000010;
ast._NOINLINE = 0b00000100;
ast._KEY = 0b00001000;
ast._MANGLEPROP = 0b00010000;
ast.noSideEffectsFunctions = new WeakSet();
return /** @type {Ast} */ (/** @type {Ast | TerserAst} */ (ast));
};
/**
* terser's `Compressor`, as a plain object: the walker a compression pass
* transforms the tree with, holding the options. Its methods are installed
* by the phases into `methods`, which each compressor is given as it is made.
* @param {TerserModules} modules the modules, read when a compressor is made
* @returns {{ createCompressor: (options: Partial<CompressOptions>, settings: { false_by_default?: boolean, mangle_options?: MinifyOptions["mangle"] | MangleOptions }) => Compressor, methods: Partial<CompressorMethods> }} the factory and the methods it installs
*/
const createCompressorFactory = (modules) => {
const { ast } = modules;
const hasOwn = Object.prototype.hasOwnProperty;
/** @type {Partial<CompressorMethods>} */
const methods = {};
/** @type {WeakMap<Node | (Node | null)[], Node | (Node | null)[]>} */
const views = new WeakMap();
// The names terser's nodes held a child under, which ESTree names otherwise.
/** @type {Record<string, string>} */
const TERSER_NAMES = { args: "arguments" };
/**
* A node as a `pure_funcs` function reads it, which is written against
* terser's tree: its fields, and terser's names and `print_to_string` too.
* @template {NodeFieldValue | number | null | undefined} T
* @param {T} value a node, a list of nodes, or any other value a node holds
* @param {TokenTables} tables what the tree's token numbers are read through
* @returns {T} the view, or the value where it is none of those
*/
const terserView = (value, tables) => {
if (
value === null ||
typeof value !== "object" ||
!(Array.isArray(value) || ast.isSyntaxNode(value))
) {
return value;
}
let view = views.get(value);
if (view === undefined) {
view = new Proxy(value, {
get(target, key) {
if (Array.isArray(target)) {
return terserView(target[/** @type {`${number}`} */ (key)], tables);
}
if (key === "print_to_string") {
return (/** @type {FormatSettings=} */ given) =>
modules.printToString(target, given, tables);
}
if (key === "expression" && target.expression === undefined) {
return terserView(ast.expressionOf(target), tables);
}
const name =
typeof key === "string" &&
Object.prototype.hasOwnProperty.call(TERSER_NAMES, key) &&
ast.isCallNode(target)
? TERSER_NAMES[key]
: key;
return terserView(target[/** @type {keyof Node} */ (name)], tables);
}
});
views.set(value, view);
}
return /** @type {T} */ (view);
};
/**
* terser's `new Compressor(options, settings)`.
* @param {Partial<CompressOptions>} options the compress options
* @param {{ false_by_default?: boolean, mangle_options?: MinifyOptions["mangle"] | MangleOptions }} settings whether options default to off, and the mangle options
* @returns {Compressor} the compressor
*/
const create = (
options,
{
false_by_default: falseByDefault = false,
mangle_options: mangleOptions = false
}
) => {
const compressor = /** @type {Compressor} */ (ast.createWalker());
compressor.webpackSkipsSqueezed = true;
compressor.webpackDefunsFirst = Boolean(
modules.improvements && modules.improvements.enabled
);
// What the tree's token numbers are read through, once it is handed in.
compressor.tokenTables = NO_TOKEN_TABLES;
for (const name of Object.keys(methods)) {
/** @type {Record<string, CompressorMethod | undefined>} */ (
/** @type {Compressor | Record<string, CompressorMethod | undefined>} */ (
compressor
)
)[name] = /** @type {Record<string, CompressorMethod | undefined>} */ (
methods
)[name];
}
if (options.defaults !== undefined && !options.defaults) {
falseByDefault = true;
}
const on = !falseByDefault;
compressor.options = defaults(
options,
{
arguments: false,
arrows: on,
booleans: on,
booleans_as_integers: false,
collapse_vars: on,
comparisons: on,
computed_props: on,
conditionals: on,
dead_code: on,
defaults: true,
directives: on,
drop_console: false,
drop_debugger: on,
ecma: 5,
builtins_ecma: 5,
builtins_pure: false,
evaluate: on,
expression: false,
global_defs: false,
hoist_funs: false,
hoist_props: on,
hoist_vars: false,
ie8: false,
if_return: on,
inline: on,
join_vars: on,
keep_classnames: false,
keep_fargs: true,
keep_fnames: false,
keep_infinity: false,
lhs_constants: on,
loops: on,
module: false,
negate_iife: on,
passes: 1,
properties: on,
pure_getters: on && "strict",
pure_funcs: null,
// Off, a comparison or a computed key converting what may be an object
// or a symbol is kept, its `valueOf`, `toString` or throw included.
pure_conversions: true,
// Off, a class nobody reads is kept where it extends what may be no
// constructor, or one whose `prototype` is no object or `null`.
pure_heritage: true,
pure_new: false,
reduce_funcs: on,
reduce_vars: on,
sequences: on,
side_effects: on,
switches: on,
top_retain: null,
toplevel: Boolean(options && options.top_retain),
typeofs: on,
unsafe: false,
unsafe_arrows: false,
unsafe_comps: false,
unsafe_Function: false,
unsafe_math: false,
unsafe_symbols: false,
unsafe_methods: false,
unsafe_proto: false,
unsafe_regexp: false,
unsafe_undefined: false,
unused: on,
warnings: false
},
true
);
const globalDefinitions = compressor.options.global_defs;
if (typeof globalDefinitions === "object") {
for (const key in globalDefinitions) {
if (key[0] === "@" && hasOwn.call(globalDefinitions, key)) {
// Read as the one expression a parenthesis holds, as terser's
// `expression` parse reads it.
const { body, tokenTables } = modules.parse.parse(
`(${/** @type {string} */ (globalDefinitions[key])}\n)`
);
if (
/** @type {Node[]} */ (body).length !== 1 ||
!ast.isSimpleStatementNode(/** @type {Node[]} */ (body)[0])
) {
throw new Error(`\`${key}\` is not one expression`);
}
// The expression joins other trees, whose numbers name other tokens.
ast.walk(
/** @type {Node[]} */ (body)[0].expression,
(/** @type {Node} */ node) => {
node.startToken = /** @type {NodeToken} */ (
/** @type {TokenTables} */ (tokenTables).of(node.startToken)
);
node.endToken = /** @type {NodeToken} */ (
/** @type {TokenTables} */ (tokenTables).of(node.endToken)
);
}
);
globalDefinitions[key.slice(1)] = /** @type {Node[]} */ (
body
)[0].expression;
}
}
}
if (compressor.options.inline === true) compressor.options.inline = 3;
const pureFunctions = compressor.options.pure_funcs;
if (typeof pureFunctions === "function") {
compressor.pure_funcs = (/** @type {Node} */ node) =>
pureFunctions(
terserView(
node,
/** @type {{ tokenTables: TokenTables }} */ (compressor).tokenTables
)
);
} else {
compressor.pure_funcs = pureFunctions
? (/** @type {Node} */ node) =>
!pureFunctions.includes(modules.printToString(node.callee))
: () => true;
}
let topRetain = compressor.options.top_retain;
if (topRetain instanceof RegExp) {
const pattern = topRetain;
compressor.top_retain = (/** @type {Definition} */ definition) =>
pattern.test(definition.name);
} else if (typeof topRetain === "function") {
compressor.top_retain = topRetain;
} else if (topRetain) {
if (typeof topRetain === "string") topRetain = topRetain.split(/,/);
const names = topRetain;
compressor.top_retain = (/** @type {Definition} */ definition) =>
names.includes(definition.name);
}
if (compressor.options.module) {
compressor.directives["use strict"] = true;
compressor.options.toplevel = true;
}
const { toplevel } = compressor.options;
compressor.toplevel =
typeof toplevel === "string"
? { funcs: /funcs/.test(toplevel), vars: /vars/.test(toplevel) }
: { funcs: toplevel, vars: toplevel };
const { sequences } = compressor.options;
// terser compares with `==`, so `true` and "1" are a limit of 800 too.
compressor.sequences_limit =
Number(sequences) === 1 ? 800 : /** @type {number} */ (sequences) | 0;
compressor.evaluated_regexps = new Map();
/** @type {Node | undefined} */
compressor._toplevel = undefined;
compressor._mangle_options = mangleOptions
? formatMangleOptions(mangleOptions, modules.scope.base54)
: mangleOptions;
const { nativeObjects } = modules;
compressor.pure_access_globals =
nativeObjects.pure_access_globals(compressor);
compressor.is_pure_native_fn = nativeObjects.is_pure_native_fn(compressor);
compressor.is_pure_native_method =
nativeObjects.is_pure_native_method(compressor);
compressor.is_pure_native_static_fn =
nativeObjects.is_pure_native_static_fn(compressor);
compressor.is_pure_native_static_property =
nativeObjects.is_pure_native_static_property(compressor);
return compressor;
};
return { createCompressor: create, methods };
};
/**
* terser's `JS_Parse_Error`, what its parser throws for a source it refuses.
*/
class JSParseError extends Error {
/**
* @param {string} message what is wrong
* @param {string | null | undefined} filename the file
* @param {number} line the line, from 1
* @param {number} col the column, from 0
* @param {number} pos the offset
*/
constructor(message, filename, line, col, pos) {
super();
this.name = "SyntaxError";
this.message = message;
this.filename = filename;
this.line = line;
this.col = col;
this.pos = pos;
}
}
/**
* terser's `js_error`.
* @param {string} message what is wrong
* @param {string | null | undefined} filename the file
* @param {number} line the line, from 1
* @param {number} col the column, from 0
* @param {number} pos the offset
* @returns {never} it always throws
*/
const throwParseError = (message, filename, line, col, pos) => {
throw new JSParseError(message, filename, line, col, pos);
};
/** @typedef {{ bare_returns?: boolean, ecma?: number | null, filename?: string | null, html5_comments?: boolean, module?: boolean, shebang?: boolean, toplevel?: Node | null, [PARSE_ONTO_TABLES]?: TokenTables }} ParseOptions terser's parse options, and the tables a parse joining a tree numbers its tokens after */
/**
* What the parse phase shares with the rest: the operator precedences and
* reserved words from terser's tables, and the error a refused source throws.
* The parse phase installs `parse` itself.
* @returns {{ parse: (text: string, options?: ParseOptions) => Node, JS_Parse_Error: typeof JSParseError, js_error: typeof throwParseError, PRECEDENCE: Record<string, number>, ALL_RESERVED_WORDS: Set<string> }} the parser's tables
*/
const createParseTables = () => {
const { parserTables } = require("./syntax-printer-data");
const tables = parserTables();
/** @type {Record<string, number>} */
const PRECEDENCE = {};
for (const [index, level] of /** @type {string[][]} */ (
tables.PRECEDENCE
).entries()) {
for (const operator of level) PRECEDENCE[operator] = index + 1;
}
return {
parse: () => {
throw new Error("the parse phase is not installed");
},
JS_Parse_Error: JSParseError,
js_error: throwParseError,
PRECEDENCE,
ALL_RESERVED_WORDS: new Set(
/** @type {string[]} */ (tables.ALL_RESERVED_WORDS)
)
};
};
/**
* A regexp's source as terser's tokenizer reads it: a backslash before a
* character beyond ASCII adds no syntax, so terser drops it.
* @param {string} pattern the pattern as written
* @returns {string} it with those backslashes gone
*/
const regexpSourceAsRead = (pattern) => {
let backslash = pattern.indexOf("\\");
if (backslash === -1) return pattern;
let source = "";
let from = 0;
while (backslash !== -1 && backslash + 1 < pattern.length) {
const code = /** @type {number} */ (pattern.codePointAt(backslash + 1));
const escaped = String.fromCodePoint(code);
source +=
pattern.slice(from, backslash) + (code < 128 ? `\\${escaped}` : escaped);
from = backslash + 1 + escaped.length;
backslash = pattern.indexOf("\\", from);
}
return source + pattern.slice(from);
};
/**
* Hands an expression's comments to the parentheses around it, as terser's
* parser leaves them: the comments outside come first, and both tokens on
* each side share the list they are in.
* @param {NodeToken} openToken the opening parenthesis
* @param {NodeToken} closeToken the closing parenthesis
* @param {NodeToken | null | undefined} inner the expression's first token, if it has one
* @param {NodeToken | null | undefined} last the expression's last token, if it has one
* @returns {number} how many comments came before the opening parenthesis, -1 where the expression has no first token
*/
const bindParenthesisComments = (openToken, closeToken, inner, last) => {
let outerCount = -1;
if (inner) {
const outer = openToken.comments_before;
outerCount = outer.length;
// Built rather than moved: the list is shared where it is empty.
const before =
outer.length === 0
? inner.comments_before
: [...outer, ...inner.comments_before];
inner.comments_before = before;
openToken.comments_before = before;
// Only a parenthesis holding no comment of its own hands its line break
// to the first comment inside it, as terser's parser has it.
if (outer.length === 0 && before.length > 0) {
const [comment] = before;
if (!comment.nlb) {
comment.nlb = openToken.nlb;
openToken.nlb = false;
}
}
openToken.comments_after = inner.comments_after;
}
if (last) {
closeToken.comments_before = last.comments_before;
const after =
closeToken.comments_after.length === 0
? last.comments_after
: [...last.comments_after, ...closeToken.comments_after];
last.comments_after = after;
closeToken.comments_after = after;
}
return outerCount;
};
/** @typedef {import("./syntax-parser").NodeOffsets} NodeOffsets where a node webpack's parser builds sits in the source */
/** @typedef {import("./syntax-parser").AnyNodeType} AnyNodeType the `type` of a node webpack's parser builds */
/** @typedef {import("./syntax-parser").Identifier} EstreeIdentifier an ESTree identifier */
/** @typedef {import("./syntax-parser").Pattern} EstreePattern an ESTree binding pattern */
/** @typedef {import("./syntax-parser").AssignmentProperty} EstreeAssignmentProperty an ESTree object pattern's property */
/** @typedef {import("./syntax-parser").Directive} EstreeDirective an ESTree directive */
/** @typedef {import("./syntax-parser").ImportAttribute} EstreeImportAttribute an ESTree import attribute */
/** @typedef {import("./syntax-parser").MaybeNamedFunctionDeclaration} EstreeDefaultFunction an ESTree function declaration, unnamed where it is a default export */
/** @typedef {import("./syntax-parser").MaybeNamedClassDeclaration} EstreeDefaultClass an ESTree class declaration, unnamed where it is a default export */
/** @typedef {AnyNode | EstreeDefaultFunction | EstreeDefaultClass} EstreeInput an ESTree node the minifier's tree is built from */
/**
* What names something in ESTree: an identifier, a private name, or a literal
* of any kind, a regular expression as older producers write one included.
* @internal
* @typedef {NodeOffsets & { type: string, name?: string, value?: string | number | boolean | bigint | RegExp | null, raw?: string, regex?: { pattern: string, flags: string }, bigint?: string }} EstreeName
*/
/**
* An import's or export's attributes as older producers name them.
* @internal
* @typedef {{ assertions?: EstreeImportAttribute[] }} EstreeAssertions
*/
/**
* terser's `MOZ_TO_ME`: per ESTree type, what builds the minifier's node of a
* node of it, and the `FieldDefinition` older producers write.
* @internal
* @typedef {{ [Type in AnyNodeType]?: (estree: NodeOf<Type>) => Node | undefined } & { FieldDefinition: (estree: NodeOf<"PropertyDefinition">) => Node }} EstreeConverters
*/
/**
* Where an ESTree node terser's conversion writes sits, as its tokens say.
* @internal
* @typedef {{ start: { line: number, column: number }, end: { line: number, column: number | null | undefined } | null, source?: string }} EstreeLocation
*/
/**
* An ESTree node as terser's conversion writes it: its type, its fields, and
* where its tokens put it.
* @internal
* @typedef {{ type: string, range?: [number, number], loc?: EstreeLocation, [field: string]: EstreeOutField }} EstreeOut
*/
/**
* What a field of an ESTree node terser's conversion writes holds.
* @internal
* @typedef {EstreeOut | (EstreeOut | null)[] | { raw: string, cooked: string | null } | { pattern: string, flags: string } | [number, number] | EstreeLocation | string | number | boolean | null | undefined} EstreeOutField
*/
/**
* A node holding one node in `body` and in `consequent`, as a loop, a label, a
* `with`, a `catch`, an `if` or a conditional does, where other kinds hold a list.
* @internal
* @typedef {Node & { body: Node, consequent: Node }} SingleBodyNode
*/
/**
* A class's or an object's member: its `value` the node it holds.
* @internal
* @typedef {Node & { value: Node }} MemberNode
*/
/** @typedef {{ type: string, value: { raw: string, cooked: string | null }, tail: boolean }} EstreeTemplateElement a template's text as terser's conversion writes it */
/**
* `ast.adoptEstree`, which builds the minifier's tree from an ESTree one, the
* parser's or one handed in, and `ast.toEstree`, terser's way back.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installEstree = (modules) => {
const { ast } = modules;
const {
AccessorNode,
ArrayNode,
ArrowNode,
AssignNode,
AwaitNode,
BigIntNode,
BinaryNode,
BlockStatementNode,
BreakNode,
CallExpressionNode,
isCallNode,
CaseNode,
CatchNode,
ChainNode,
ClassBodyNode,
isClassNode,
ClassExpressionNode,
isClassExpressionNode,
ClassPrivatePropertyNode,
isClassPrivatePropertyNode,
ClassPropertyNode,
isClassPropertyNode,
ClassStaticBlockNode,
ConciseMethodNode,
ConditionalNode,
ConstNode,
isConstNode,
ContinueNode,
DebuggerNode,
DefaultNode,
DefaultAssignNode,
isDefaultAssignNode,
DefClassNode,
DefunNode,
DestructuringNode,
isDestructuringNode,
DirectiveNode,
DoNode,
DotNode,
DotHashNode,
DynamicImportNode,
EmptyStatementNode,
RestElementNode,
SpreadElementNode,
ClassGetterNode,
ClassSetterNode,
ClassMethodNode,
isExpansionNode,
ExportNode,
FalseNode,
FinallyNode,
ForNode,
ForInNode,
ForOfNode,
FunctionNode,
IfNode,
ImportNode,
ImportMetaNode,
InfinityNode,
LabelNode,
LabeledStatementNode,
LabelRefNode,
LetNode,
isLetNode,
NameMappingNode,
NewNode,
NewTargetNode,
isSyntaxNode,
NullNode,
NumberNode,
ObjectNode,
isObjectNode,
ObjectGetterNode,
isObjectGetterNode,
ObjectKeyValNode,
isObjectKeyValNode,
ObjectSetterNode,
isObjectSetterNode,
PrefixedTemplateStringNode,
PrivateGetterNode,
isPrivateGetterNode,
PrivateInNode,
PrivateMethodNode,
PrivateSetterNode,
isPrivateSetterNode,
isPropAccessNode,
RegExpNode,
ReturnNode,
isReturnNode,
SequenceNode,
SimpleStatementNode,
isSimpleStatementNode,
StringNode,
isStringNode,
SubNode,
SuperNode,
SwitchNode,
isSymbolNode,
SymbolCatchNode,
SymbolClassNode,
SymbolClassPropertyNode,
SymbolConstNode,
SymbolDefClassNode,
SymbolDefunNode,
SymbolExportNode,
isSymbolExportNode,
SymbolExportForeignNode,
isSymbolExportForeignNode,
SymbolFunargNode,
SymbolImportNode,
SymbolImportForeignNode,
isSymbolImportForeignNode,
SymbolLambdaNode,
SymbolLetNode,
SymbolMethodNode,
isSymbolMethodNode,
SymbolPrivatePropertyNode,
SymbolPropertyNode,
SymbolRefNode,
SymbolUsingNode,
SymbolVarNode,
TemplateSegmentNode,
TemplateStringNode,
ThisNode,
ThrowNode,
ParsedToken,
ToplevelNode,
TrueNode,
TryNode,
TryBlockNode,
UnaryPostfixNode,
UnaryPrefixNode,
isUnaryPrefixNode,
UsingNode,
isUsingNode,
UsingDefNode,
VarNode,
VarDefNode,
WhileNode,
WithNode,
YieldNode
} = ast;
const { isBasicIdentifier } = createUnicode();
// Where a parser hands its own tokens over, the token at an offset; null
// where the tree being converted comes without them.
/**
* @type {((pos: number, ending: boolean, forward?: number) => number | undefined) | null}
*/
let tokenAt = null;
// The source those tokens were read from, which a directive is read from.
/** @type {string | null} */
let tokenSource = null;
// The table those tokens' numbers are read through.
/** @type {TokenTable | null} */
let tokenTable = null;
/**
* @param {NodeToken | number} token a node's token, or its number in this parse
* @returns {NodeToken} the token itself
*/
const ownToken = (token) =>
typeof token === "number"
? /** @type {TokenTable} */ (tokenTable).token(
token - /** @type {TokenTable} */ (tokenTable).base
)
: token;
/**
* @param {NodeOffsets} estree an ESTree node
* @returns {NodeToken | number} the token where it starts
*/
const startToken = (estree) => {
if (tokenAt !== null) {
const own = tokenAt(estree.start, false);
if (own !== undefined) return own;
}
const { loc, range } = estree;
const start = loc && loc.start;
return new ParsedToken(
"",
"",
(start && start.line) || 0,
(start && start.column) || 0,
range ? range[0] : estree.start,
false,
[],
[],
loc && loc.source
);
};
/**
* terser reads `range[0]` here too, so a ranged node ends where it starts.
* @param {NodeOffsets} estree an ESTree node
* @returns {NodeToken | number} the token where it ends
*/
const endToken = (estree) => {
if (tokenAt !== null) {
const own = tokenAt(estree.end, true);
if (own !== undefined) return own;
}
const { loc, range } = estree;
const end = loc && loc.end;
return new ParsedToken(
"",
"",
(end && end.line) || 0,
(end && end.column) || 0,
range ? range[0] : estree.end,
false,
[],
[],
loc && loc.source
);
};
/**
* terser reads a sequence's end off `peek()`, the token past the one its last
* expression is followed by, which a trailing comment then prints before.
* @param {NodeOffsets} estree a sequence
* @returns {NodeToken | number} the token it ends at
*/
const sequenceEndToken = (estree) => {
if (tokenAt !== null) {
const own = tokenAt(estree.end, true, 2);
if (own !== undefined) return own;
}
return endToken(estree);
};
// The labels of the statements being converted, innermost last.
/** @type {Node[] | null} */
let fromLabels = null;
/**
* @param {EstreeInput | Node | null | undefined} estree an ESTree node, one the parser already converted, or null
* @returns {Node} terser's node, or null, which a child's type leaves out as `NO_NODE` does
*/
const fromEstree = (estree) => {
if (estree === null || estree === undefined) {
return /** @type {Node} */ (/** @type {Node | null} */ (null));
}
// A parser converting as it goes hands its children over already built.
if (isSyntaxNode(estree)) return estree;
const convert =
/** @type {((estree: EstreeInput) => Node) | undefined} */
(FROM_ESTREE[estree.type]);
if (typeof convert !== "function") {
throw new TypeError("MOZ_TO_ME[node.type] is not a function");
}
const node = convert(estree);
// Only a parser handing its own tokens over read the comments an
// annotation is written in; terser's conversion leaves every node bare.
if (
tokenAt !== null &&
node !== null &&
(ANNOTATED_TYPES.has(estree.type) ||
// terser leaves a tagged template unmarked, so never drops one.
(estree.type === "TaggedTemplateExpression" &&
modules.improvements !== undefined &&
modules.improvements.enabled))
) {
annotate(node);
}
if (
tokenAt !== null &&
node !== null &&
NO_SIDE_EFFECTS_TYPES.has(estree.type) &&
modules.improvements !== undefined &&
modules.improvements.enabled
) {
markNoSideEffects(node);
}
return node;
};
/**
* The quotes and the text between them of a string written where a directive
* can be, as terser's parser reads one: an escape in it is kept, so a
* directive spelled with one is not the directive it reads as.
* @param {Node} node the string the statement holds
* @returns {string | null | undefined} the source it was written as, null
* where there is none to read, undefined where no directive is written
*/
const directiveSource = (node) => {
if (tokenSource === null || node.startToken === undefined) return null;
const text = tokenSource;
const open = ownToken(node.startToken).pos;
const quote = text[open];
if (quote !== '"' && quote !== "'") return undefined;
for (let i = open + 1; i < text.length; i++) {
const character = text[i];
if (character === "\\") {
i++;
} else if (character === quote) {
return text.slice(open, i + 1);
}
}
return undefined;
};
// A `"` no backslash escapes: an odd run of them escapes it.
const UNESCAPED_DOUBLE_QUOTE = /(?:^|[^\\])(?:\\\\)*"/;
/**
* Quotes a directive's spelling, escaping the quote wherever it stands bare.
* @param {string} spelling the text between a directive's quotes, escapes kept
* @returns {string} the spelling in quotes
*/
const quoteSpelling = (spelling) => {
const quote = UNESCAPED_DOUBLE_QUOTE.test(spelling) ? "'" : '"';
let quoted = quote;
let escaped = false;
for (const char of spelling) {
if (char === quote && !escaped) quoted += "\\";
quoted += char;
escaped = char === "\\" && !escaped;
}
return quoted + quote;
};
/**
* Turns the leading string statements of a body into directives.
* @param {Node[]} body the statements
* @param {AnyNode[]=} prologue the ESTree statements, whose `directive` says which are directives when no source is held
* @returns {Node[]} the same array
*/
const normalizeDirectives = (body, prologue) => {
for (let i = 0; i < body.length; i++) {
const statement = body[i];
if (
!isSimpleStatementNode(statement) ||
!isStringNode(statement.expression)
) {
return body;
}
let raw = directiveSource(statement.expression);
if (raw === null && prologue !== undefined) {
const { directive, expression } = /** @type {EstreeDirective} */ (
prologue[i]
);
if (typeof directive !== "string") return body;
// `directive` is the raw text inside the quotes, escapes kept.
raw = expression.raw || quoteSpelling(directive);
}
// A string the source does not open a prologue with ends it: one inside
// parentheses is an expression, whatever it spells.
if (raw === undefined) return body;
const directive =
raw === null
? /** @type {string} */ (statement.expression.value)
: raw.slice(1, -1);
body[i] = DirectiveNode({
startToken: statement.startToken,
endToken: statement.endToken,
directive,
// The literal spelling it, as terser held the directive's quote; its
// value is what the spelling says, where `directive` keeps the spelling.
expression: StringNode({
startToken: statement.startToken,
endToken: statement.endToken,
value: statement.expression.value,
quote: raw === null ? statement.expression.quote : raw[0]
})
});
}
return body;
};
/**
* @returns {boolean} whether the `correct` phase is on, which reads a given
* ESTree tree where terser's conversion changes or throws on it
*/
const correcting = () =>
modules.corrections !== undefined && modules.corrections.enabled;
/**
* @param {AnyNode[]} statements a body given as ESTree
* @returns {AnyNode[] | undefined} them, when their `directive` decides
* the prologue: terser's conversion reads any leading string as one
*/
const prologueFromEstree = (statements) =>
tokenAt === null && correcting() ? statements : undefined;
/**
* A function's block, its leading string statements read as directives.
* @param {NodeOf<"BlockStatement">} estree the block
* @returns {Node} terser's block
*/
const blockFromEstree = (estree) => {
const block = fromEstree(estree);
normalizeDirectives(
/** @type {Node[]} */ (block.body),
prologueFromEstree(estree.body)
);
return block;
};
/**
* @param {EstreeImportAttribute[] | undefined} attributes an import's attributes
* @returns {Node | null} them as an object, or null when there are none
*/
const importAttributesFromEstree = (attributes) => {
if (!attributes || attributes.length === 0) return null;
return ObjectNode({
// terser reads the list's place as a node's, which it has not got.
startToken: startToken(
/** @type {NodeOffsets} */ (
/** @type {NodeOffsets | EstreeImportAttribute[]} */ (attributes)
)
),
endToken: endToken(
/** @type {NodeOffsets} */ (
/** @type {NodeOffsets | EstreeImportAttribute[]} */ (attributes)
)
),
properties: attributes.map((attribute) =>
ObjectKeyValNode({
startToken: startToken(attribute),
endToken: endToken(attribute),
key: propertyNameFromEstree(attribute.key),
value: fromEstree(attribute.value),
computed: false,
kind: "init",
method: false,
shorthand: false
})
)
});
};
/**
* @param {EstreeName} key an identifier or literal naming a property
* @returns {Node} the name, as terser's string read it
*/
const propertyNameFromEstree = (key) =>
SymbolPropertyNode({
startToken: startToken(key),
endToken: endToken(key),
name: key.type === "Identifier" ? key.name : String(key.value)
});
/**
* @param {AnyNode} key a property's key
* @param {boolean} computed whether it is computed
* @returns {string} the quote a string key had
*/
const quoteFromEstree = (key, computed) => {
if (computed || key.type !== "Literal" || typeof key.value !== "string") {
return "";
}
// Only a parser handing its own tokens over is known to have written `raw`
// from the source, which holds the quote the key had.
return tokenAt !== null && typeof key.raw === "string" ? key.raw[0] : '"';
};
/**
* @param {NodeFactory} SymbolType the symbol's class
* @param {EstreeName} estree an identifier or a string literal
* @param {boolean=} hasQuote whether it was a string
* @returns {Node} the symbol
*/
const symbolFromEstree = (SymbolType, estree, hasQuote) =>
makeNode(SymbolType, undefined, {
startToken: startToken(estree),
quote: hasQuote
? tokenAt !== null && typeof estree.raw === "string"
? estree.raw[0]
: '"'
: undefined,
name: estree.type === "Identifier" ? estree.name : String(estree.value),
endToken: endToken(estree)
});
/**
* @param {NodeOf<"FunctionExpression"> | EstreeDefaultFunction} estree a function
* @param {boolean} isMethod whether it is a method's value
* @returns {Node} terser's function
*/
const lambdaFromEstree = (estree, isMethod) =>
(isMethod ? AccessorNode : FunctionNode)({
startToken: startToken(estree),
endToken: endToken(estree),
id:
estree.id &&
symbolFromEstree(
isMethod ? SymbolMethodNode : SymbolLambdaNode,
estree.id
),
params: estree.params.map((param) =>
patternFromEstree(param, SymbolFunargNode)
),
generator: estree.generator,
async: estree.async,
body: blockFromEstree(estree.body)
});
/**
* @param {EstreePattern | EstreeAssignmentProperty | NodeOf<"SpreadElement">} estree a binding pattern
* @param {NodeFactory} SymbolType the class of the names it binds
* @returns {Node} terser's binding
*/
const patternFromEstree = (estree, SymbolType) => {
switch (estree.type) {
case "ObjectPattern":
return makeNode(DestructuringNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
elements: null,
properties: estree.properties.map((property) =>
patternFromEstree(property, SymbolType)
),
is_array: false
});
case "Property": {
const { key } = estree;
/** @type {NodeProps} */
const args = {
startToken: startToken(key || estree.value),
endToken: endToken(estree.value),
key: estree.computed
? fromEstree(estree.key)
: propertyNameFromEstree(key),
quote:
!estree.computed &&
key.type === "Literal" &&
typeof key.value === "string"
? '"'
: "",
value: patternFromEstree(estree.value, SymbolType),
computed: Boolean(estree.computed),
kind: "init",
method: false,
shorthand: Boolean(estree.shorthand)
};
return ObjectKeyValNode(args);
}
case "ArrayPattern":
return makeNode(DestructuringNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
elements: estree.elements.map((element) =>
element === null ? null : patternFromEstree(element, SymbolType)
),
properties: null,
is_array: true
});
case "SpreadElement":
case "RestElement":
return RestElementNode({
startToken: startToken(estree),
endToken: endToken(estree),
argument: patternFromEstree(
/** @type {EstreePattern} */ (estree.argument),
SymbolType
)
});
case "AssignmentPattern":
return DefaultAssignNode({
startToken: startToken(estree),
endToken: endToken(estree),
left: patternFromEstree(estree.left, SymbolType),
operator: "=",
right: fromEstree(estree.right)
});
case "Identifier":
return SymbolType({
startToken: startToken(estree),
endToken: endToken(estree),
name: estree.name
});
default:
throw new Error(`Invalid node type for destructuring: ${estree.type}`);
}
};
/**
* @param {EstreeIdentifier | null | undefined} estree a `break` or `continue` label, or null
* @returns {Node | null} the label reference, tied to its label
*/
const labelReferenceFromEstree = (estree) => {
if (!estree) return null;
const label = symbolFromEstree(LabelRefNode, estree);
const labels = /** @type {Node[]} */ (fromLabels);
let i = labels.length;
while (i--) {
if (label.name === labels[i].name) {
label.definition = /** @type {Definition} */ (
/** @type {Definition | Node} */ (labels[i])
);
break;
}
}
return label;
};
/**
* @param {Omit<NodeOf<"UnaryExpression" | "UpdateExpression">, "prefix"> & { prefix?: boolean }} estree a unary or update expression, without `prefix` where an older producer wrote a unary
* @returns {Node} terser's prefix or postfix unary
*/
const unaryFromEstree = (estree) => {
const prefix =
"prefix" in estree ? estree.prefix : estree.type === "UnaryExpression";
return makeNode(prefix ? UnaryPrefixNode : UnaryPostfixNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
operator: estree.operator,
argument: fromEstree(estree.argument)
});
};
/**
* @param {NodeOf<"ClassDeclaration" | "ClassExpression"> | EstreeDefaultClass} estree a class declaration or expression
* @param {boolean=} asExpression whether to build the expression, whatever the type says
* @returns {Node} terser's class
*/
const classFromEstree = (estree, asExpression) => {
const expression =
asExpression === undefined
? estree.type !== "ClassDeclaration"
: asExpression;
return (expression ? ClassExpressionNode : DefClassNode)({
startToken: startToken(estree),
endToken: endToken(estree),
id:
estree.id &&
symbolFromEstree(
expression ? SymbolClassNode : SymbolDefClassNode,
estree.id
),
superClass: fromEstree(estree.superClass),
body: ClassBodyNode({
startToken: startToken(estree.body),
endToken: endToken(estree.body),
body: estree.body.body.map(fromEstree)
})
});
};
// How many of a parenthesis token's comments came before the parenthesis,
// which are the only ones an annotation is read from.
/** @type {WeakMap<NodeToken, number>} */
const outerCommentsBeforeCounts = new WeakMap();
// The node kinds terser's parser annotates, by the ESTree type each is built
// from; annotating another would mark what terser leaves unmarked.
const ANNOTATED_TYPES = new Set([
"CallExpression",
"NewExpression",
"ImportExpression",
"MemberExpression",
"Property",
"MethodDefinition",
"PropertyDefinition",
"Literal"
]);
/** @type {[RegExp, number][]} */
const ANNOTATIONS = [
[/[@#]__PURE__/, ast._PURE],
[/[@#]__INLINE__/, ast._INLINE],
[/[@#]__NOINLINE__/, ast._NOINLINE],
[/[@#]__KEY__/, ast._KEY],
[/[@#]__MANGLE_PROP__/, ast._MANGLEPROP]
];
// What `#__NO_SIDE_EFFECTS__` marks, as Rollup reads it: a function, or the
// functions a declaration or an export before it declares.
const NO_SIDE_EFFECTS_TYPES = new Set([
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
"VariableDeclaration",
"ExportNamedDeclaration",
"ExportDefaultDeclaration"
]);
/**
* Adds the functions a node declares, and their names, to
* `ast.noSideEffectsFunctions` when a `#__NO_SIDE_EFFECTS__` comment before
* it, outside its parentheses, or before the function itself marks them.
* @param {Node} node the node built
* @returns {void}
*/
const markNoSideEffects = (node) => {
const token = node.startToken;
if (token === undefined || token === null) return;
const own = typeof token === "number";
const table = /** @type {TokenTable} */ (tokenTable);
const comments = own
? table.commentsOf(token - table.base, false)
: token.comments_before;
const outside = own
? table.outerCounts.get(token)
: outerCommentsBeforeCounts.get(token);
const end = outside === undefined ? comments.length : outside;
let annotated = false;
for (let i = 0; i < end; i++) {
if (/[@#]__NO_SIDE_EFFECTS__/.test(comments[i].value)) annotated = true;
}
const declared = ast.isExportNode(node)
? node.exported_definition || node.exported_value
: node;
if (!declared) return;
const marked = ast.noSideEffectsFunctions;
if (ast.isLambdaNode(declared)) {
if (annotated) marked.add(declared);
} else if (declared.declarations) {
// A function marked on its own marks the name declared with it too.
for (const declaration of declared.declarations) {
const { init } = declaration;
if (init && ast.isLambdaNode(init) && (annotated || marked.has(init))) {
marked.add(init);
marked.add(declaration.id);
}
}
}
};
/**
* Marks a node with the `#__PURE__`-style comment nearest before it, as
* terser's parser does, only those outside its parentheses counting.
* @param {Node} node the node built
* @returns {void}
*/
const annotate = (node) => {
const token = node.startToken;
if (token === undefined || token === null) return;
const own = typeof token === "number";
const table = /** @type {TokenTable} */ (tokenTable);
const comments = own
? table.commentsOf(token - table.base, false)
: token.comments_before;
const outside = own
? table.outerCounts.get(token)
: outerCommentsBeforeCounts.get(token);
let i = outside === undefined ? comments.length : outside;
while (--i >= 0) {
const { value } = comments[i];
if (!/[@#]__/.test(value)) continue;
for (const [pattern, annotation] of ANNOTATIONS) {
if (!pattern.test(value)) continue;
node._annotations |= annotation;
// WHY: terser's parser annotates a `.` or `[…]` link by annotating
// whatever the rest of the chain returns, so such a link holds the mark
// only where the chain ends there, while every call annotates itself.
const inner = /** @type {Node} */ (
node.type === "TaggedTemplateExpression"
? node.tag
: ast.expressionOf(node)
);
if (isPropAccessNode(inner) && inner.startToken === token) {
inner._annotations &= ~annotation;
}
return;
}
}
};
/**
* The expression parentheses hold, holding the parentheses' own tokens, as
* terser's parser leaves it: the comments outside them come first, and both
* tokens share the list they are in.
* @param {NodeOf<"ParenthesizedExpression">} estree a parenthesized expression
* @returns {Node} the expression inside it
*/
const parenthesizedFromEstree = (estree) => {
const expression = fromEstree(estree.expression);
const open = startToken(estree);
const close = endToken(estree);
// terser reads a sequence's end off `peek()` once the parenthesis holding
// it is read, so this one ends two tokens past its own close.
if (estree.expression.type === "SequenceExpression") {
expression.endToken = sequenceEndToken(estree);
}
const outerCount = bindParenthesisComments(
ownToken(open),
ownToken(close),
ownToken(expression.startToken),
ownToken(expression.endToken)
);
if (outerCount !== -1) {
if (typeof open === "number") {
/** @type {TokenTable} */ (tokenTable).outerCounts.set(
open,
outerCount
);
} else {
outerCommentsBeforeCounts.set(open, outerCount);
}
}
expression.startToken = open;
expression.endToken = close;
// terser reads an annotation again off the parenthesis a call opens at.
if (isCallNode(expression)) annotate(expression);
return expression;
};
/** @type {EstreeConverters} */
const FROM_ESTREE = {
ParenthesizedExpression: parenthesizedFromEstree,
Program: (estree) =>
ToplevelNode({
startToken: startToken(estree),
endToken: endToken(estree),
body: normalizeDirectives(
estree.body.map(fromEstree),
prologueFromEstree(estree.body)
)
}),
ArrayPattern: (estree) =>
makeNode(DestructuringNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
elements: estree.elements.map((element) =>
element === null ? null : fromEstree(element)
),
properties: null,
is_array: true
}),
ObjectPattern: (estree) =>
makeNode(DestructuringNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
elements: null,
properties: estree.properties.map(fromEstree),
is_array: false
}),
AssignmentPattern: (estree) =>
DefaultAssignNode({
startToken: startToken(estree),
endToken: endToken(estree),
left: fromEstree(estree.left),
operator: "=",
right: fromEstree(estree.right)
}),
SpreadElement: (estree) =>
SpreadElementNode({
startToken: startToken(estree),
endToken: endToken(estree),
argument: fromEstree(estree.argument)
}),
RestElement: (estree) =>
RestElementNode({
startToken: startToken(estree),
endToken: endToken(estree),
argument: fromEstree(estree.argument)
}),
TemplateElement: (estree) =>
TemplateSegmentNode({
startToken: startToken(estree),
endToken: endToken(estree),
value: estree.value.cooked,
raw: estree.value.raw
}),
TemplateLiteral: (estree) => {
/** @type {Node[]} */
const segments = [];
for (let i = 0; i < estree.quasis.length; i++) {
segments.push(fromEstree(estree.quasis[i]));
if (estree.expressions[i]) {
segments.push(fromEstree(estree.expressions[i]));
}
}
return TemplateStringNode({
startToken: startToken(estree),
endToken: endToken(estree),
segments
});
},
TaggedTemplateExpression: (estree) =>
PrefixedTemplateStringNode({
startToken: startToken(estree),
endToken: endToken(estree),
quasi: fromEstree(estree.quasi),
tag: fromEstree(estree.tag)
}),
FunctionDeclaration: (estree) =>
DefunNode({
startToken: startToken(estree),
endToken: endToken(estree),
id: estree.id && symbolFromEstree(SymbolDefunNode, estree.id),
params: estree.params.map((param) =>
patternFromEstree(param, SymbolFunargNode)
),
generator: estree.generator,
async: estree.async,
body: blockFromEstree(estree.body)
}),
FunctionExpression: (estree) => lambdaFromEstree(estree, false),
ArrowFunctionExpression: (estree) => {
// terser's conversion leaves an arrow's directive a statement, where its
// parser reads one, so the body turns sloppy: corrections read it too.
const body =
estree.body.type === "BlockStatement"
? tokenAt === null && !correcting()
? fromEstree(estree.body)
: blockFromEstree(estree.body)
: BlockStatementNode({
body: [
// terser's parser gives the return the tokens of the expression
// it returns; its conversion, `make_node` with no original,
// leaves it without any.
ReturnNode({
argument: fromEstree(estree.body),
startToken:
tokenAt === null ? undefined : startToken(estree.body),
// terser reads the token it has reached, past the expression.
endToken:
tokenAt === null
? undefined
: tokenAt(estree.body.end, false)
})
]
});
return ArrowNode({
startToken: startToken(estree),
// terser's parser leaves an arrow without the token it ends at.
endToken: tokenAt === null ? endToken(estree) : undefined,
params: estree.params.map((param) =>
patternFromEstree(param, SymbolFunargNode)
),
body,
async: estree.async
});
},
ExpressionStatement: (estree) =>
SimpleStatementNode({
startToken: startToken(estree),
endToken: endToken(estree),
expression: fromEstree(estree.expression)
}),
TryStatement: (
/** @type {NodeOf<"TryStatement"> & { handlers?: NodeOf<"CatchClause">[], guardedHandlers?: NodeOf<"CatchClause">[] }} */ estree
) => {
const handlers = estree.handlers || [estree.handler];
if (
handlers.length > 1 ||
(estree.guardedHandlers && estree.guardedHandlers.length)
) {
throw new Error("Multiple catch clauses are not supported.");
}
return TryNode({
startToken: startToken(estree),
endToken: endToken(estree),
block: TryBlockNode(fromEstree(estree.block)),
handler: fromEstree(handlers[0]),
finalizer: estree.finalizer
? FinallyNode(fromEstree(estree.finalizer))
: null
});
},
Property: (estree) => {
// terser's parser opens a computed property at the `[`, where its
// conversion reads the key, which starts one token later.
const opensAt =
tokenAt !== null && estree.computed
? estree
: estree.key || estree.value;
if (estree.kind === "init" && !estree.method) {
return ObjectKeyValNode({
startToken: startToken(opensAt),
endToken: endToken(estree.value),
key: estree.computed
? fromEstree(estree.key)
: propertyNameFromEstree(estree.key),
quote: quoteFromEstree(estree.key, estree.computed),
value: fromEstree(estree.value),
computed: Boolean(estree.computed),
kind: "init",
method: false,
shorthand: Boolean(estree.shorthand)
});
}
const value = lambdaFromEstree(
/** @type {NodeOf<"FunctionExpression">} */ (estree.value),
true
);
const args = {
startToken: startToken(opensAt),
endToken: endToken(estree.value),
key: estree.computed
? fromEstree(estree.key)
: symbolFromEstree(SymbolMethodNode, estree.key),
quote: quoteFromEstree(estree.key, estree.computed),
static: false,
value,
computed: Boolean(estree.computed),
kind: estree.kind,
method: Boolean(estree.method),
shorthand: false
};
if (estree.kind === "get") return ObjectGetterNode(args);
if (estree.kind === "set") return ObjectSetterNode(args);
if (estree.method) return ConciseMethodNode(args);
},
MethodDefinition: (estree) => {
const isPrivate = estree.key.type === "PrivateIdentifier";
// terser's parser names a member with the token it read, where its
// conversion leaves the symbol without one.
const key = estree.computed
? fromEstree(estree.key)
: SymbolMethodNode({
type: isPrivate ? "PrivateIdentifier" : "Identifier",
name:
/** @type {EstreeName} */ (estree.key).name ||
String(/** @type {EstreeName} */ (estree.key).value),
startToken: tokenAt === null ? undefined : startToken(estree.key),
endToken: tokenAt === null ? undefined : endToken(estree.key)
});
const args = {
startToken: startToken(estree),
endToken: endToken(estree),
key,
quote: quoteFromEstree(estree.key, estree.computed),
value: lambdaFromEstree(estree.value, true),
static: estree.static,
computed: Boolean(estree.computed),
kind: estree.kind
};
if (estree.kind === "get") {
return (isPrivate ? PrivateGetterNode : ClassGetterNode)(args);
}
if (estree.kind === "set") {
return (isPrivate ? PrivateSetterNode : ClassSetterNode)(args);
}
return (isPrivate ? PrivateMethodNode : ClassMethodNode)(args);
},
FieldDefinition: (estree) => {
if (!estree.computed && estree.key.type !== "Identifier") {
throw new Error("Non-Identifier key in FieldDefinition");
}
// terser reads the key's symbol as an expression: computed.
return ClassPropertyNode({
startToken: startToken(estree),
endToken: endToken(estree),
quote: quoteFromEstree(estree.key, estree.computed),
key: fromEstree(estree.key),
value: fromEstree(estree.value),
static: estree.static,
computed: true
});
},
PropertyDefinition: (estree) => {
if (!estree.computed && estree.key.type === "PrivateIdentifier") {
return ClassPrivatePropertyNode({
startToken: startToken(estree),
endToken: endToken(estree),
key: fromEstree(estree.key),
value: fromEstree(estree.value),
static: estree.static,
computed: false
});
}
return ClassPropertyNode({
startToken: startToken(estree),
endToken: endToken(estree),
quote: quoteFromEstree(estree.key, estree.computed),
key: estree.computed
? fromEstree(estree.key)
: symbolFromEstree(SymbolClassPropertyNode, estree.key),
value: fromEstree(estree.value),
static: estree.static,
computed: Boolean(estree.computed)
});
},
PrivateIdentifier: (estree) =>
SymbolPrivatePropertyNode({
startToken: startToken(estree),
endToken: endToken(estree),
name: estree.name
}),
StaticBlock: (estree) =>
ClassStaticBlockNode({
startToken: startToken(estree),
endToken: endToken(estree),
body: estree.body.map(fromEstree)
}),
ArrayExpression: (estree) =>
ArrayNode({
startToken: startToken(estree),
endToken: endToken(estree),
elements: estree.elements.map((element) =>
element === null ? null : fromEstree(element)
)
}),
ObjectExpression: (estree) =>
ObjectNode({
startToken: startToken(estree),
endToken: endToken(estree),
properties: estree.properties.map((property) => {
// terser converts every other member as a plain property.
if (property.type !== "SpreadElement") property.type = "Property";
return fromEstree(property);
})
}),
SequenceExpression: (estree) =>
SequenceNode({
startToken: startToken(estree),
endToken: sequenceEndToken(estree),
expressions: estree.expressions.map(fromEstree)
}),
MemberExpression: (estree) => {
if (estree.property.type === "PrivateIdentifier") {
return DotHashNode({
startToken: startToken(estree),
endToken: endToken(estree),
property: fromEstree(estree.property),
object: fromEstree(estree.object),
optional: estree.optional || false,
computed: false
});
}
return (estree.computed ? SubNode : DotNode)({
startToken: startToken(estree),
endToken: endToken(estree),
property: estree.computed
? fromEstree(estree.property)
: propertyNameFromEstree(estree.property),
object: fromEstree(estree.object),
optional: estree.optional || false,
computed: Boolean(estree.computed)
});
},
ChainExpression: (estree) =>
ChainNode({
startToken: startToken(estree),
endToken: endToken(estree),
expression: fromEstree(estree.expression)
}),
SwitchCase: (estree) =>
(estree.test ? CaseNode : DefaultNode)({
startToken: startToken(estree),
endToken: endToken(estree),
test: fromEstree(estree.test),
consequent: estree.consequent.map(fromEstree)
}),
VariableDeclaration: (estree) => {
let DeclarationType = VarNode;
let DefinitionType = VarDefNode;
let SymbolType = SymbolVarNode;
let awaitUsing = false;
if (estree.kind === "const") {
DeclarationType = ConstNode;
SymbolType = SymbolConstNode;
} else if (estree.kind === "let") {
DeclarationType = LetNode;
SymbolType = SymbolLetNode;
} else if (estree.kind === "using" || estree.kind === "await using") {
DeclarationType = UsingNode;
DefinitionType = UsingDefNode;
SymbolType = SymbolUsingNode;
awaitUsing = estree.kind === "await using";
}
return DeclarationType({
startToken: startToken(estree),
endToken: endToken(estree),
declarations: estree.declarations.map((declarator) =>
DefinitionType({
startToken: startToken(declarator),
endToken: endToken(declarator),
id: patternFromEstree(declarator.id, SymbolType),
init: fromEstree(declarator.init)
})
),
await: awaitUsing
});
},
ImportDeclaration: (
/** @type {NodeOf<"ImportDeclaration"> & EstreeAssertions} */ estree
) => {
let importedName = null;
/** @type {Node[] | null} */
let importedNames = null;
for (const specifier of estree.specifiers) {
if (
specifier.type === "ImportSpecifier" ||
specifier.type === "ImportNamespaceSpecifier"
) {
if (!importedNames) importedNames = [];
importedNames.push(fromEstree(specifier));
} else if (specifier.type === "ImportDefaultSpecifier") {
importedName = fromEstree(specifier);
}
}
return ImportNode({
startToken: startToken(estree),
endToken: endToken(estree),
imported_name: importedName,
imported_names: importedNames,
source: fromEstree(estree.source),
attributes: importAttributesFromEstree(
estree.attributes || estree.assertions
),
phase: estree.phase || null
});
},
ImportSpecifier: (estree) =>
makeNode(NameMappingNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
imported: symbolFromEstree(
SymbolImportForeignNode,
estree.imported,
estree.imported.type === "Literal"
),
exported: null,
local: symbolFromEstree(SymbolImportNode, estree.local)
}),
ImportDefaultSpecifier: (estree) =>
symbolFromEstree(SymbolImportNode, estree.local),
ImportNamespaceSpecifier: (estree) =>
makeNode(NameMappingNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
imported: SymbolImportForeignNode({ name: "*" }),
exported: null,
local: symbolFromEstree(SymbolImportNode, estree.local)
}),
ImportExpression: (estree) => {
const args = [fromEstree(estree.source)];
if (estree.options) args.push(fromEstree(estree.options));
return DynamicImportNode({
startToken: startToken(estree),
endToken: endToken(estree),
phase: estree.phase,
args
});
},
ExportAllDeclaration: (
/** @type {NodeOf<"ExportAllDeclaration"> & EstreeAssertions} */ estree
) => {
const foreignName =
estree.exported === null || estree.exported === undefined
? SymbolExportForeignNode({ name: "*" })
: symbolFromEstree(
SymbolExportForeignNode,
estree.exported,
estree.exported.type === "Literal"
);
return makeNode(ExportNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
exported_names: [
makeNode(NameMappingNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
local: SymbolExportNode({ name: "*" }),
imported: null,
exported: foreignName
})
],
source: fromEstree(estree.source),
attributes: importAttributesFromEstree(
estree.attributes || estree.assertions
)
});
},
ExportNamedDeclaration: (
/** @type {NodeOf<"ExportNamedDeclaration"> & EstreeAssertions} */ estree
) => {
if (estree.declaration) {
return makeNode(ExportNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
exported_definition: fromEstree(estree.declaration),
exported_names: null,
source: null,
attributes: null
});
}
return makeNode(ExportNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
exported_definition: null,
exported_names:
estree.specifiers && estree.specifiers.length
? estree.specifiers.map(fromEstree)
: [],
source: fromEstree(estree.source),
attributes: importAttributesFromEstree(
estree.attributes || estree.assertions
)
});
},
ExportDefaultDeclaration: (
/** @type {NodeOf<"ExportDefaultDeclaration"> & { declaration: { id?: EstreeIdentifier | null } }} */ estree
) => {
const { declaration } = estree;
const isClass = declaration.type === "ClassDeclaration";
const isFunction = declaration.type === "FunctionDeclaration";
const start = startToken(estree);
const end = endToken(estree);
// WHY: terser's conversion exports every declaration as a value, where
// its parser exports a named one as a declaration and an anonymous class
// or function as the expression it is; `drop_unused` then reads a name
// the value has not got. Only a tree we parsed ourselves is ours to fix,
// and, while correcting, an anonymous one, which terser's throws on.
if (
(tokenAt === null && (declaration.id || !correcting())) ||
(!isClass && !isFunction)
) {
return makeNode(ExportNode, undefined, {
startToken: start,
endToken: end,
exported_value: fromEstree(declaration),
is_default: true
});
}
if (declaration.id) {
return makeNode(ExportNode, undefined, {
startToken: start,
endToken: end,
exported_definition: fromEstree(declaration),
is_default: true
});
}
return makeNode(ExportNode, undefined, {
startToken: start,
endToken: end,
exported_value: isClass
? classFromEstree(declaration, true)
: lambdaFromEstree(declaration, false),
is_default: true
});
},
ExportSpecifier: (estree) =>
makeNode(NameMappingNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
imported: null,
exported: symbolFromEstree(
SymbolExportForeignNode,
estree.exported,
estree.exported.type === "Literal"
),
local: symbolFromEstree(
SymbolExportNode,
estree.local,
estree.local.type === "Literal"
)
}),
Literal: (/** @type {EstreeName} */ estree) => {
const { value, regex } = estree;
/** @type {NodeProps} */
const args = {
startToken: startToken(estree),
endToken: endToken(estree)
};
if (regex && regex.pattern) {
args.value = {
source:
tokenAt === null
? regex.pattern
: regexpSourceAsRead(regex.pattern),
flags: regex.flags
};
return RegExpNode(args);
} else if (regex) {
// A legacy regexp, read from its source.
const regexSource = /** @type {string} */ (estree.raw || value);
const match = regexSource.match(/^\/(.*)\/(\w*)$/);
if (!match) throw new Error(`Invalid regex source ${regexSource}`);
args.value = { source: match[1], flags: match[2] };
return RegExpNode(args);
}
const bigint =
typeof value === "bigint" ? value.toString() : estree.bigint;
if (typeof bigint === "string") {
// The digits are kept as written, radix and all, where `bigint` holds
// them in decimal; only `raw` says how they were read. A separator is
// not one of the digits, so it goes, as the tokenizer drops it.
args.value =
tokenAt !== null && typeof estree.raw === "string"
? estree.raw.slice(0, -1).replace(/_/g, "")
: bigint;
args.raw = estree.raw;
return BigIntNode(args);
}
if (value === null) return NullNode(args);
switch (typeof value) {
case "string":
// Only a parser handing its own tokens over is known to have written
// `raw` from the source, which holds the quote the string had.
args.quote =
tokenAt !== null && typeof estree.raw === "string"
? estree.raw[0]
: '"';
args.value = value;
return StringNode(args);
case "number":
// As terser's parser reads it: a literal too large for a double is
// the infinity it holds, which the printer writes as a division.
if (value === Number.POSITIVE_INFINITY) {
return InfinityNode(args);
}
args.value = value;
args.raw = estree.raw || value.toString();
return NumberNode(args);
case "boolean":
return (value ? TrueNode : FalseNode)(args);
}
},
MetaProperty: (estree) => {
if (estree.meta.name === "new" && estree.property.name === "target") {
return NewTargetNode({
startToken: startToken(estree),
endToken: endToken(estree)
});
} else if (
estree.meta.name === "import" &&
estree.property.name === "meta"
) {
return ImportMetaNode({
startToken: startToken(estree),
endToken: endToken(estree)
});
}
},
Identifier: (estree) =>
SymbolRefNode({
startToken: startToken(estree),
endToken: endToken(estree),
name: estree.name
}),
EmptyStatement: (estree) =>
EmptyStatementNode({
startToken: startToken(estree),
endToken: endToken(estree)
}),
BlockStatement: (estree) =>
BlockStatementNode({
startToken: startToken(estree),
endToken: endToken(estree),
body: estree.body.map(fromEstree)
}),
IfStatement: (estree) =>
IfNode({
startToken: startToken(estree),
endToken: endToken(estree),
test: fromEstree(estree.test),
consequent: fromEstree(estree.consequent),
alternate: fromEstree(estree.alternate)
}),
LabeledStatement: (estree) => {
const labels = /** @type {Node[]} */ (fromLabels);
try {
const label = symbolFromEstree(LabelNode, estree.label);
labels.push(label);
return LabeledStatementNode({
startToken: startToken(estree),
endToken: endToken(estree),
label,
body: fromEstree(estree.body)
});
} finally {
labels.pop();
}
},
BreakStatement: (estree) =>
BreakNode({
startToken: startToken(estree),
endToken: endToken(estree),
label: labelReferenceFromEstree(estree.label)
}),
ContinueStatement: (estree) =>
ContinueNode({
startToken: startToken(estree),
endToken: endToken(estree),
label: labelReferenceFromEstree(estree.label)
}),
WithStatement: (estree) =>
WithNode({
startToken: startToken(estree),
endToken: endToken(estree),
object: fromEstree(estree.object),
body: fromEstree(estree.body)
}),
SwitchStatement: (estree) =>
SwitchNode({
startToken: startToken(estree),
endToken: endToken(estree),
discriminant: fromEstree(estree.discriminant),
cases: estree.cases.map(fromEstree)
}),
ReturnStatement: (estree) =>
ReturnNode({
startToken: startToken(estree),
endToken: endToken(estree),
argument: fromEstree(estree.argument)
}),
ThrowStatement: (estree) =>
ThrowNode({
startToken: startToken(estree),
endToken: endToken(estree),
argument: fromEstree(estree.argument)
}),
WhileStatement: (estree) =>
WhileNode({
startToken: startToken(estree),
endToken: endToken(estree),
test: fromEstree(estree.test),
body: fromEstree(estree.body)
}),
DoWhileStatement: (estree) =>
DoNode({
startToken: startToken(estree),
endToken: endToken(estree),
test: fromEstree(estree.test),
body: fromEstree(estree.body)
}),
ForStatement: (estree) =>
ForNode({
startToken: startToken(estree),
endToken: endToken(estree),
init: fromEstree(estree.init),
test: fromEstree(estree.test),
update: fromEstree(estree.update),
body: fromEstree(estree.body)
}),
ForInStatement: (estree) =>
ForInNode({
startToken: startToken(estree),
endToken: endToken(estree),
left: fromEstree(estree.left),
right: fromEstree(estree.right),
body: fromEstree(estree.body)
}),
ForOfStatement: (estree) =>
ForOfNode({
startToken: startToken(estree),
endToken: endToken(estree),
left: fromEstree(estree.left),
right: fromEstree(estree.right),
body: fromEstree(estree.body),
await: estree.await
}),
AwaitExpression: (estree) =>
AwaitNode({
startToken: startToken(estree),
endToken: endToken(estree),
argument: fromEstree(estree.argument)
}),
YieldExpression: (estree) =>
YieldNode({
startToken: startToken(estree),
endToken: endToken(estree),
argument: fromEstree(estree.argument),
delegate: estree.delegate
}),
DebuggerStatement: (estree) =>
DebuggerNode({
startToken: startToken(estree),
endToken: endToken(estree)
}),
CatchClause: (estree) =>
CatchNode({
startToken: startToken(estree),
endToken: endToken(estree),
param: estree.param
? patternFromEstree(estree.param, SymbolCatchNode)
: null,
body: fromEstree(estree.body)
}),
ThisExpression: (estree) =>
ThisNode({
startToken: startToken(estree),
name: "this",
endToken: endToken(estree)
}),
Super: (estree) =>
SuperNode({
startToken: startToken(estree),
endToken: endToken(estree),
name: "super"
}),
BinaryExpression: (estree) => {
if (estree.left.type === "PrivateIdentifier") {
return PrivateInNode({
startToken: startToken(estree),
endToken: endToken(estree),
left: SymbolPrivatePropertyNode({
startToken: startToken(estree.left),
endToken: endToken(estree.left),
name: estree.left.name
}),
right: fromEstree(estree.right)
});
}
return makeNode(BinaryNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
operator: estree.operator,
left: fromEstree(estree.left),
right: fromEstree(estree.right)
});
},
LogicalExpression: (estree) =>
makeNode(BinaryNode, undefined, {
startToken: startToken(estree),
endToken: endToken(estree),
operator: estree.operator,
left: fromEstree(estree.left),
right: fromEstree(estree.right)
}),
AssignmentExpression: (estree) =>
AssignNode({
startToken: startToken(estree),
endToken: endToken(estree),
operator: estree.operator,
logical:
estree.operator === "??=" ||
estree.operator === "&&=" ||
estree.operator === "||=",
left: fromEstree(estree.left),
right: fromEstree(estree.right)
}),
ConditionalExpression: (estree) =>
ConditionalNode({
startToken: startToken(estree),
endToken: endToken(estree),
test: fromEstree(estree.test),
consequent: fromEstree(estree.consequent),
alternate: fromEstree(estree.alternate)
}),
NewExpression: (estree) =>
NewNode({
startToken: startToken(estree),
endToken: endToken(estree),
callee: fromEstree(estree.callee),
arguments: estree.arguments.map(fromEstree)
}),
CallExpression: (estree) =>
CallExpressionNode({
startToken: startToken(estree),
endToken: endToken(estree),
callee: fromEstree(estree.callee),
optional: estree.optional,
arguments: estree.arguments.map(fromEstree)
}),
UpdateExpression: unaryFromEstree,
UnaryExpression: unaryFromEstree,
ClassDeclaration: classFromEstree,
ClassExpression: classFromEstree
};
/**
* @param {NodeLike} estree an ESTree tree
* @param {((pos: number, ending: boolean) => number | undefined)=} ownTokenAt the parser's token at an offset, where it kept them
* @param {string=} source what those tokens were read from
* @param {TokenTable=} ownTable the table the parser's token numbers are read through
* @returns {Node} the minifier's tree, as terser's conversion builds it
*/
ast.adoptEstree = (estree, ownTokenAt, source, ownTable) => {
const savedLabels = fromLabels;
const savedTokens = tokenAt;
const savedSource = tokenSource;
const savedTable = tokenTable;
fromLabels = [];
tokenAt = ownTokenAt === undefined ? null : ownTokenAt;
tokenSource = source === undefined ? null : source;
tokenTable = ownTable === undefined ? null : ownTable;
try {
return fromEstree(/** @type {AnyNode} */ (estree));
} finally {
fromLabels = savedLabels;
tokenAt = savedTokens;
tokenSource = savedSource;
tokenTable = savedTable;
}
};
// What the tree being converted reads its token numbers through.
/** @type {TokenTables} */
let estreeTables = NO_TOKEN_TABLES;
/**
* @param {Node} node terser's node
* @param {EstreeOut} estree the ESTree node it converts to
* @returns {EstreeOut} the ESTree node, with `range` and `loc` from the node's tokens
*/
const setEstreeLocation = (node, estree) => {
const start = estreeTables.of(node.startToken);
const end = estreeTables.of(node.endToken);
if (!(start && end)) return estree;
if (
start.pos !== null &&
start.pos !== undefined &&
end.endpos !== null &&
end.endpos !== undefined
) {
estree.range = [start.pos, end.endpos];
}
if (start.line) {
estree.loc = {
start: { line: start.line, column: start.col },
end: end.endline ? { line: end.endline, column: end.endcol } : null
};
if (start.file) estree.loc.source = start.file;
}
return estree;
};
/** @type {Record<string, (self: Node, parent?: Node) => EstreeOut>} */
const toEstreeHandlers = {};
/**
* terser's `def_to_moz`: the conversion of the nodes named, terser's class.
* @template {Node} N
* @param {string} name the nodes it converts, by terser's class
* @param {(node: N, parent: Node | undefined) => EstreeOut} handler its conversion, of the nodes of that class
* @returns {void}
*/
const defineToEstree = (name, handler) => {
toEstreeHandlers[name] = (node, parent) =>
setEstreeLocation(node, handler(/** @type {N} */ (node), parent));
};
// The nodes being converted, outermost first; null between conversions.
/** @type {(Node | null | undefined)[] | null} */
let toStack = null;
/**
* @param {Node | null | undefined} node terser's node, or null
* @returns {EstreeOut | null} the ESTree node, or null
*/
const toEstree = (node) => {
if (toStack === null) toStack = [];
toStack.push(node);
const estree =
node !== null && node !== undefined
? ast.toEstree(
node,
/** @type {Node | undefined} */ (toStack[toStack.length - 2])
)
: null;
toStack.pop();
if (toStack.length === 0) toStack = null;
return estree;
};
/**
* @returns {boolean} whether a destructuring pattern holds the node being converted
*/
const inDestructuring = () => {
const stack = /** @type {(Node | null | undefined)[]} */ (toStack);
let i = stack.length;
while (i--) {
if (isDestructuringNode(stack[i])) return true;
}
return false;
};
/**
* A property key: a number literal where it reads as one, a string where
* quoted, else a name — shorthand where the value is that same name.
* @param {Node} key the key
* @param {boolean=} computed whether it is computed
* @param {(string | false)=} quote the quote it had
* @param {NodeFields["value"]=} value the property's value
* @returns {[boolean, EstreeOut | null]} whether it is shorthand, and the key
*/
const propertyKeyToEstree = (
key,
computed = false,
quote = false,
value = null
) => {
if (computed) return [false, toEstree(key)];
const keyName = key.name;
let estreeKey;
if (quote) {
estreeKey = {
type: "Literal",
value: keyName,
raw: JSON.stringify(keyName)
};
} else if (`${Number(keyName)}` === keyName && Number(keyName) >= 0) {
estreeKey = {
type: "Literal",
value: Number(keyName),
raw: JSON.stringify(Number(keyName))
};
} else {
estreeKey = { type: "Identifier", name: keyName };
}
const shorthand =
estreeKey.type === "Identifier" &&
estreeKey.name === keyName &&
((isSymbolNode(value) && /** @type {Node} */ (value).name === keyName) ||
(isDefaultAssignNode(value) &&
/** @type {Node} */ (value).left.name === keyName));
return [shorthand, estreeKey];
};
/**
* @param {Node} node a binding
* @returns {EstreeOut | null} its ESTree pattern
*/
const patternToEstree = (node) => {
if (isExpansionNode(node)) {
return {
type: "RestElement",
argument: patternToEstree(node.argument)
};
}
if (
isSymbolNode(node) ||
isDestructuringNode(node) ||
isDefaultAssignNode(node) ||
isPropAccessNode(node)
) {
return toEstree(node);
}
throw new Error(ast.kindOf(node));
};
/**
* @param {Node} node a node with a statement list
* @returns {EstreeOut} the ESTree block of its statements
*/
const blockToEstree = (node) => ({
type: "BlockStatement",
body: /** @type {Node[]} */ (node.body).map(toEstree)
});
/**
* A leading string statement gets an empty statement before it, so ESTree
* doesn't read it as a directive.
* @param {string} type the ESTree type
* @param {Node} node a scope
* @returns {EstreeOut} the ESTree node of its body
*/
const scopeToEstree = (type, node) => {
/** @type {Node[]} */
const statements = /** @type {Node[]} */ (modules.ast.bodyOf(node));
const body = statements.map(toEstree);
if (
isSimpleStatementNode(statements[0]) &&
isStringNode(statements[0].expression)
) {
body.unshift(toEstree(EmptyStatementNode(statements[0])));
}
return { type, body };
};
/**
* @param {Node | null | undefined} attributes an import's attributes object, or null
* @returns {EstreeOut[]} them as ESTree import attributes
*/
const importAttributesToEstree = (attributes) => {
/** @type {EstreeOut[]} */
const result = [];
if (attributes) {
for (const { key: name, value } of attributes.properties) {
const key = name.name;
result.push({
type: "ImportAttribute",
key: isBasicIdentifier(key)
? { type: "Identifier", name: key }
: { type: "Literal", value: key, raw: JSON.stringify(key) },
value: toEstree(/** @type {Node} */ (value))
});
}
}
return result;
};
defineToEstree("EmptyStatement", () => ({ type: "EmptyStatement" }));
defineToEstree("BlockStatement", (node) => blockToEstree(node));
defineToEstree("If", (/** @type {SingleBodyNode} */ node) => ({
type: "IfStatement",
test: toEstree(node.test),
consequent: toEstree(node.consequent),
alternate: toEstree(node.alternate)
}));
defineToEstree("LabeledStatement", (/** @type {SingleBodyNode} */ node) => ({
type: "LabeledStatement",
label: toEstree(node.label),
body: toEstree(node.body)
}));
defineToEstree("Break", (node) => ({
type: "BreakStatement",
label: toEstree(node.label)
}));
defineToEstree("Continue", (node) => ({
type: "ContinueStatement",
label: toEstree(node.label)
}));
defineToEstree("With", (/** @type {SingleBodyNode} */ node) => ({
type: "WithStatement",
object: toEstree(node.object),
body: toEstree(node.body)
}));
defineToEstree("Switch", (node) => ({
type: "SwitchStatement",
discriminant: toEstree(node.discriminant),
cases: node.cases.map(toEstree)
}));
defineToEstree("Return", (node) => ({
type: "ReturnStatement",
argument: toEstree(node.argument)
}));
defineToEstree("Throw", (node) => ({
type: "ThrowStatement",
argument: toEstree(node.argument)
}));
defineToEstree("While", (/** @type {SingleBodyNode} */ node) => ({
type: "WhileStatement",
test: toEstree(node.test),
body: toEstree(node.body)
}));
defineToEstree("Do", (/** @type {SingleBodyNode} */ node) => ({
type: "DoWhileStatement",
test: toEstree(node.test),
body: toEstree(node.body)
}));
defineToEstree("For", (/** @type {SingleBodyNode} */ node) => ({
type: "ForStatement",
init: toEstree(node.init),
test: toEstree(node.test),
update: toEstree(node.update),
body: toEstree(node.body)
}));
defineToEstree("ForIn", (/** @type {SingleBodyNode} */ node) => ({
type: "ForInStatement",
left: toEstree(node.left),
right: toEstree(node.right),
body: toEstree(node.body)
}));
defineToEstree("ForOf", (/** @type {SingleBodyNode} */ node) => ({
type: "ForOfStatement",
left: toEstree(node.left),
right: toEstree(node.right),
body: toEstree(node.body),
await: node.await
}));
defineToEstree("Await", (node) => ({
type: "AwaitExpression",
argument: toEstree(node.argument)
}));
defineToEstree("Yield", (node) => ({
type: "YieldExpression",
argument: toEstree(node.argument),
delegate: node.delegate
}));
defineToEstree("Debugger", () => ({ type: "DebuggerStatement" }));
defineToEstree("VarDefLike", (node) => ({
type: "VariableDeclarator",
id: toEstree(node.id),
init: toEstree(node.init)
}));
defineToEstree("This", () => ({ type: "ThisExpression" }));
defineToEstree("Super", () => ({ type: "Super" }));
defineToEstree("Conditional", (/** @type {SingleBodyNode} */ node) => ({
type: "ConditionalExpression",
test: toEstree(node.test),
consequent: toEstree(node.consequent),
alternate: toEstree(node.alternate)
}));
defineToEstree("New", (node) => ({
type: "NewExpression",
callee: toEstree(node.callee),
arguments: node.arguments.map(toEstree)
}));
defineToEstree("Call", (node) => ({
type: "CallExpression",
callee: toEstree(node.callee),
optional: node.optional,
arguments: node.arguments.map(toEstree)
}));
defineToEstree("DynamicImport", (node) => {
const [source, options] = node.args.map(toEstree);
return {
type: "ImportExpression",
source,
options: options || null,
phase: node.phase
};
});
defineToEstree("Toplevel", (node) => scopeToEstree("Program", node));
defineToEstree("Expansion", (node) => ({
type: inDestructuring() ? "RestElement" : "SpreadElement",
argument: toEstree(node.argument)
}));
defineToEstree("PrefixedTemplateString", (node) => ({
type: "TaggedTemplateExpression",
tag: toEstree(node.tag),
quasi: templateToEstree(node.quasi, true)
}));
defineToEstree("TemplateString", (node) => templateToEstree(node, false));
/**
* A template as ESTree; an untagged one's raw text is written anew from its
* value while correcting, since the compressor changes only the value.
* @param {Node} node terser's template
* @param {boolean} tagged whether a tag reads its raw text
* @returns {EstreeOut} the template literal
*/
const templateToEstree = (node, tagged) => {
/** @type {EstreeTemplateElement[]} */
const quasis = [];
/** @type {(EstreeOut | null)[]} */
const expressions = [];
// Inlining a template into another leaves two text segments in a row,
// which ESTree holds as one.
let afterText = false;
for (const segment of node.segments) {
if (segment.type !== "TemplateElement") {
expressions.push(toEstree(segment));
afterText = false;
} else if (afterText) {
const { value } = quasis[quasis.length - 1];
value.raw += segment.raw;
value.cooked =
value.cooked === null || segment.value === null
? null
: value.cooked + segment.value;
} else {
quasis.push({
type: "TemplateElement",
value: {
raw: /** @type {string} */ (segment.raw),
cooked: /** @type {string | null} */ (segment.value)
},
tail: false
});
afterText = true;
}
}
quasis[quasis.length - 1].tail = true;
if (!tagged && correcting()) {
for (const { value } of quasis) {
value.raw = /** @type {string} */ (value.cooked).replace(
/\\|`|\$\{|\r/g,
(/** @type {string} */ match) =>
match === "\r" ? "\\r" : `\\${match}`
);
}
}
return { type: "TemplateLiteral", quasis, expressions };
};
defineToEstree("Defun", (node) => ({
type: "FunctionDeclaration",
id: toEstree(node.id),
params: node.params.map(patternToEstree),
generator: node.generator,
async: node.async,
body: scopeToEstree("BlockStatement", node)
}));
defineToEstree("Function", (node) => ({
type: "FunctionExpression",
id: toEstree(node.id),
params: node.params.map(patternToEstree),
generator: node.generator || false,
async: node.async || false,
body: scopeToEstree("BlockStatement", node)
}));
defineToEstree(
"Arrow",
(/** @type {Node & { body: Node & { body: Node[] } }} */ node) => {
const body =
node.body.body.length === 1 &&
isReturnNode(node.body.body[0]) &&
node.body.body[0].argument
? toEstree(node.body.body[0].argument)
: blockToEstree(node.body);
return {
type: "ArrowFunctionExpression",
params: node.params.map(patternToEstree),
async: node.async,
body
};
}
);
defineToEstree("Destructuring", (node) => {
if (node.is_array) {
return {
type: "ArrayPattern",
elements: node.elements.map((name) =>
name === null ? null : patternToEstree(name)
)
};
}
return {
type: "ObjectPattern",
properties: node.properties.map((name) => {
if (!isObjectKeyValNode(name)) return patternToEstree(name);
const computed = name.computed;
const [shorthand, key] = propertyKeyToEstree(
name.key,
computed,
name.quote,
name.value
);
return {
type: "Property",
computed,
kind: "init",
key,
method: false,
shorthand,
value: patternToEstree(/** @type {Node} */ (name.value))
};
})
};
});
defineToEstree("DefaultAssign", (node) => ({
type: "AssignmentPattern",
left: patternToEstree(node.left),
right: toEstree(node.right)
}));
defineToEstree("Directive", (node) => ({
type: "ExpressionStatement",
expression: {
type: "Literal",
value: node.directive,
raw: modules.printToString(node)
},
directive: node.directive
}));
defineToEstree("SimpleStatement", (node) => ({
type: "ExpressionStatement",
expression: toEstree(node.expression)
}));
defineToEstree("SwitchBranch", (node) => ({
type: "SwitchCase",
test: toEstree(node.test),
consequent: /** @type {Node[]} */ (node.consequent).map(toEstree)
}));
defineToEstree("Try", (node) => ({
type: "TryStatement",
block: blockToEstree(node.block),
handler: toEstree(node.handler),
guardedHandlers: [],
finalizer: toEstree(node.finalizer)
}));
defineToEstree("Catch", (/** @type {SingleBodyNode} */ node) => ({
type: "CatchClause",
param:
node.param !== null && node.param !== undefined
? patternToEstree(node.param)
: null,
body: blockToEstree(node.body)
}));
defineToEstree("DefinitionsLike", (node) => ({
type: "VariableDeclaration",
kind: isConstNode(node)
? "const"
: isLetNode(node)
? "let"
: isUsingNode(node)
? node.await
? "await using"
: "using"
: "var",
declarations: node.declarations.map(toEstree)
}));
defineToEstree("Export", (node) => {
if (node.exported_names) {
const firstExported = node.exported_names[0];
if (
firstExported &&
firstExported.local.name === "*" &&
!firstExported.local.quote
) {
const foreignName = firstExported.exported;
return {
type: "ExportAllDeclaration",
source: toEstree(node.source),
exported:
foreignName.name === "*" && !foreignName.quote
? null
: toEstree(foreignName),
attributes: importAttributesToEstree(node.attributes)
};
}
return {
type: "ExportNamedDeclaration",
specifiers: node.exported_names.map((mapping) => ({
type: "ExportSpecifier",
exported: toEstree(mapping.exported),
local: toEstree(mapping.local)
})),
declaration: toEstree(node.exported_definition),
source: toEstree(node.source),
attributes: importAttributesToEstree(node.attributes)
};
}
if (node.is_default) {
return {
type: "ExportDefaultDeclaration",
declaration: toEstree(node.exported_value || node.exported_definition)
};
}
return {
type: "ExportNamedDeclaration",
declaration: toEstree(node.exported_value || node.exported_definition),
specifiers: [],
source: null
};
});
defineToEstree("Import", (node) => {
/** @type {EstreeOut[]} */
const specifiers = [];
if (node.imported_name) {
specifiers.push({
type: "ImportDefaultSpecifier",
local: toEstree(node.imported_name)
});
}
if (node.imported_names) {
const firstForeignName =
node.imported_names[0] && node.imported_names[0].imported;
if (
firstForeignName &&
firstForeignName.name === "*" &&
!firstForeignName.quote
) {
specifiers.push({
type: "ImportNamespaceSpecifier",
local: toEstree(node.imported_names[0].local)
});
} else {
for (const mapping of node.imported_names) {
specifiers.push({
type: "ImportSpecifier",
local: toEstree(mapping.local),
imported: toEstree(mapping.imported)
});
}
}
}
/** @type {EstreeOut} */
const estree = {
type: "ImportDeclaration",
specifiers,
source: toEstree(node.source),
attributes: importAttributesToEstree(node.attributes)
};
if (node.phase) estree.phase = node.phase;
return estree;
});
defineToEstree("ImportMeta", () => ({
type: "MetaProperty",
meta: { type: "Identifier", name: "import" },
property: { type: "Identifier", name: "meta" }
}));
defineToEstree("Sequence", (node) => ({
type: "SequenceExpression",
expressions: node.expressions.map(toEstree)
}));
defineToEstree("DotHash", (node) => ({
type: "MemberExpression",
object: toEstree(node.object),
computed: false,
property: { type: "PrivateIdentifier", name: node.property.name },
optional: node.optional
}));
defineToEstree("PropAccess", (node) => {
const computed = node.computed;
return {
type: "MemberExpression",
object: toEstree(node.object),
computed,
property: computed
? toEstree(node.property)
: { type: "Identifier", name: node.property.name },
optional: node.optional
};
});
defineToEstree("Chain", (node) => ({
type: "ChainExpression",
expression: toEstree(node.expression)
}));
defineToEstree("Unary", (node) => ({
type:
node.operator === "++" || node.operator === "--"
? "UpdateExpression"
: "UnaryExpression",
operator: node.operator,
prefix: isUnaryPrefixNode(node),
argument: toEstree(node.argument)
}));
defineToEstree("Binary", (node) => {
if (node.operator === "=" && inDestructuring()) {
return {
type: "AssignmentPattern",
left: toEstree(node.left),
right: toEstree(node.right)
};
}
return {
type:
node.operator === "&&" ||
node.operator === "||" ||
node.operator === "??"
? "LogicalExpression"
: "BinaryExpression",
left: toEstree(node.left),
operator: node.operator,
right: toEstree(node.right)
};
});
defineToEstree("Assign", (node) => ({
type: "AssignmentExpression",
operator: node.operator,
left: toEstree(node.left),
right: toEstree(node.right)
}));
defineToEstree("PrivateIn", (node) => ({
type: "BinaryExpression",
left: { type: "PrivateIdentifier", name: node.left.name },
operator: "in",
right: toEstree(node.right)
}));
defineToEstree("Array", (node) => ({
type: "ArrayExpression",
elements: node.elements.map(toEstree)
}));
defineToEstree("Object", (node) => ({
type: "ObjectExpression",
properties: node.properties.map(toEstree)
}));
defineToEstree("ObjectProperty", (/** @type {MemberNode} */ node, parent) => {
const computed = node.computed;
const [shorthand, key] = propertyKeyToEstree(
node.key,
computed,
node.quote,
node.value
);
let kind;
if (isObjectGetterNode(node)) {
kind = "get";
} else if (isObjectSetterNode(node)) {
kind = "set";
}
if (isPrivateGetterNode(node) || isPrivateSetterNode(node)) {
return {
type: "MethodDefinition",
computed: false,
kind: isPrivateGetterNode(node) ? "get" : "set",
static: node.static,
key: { type: "PrivateIdentifier", name: node.key.name },
value: toEstree(node.value)
};
}
if (isClassPrivatePropertyNode(node)) {
return {
type: "PropertyDefinition",
key: { type: "PrivateIdentifier", name: node.key.name },
value: toEstree(node.value),
computed: false,
static: node.static
};
}
if (isClassPropertyNode(node)) {
return {
type: "PropertyDefinition",
key,
value: toEstree(node.value),
computed,
static: node.static
};
}
if (isClassNode(parent)) {
return {
type: "MethodDefinition",
computed,
kind,
static: node.static,
key: toEstree(node.key),
value: toEstree(node.value)
};
}
return {
type: "Property",
computed,
method: false,
shorthand,
kind,
key,
value: toEstree(node.value)
};
});
defineToEstree("ObjectKeyVal", (/** @type {MemberNode} */ node) => {
const computed = node.computed;
const [shorthand, key] = propertyKeyToEstree(
node.key,
computed,
node.quote,
node.value
);
return {
type: "Property",
computed,
shorthand,
method: false,
kind: "init",
key,
value: toEstree(node.value)
};
});
defineToEstree("ConciseMethod", (/** @type {MemberNode} */ node, parent) => {
const computed = node.computed;
const [, key] = propertyKeyToEstree(
node.key,
computed,
node.quote,
node.value
);
if (isObjectNode(parent)) {
return {
type: "Property",
kind: "init",
computed,
method: true,
shorthand: false,
key,
value: toEstree(node.value)
};
}
return {
type: "MethodDefinition",
kind:
!computed && node.key.name === "constructor" ? "constructor" : "method",
computed,
key,
value: toEstree(node.value),
static: node.static
};
});
defineToEstree("PrivateMethod", (/** @type {MemberNode} */ node) => ({
type: "MethodDefinition",
kind: "method",
key: { type: "PrivateIdentifier", name: node.key.name },
value: toEstree(node.value),
computed: false,
static: node.static
}));
defineToEstree("Class", (node) => ({
type: isClassExpressionNode(node) ? "ClassExpression" : "ClassDeclaration",
superClass: toEstree(node.superClass),
id: node.id ? toEstree(node.id) : null,
body: {
type: "ClassBody",
body: /** @type {Node[]} */ (/** @type {Node} */ (node.body).body).map(
toEstree
)
}
}));
defineToEstree("ClassStaticBlock", (node) => ({
type: "StaticBlock",
body: /** @type {Node[]} */ (node.body).map(toEstree)
}));
defineToEstree("NewTarget", () => ({
type: "MetaProperty",
meta: { type: "Identifier", name: "new" },
property: { type: "Identifier", name: "target" }
}));
defineToEstree("Symbol", (node, parent) => {
if (
(isSymbolMethodNode(node) && /** @type {Node} */ (parent).quote) ||
((isSymbolImportForeignNode(node) ||
isSymbolExportForeignNode(node) ||
isSymbolExportNode(node)) &&
node.quote)
) {
return { type: "Literal", value: node.name };
}
const definition = node.definition;
return {
type: "Identifier",
name: definition ? definition.mangled_name || definition.name : node.name
};
});
defineToEstree("RegExp", (node) => ({
type: "Literal",
value: null,
raw: modules.printToString(node),
regex: {
pattern: /** @type {RegExpValue} */ (node.value).source,
flags: /** @type {RegExpValue} */ (node.value).flags
}
}));
defineToEstree("Constant", (node) => ({
type: "Literal",
value: /** @type {string | number | boolean | null} */ (node.value),
raw: node.raw || modules.printToString(node)
}));
defineToEstree("Atom", (node) => ({
type: "Identifier",
name: String(node.value)
}));
// ESTree holds a bigint's decimal digits; terser may hold hexadecimal ones.
defineToEstree("BigInt", (node) => ({
type: "Literal",
value: null,
bigint:
typeof BigInt === "function"
? BigInt(/** @type {string} */ (node.value)).toString()
: /** @type {string} */ (node.value),
raw: node.raw
}));
toEstreeHandlers.Boolean = toEstreeHandlers.Constant;
toEstreeHandlers.Null = toEstreeHandlers.Constant;
toEstreeHandlers.Block = toEstreeHandlers.BlockStatement;
toEstreeHandlers.Lambda = toEstreeHandlers.Function;
// terser's `to_mozilla_ast`, by ESTree type.
/** @type {Record<string, (self: Node, parent?: Node) => EstreeOut>} */
const TO_ESTREE = {
ArrayExpression: toEstreeHandlers.Array,
ArrayPattern: toEstreeHandlers.Destructuring,
ArrowFunctionExpression: toEstreeHandlers.Arrow,
AssignmentExpression: toEstreeHandlers.Assign,
AssignmentPattern: toEstreeHandlers.DefaultAssign,
AwaitExpression: toEstreeHandlers.Await,
BinaryExpression: (self, parent) =>
ast.isBinaryNode(self)
? toEstreeHandlers.Binary(self, parent)
: toEstreeHandlers.PrivateIn(self, parent),
BlockStatement: (self, parent) =>
ast.isBlockStatementNode(self)
? toEstreeHandlers.BlockStatement(self, parent)
: toEstreeHandlers.Block(self, parent),
BreakStatement: toEstreeHandlers.Break,
CallExpression: toEstreeHandlers.Call,
CatchClause: toEstreeHandlers.Catch,
ChainExpression: toEstreeHandlers.Chain,
ClassDeclaration: toEstreeHandlers.Class,
ClassExpression: toEstreeHandlers.Class,
ConditionalExpression: toEstreeHandlers.Conditional,
ContinueStatement: toEstreeHandlers.Continue,
DebuggerStatement: toEstreeHandlers.Debugger,
DoWhileStatement: toEstreeHandlers.Do,
EmptyStatement: toEstreeHandlers.EmptyStatement,
ExportAllDeclaration: toEstreeHandlers.Export,
ExportDefaultDeclaration: toEstreeHandlers.Export,
ExportNamedDeclaration: toEstreeHandlers.Export,
ExpressionStatement: (self, parent) =>
ast.isDirectiveNode(self)
? toEstreeHandlers.Directive(self, parent)
: toEstreeHandlers.SimpleStatement(self, parent),
ForInStatement: toEstreeHandlers.ForIn,
ForOfStatement: toEstreeHandlers.ForOf,
ForStatement: toEstreeHandlers.For,
FunctionDeclaration: toEstreeHandlers.Defun,
FunctionExpression: (self, parent) =>
ast.isFunctionNode(self)
? toEstreeHandlers.Function(self, parent)
: toEstreeHandlers.Lambda(self, parent),
Identifier: (self, parent) =>
ast.isAtomNode(self)
? toEstreeHandlers.Atom(self, parent)
: toEstreeHandlers.Symbol(self, parent),
IfStatement: toEstreeHandlers.If,
ImportDeclaration: toEstreeHandlers.Import,
ImportExpression: toEstreeHandlers.DynamicImport,
LabeledStatement: toEstreeHandlers.LabeledStatement,
Literal: (self, parent) =>
ast.isNullNode(self)
? toEstreeHandlers.Null(self, parent)
: ast.isBooleanNode(self)
? toEstreeHandlers.Boolean(self, parent)
: ast.isBigIntNode(self)
? toEstreeHandlers.BigInt(self, parent)
: ast.isRegExpNode(self)
? toEstreeHandlers.RegExp(self, parent)
: ast.isConstantNode(self)
? toEstreeHandlers.Constant(self, parent)
: toEstreeHandlers.Symbol(self, parent),
LogicalExpression: toEstreeHandlers.Binary,
MemberExpression: (self, parent) =>
ast.isDotHashNode(self)
? toEstreeHandlers.DotHash(self, parent)
: toEstreeHandlers.PropAccess(self, parent),
MetaProperty: (self, parent) =>
ast.isImportMetaNode(self)
? toEstreeHandlers.ImportMeta(self, parent)
: toEstreeHandlers.NewTarget(self, parent),
MethodDefinition: (self, parent) =>
ast.isPrivateMethodNode(self)
? toEstreeHandlers.PrivateMethod(self, parent)
: ast.isConciseMethodNode(self)
? toEstreeHandlers.ConciseMethod(self, parent)
: toEstreeHandlers.ObjectProperty(self, parent),
NewExpression: toEstreeHandlers.New,
ObjectExpression: toEstreeHandlers.Object,
ObjectPattern: toEstreeHandlers.Destructuring,
PrivateIdentifier: toEstreeHandlers.Symbol,
Program: toEstreeHandlers.Toplevel,
Property: (self, parent) =>
ast.isObjectKeyValNode(self)
? toEstreeHandlers.ObjectKeyVal(self, parent)
: ast.isConciseMethodNode(self)
? toEstreeHandlers.ConciseMethod(self, parent)
: toEstreeHandlers.ObjectProperty(self, parent),
PropertyDefinition: toEstreeHandlers.ObjectProperty,
RestElement: toEstreeHandlers.Expansion,
ReturnStatement: toEstreeHandlers.Return,
SequenceExpression: toEstreeHandlers.Sequence,
SpreadElement: toEstreeHandlers.Expansion,
StaticBlock: toEstreeHandlers.ClassStaticBlock,
Super: toEstreeHandlers.Super,
SwitchCase: toEstreeHandlers.SwitchBranch,
SwitchStatement: toEstreeHandlers.Switch,
TaggedTemplateExpression: toEstreeHandlers.PrefixedTemplateString,
TemplateLiteral: toEstreeHandlers.TemplateString,
ThisExpression: toEstreeHandlers.This,
ThrowStatement: toEstreeHandlers.Throw,
TryStatement: toEstreeHandlers.Try,
UnaryExpression: toEstreeHandlers.Unary,
UpdateExpression: toEstreeHandlers.Unary,
VariableDeclaration: toEstreeHandlers.DefinitionsLike,
VariableDeclarator: toEstreeHandlers.VarDefLike,
WhileStatement: toEstreeHandlers.While,
WithStatement: toEstreeHandlers.With,
YieldExpression: toEstreeHandlers.Yield
};
/**
* terser's `to_mozilla_ast`: a node as terser converts it to ESTree.
* @param {Node} node a node
* @param {Node=} parent the node holding it
* @returns {EstreeOut} the ESTree node
*/
ast.toEstree = (node, parent) => {
const convert = TO_ESTREE[node.type];
// terser converts a node with no conversion of its own by a method it lacks.
if (convert === undefined) {
throw new TypeError("node.to_mozilla_ast is not a function");
}
if (node.type !== "Program") {
return convert(node, parent);
}
const saved = estreeTables;
estreeTables = node.tokenTables || NO_TOKEN_TABLES;
try {
return convert(node, parent);
} finally {
estreeTables = saved;
}
};
};
/**
* What every node the printer reads holds: the tokens it was read between,
* none on a node the compressor made.
* @typedef {object} PrintBase
* @property {NodeToken | null | undefined} startToken the token it starts at
* @property {NodeToken | null | undefined} endToken the token it ends at
*/
/** @typedef {PrintBase & { type: "Program", body: PrintNode[], tokenTables?: TokenTables | null }} PrintProgram the program, with the tables its token numbers are read through */
/** @typedef {PrintBase & { type: "ExpressionStatement", expression: PrintNode, directive: string | undefined }} PrintExpressionStatement a statement of one expression, a directive where `directive` is set */
/** @typedef {PrintBase & { type: "BlockStatement", body: PrintNode[] }} PrintBlockStatement a block */
/** @typedef {PrintBase & { type: "EmptyStatement" | "DebuggerStatement" | "ThisExpression" | "Super" }} PrintLeaf a node holding nothing but its type */
/** @typedef {PrintBase & { type: "LabeledStatement", label: PrintNode, body: PrintNode }} PrintLabeledStatement a labeled statement */
/** @typedef {PrintBase & { type: "DoWhileStatement" | "WhileStatement", test: PrintNode, body: PrintNode }} PrintWhileStatement a `while` or `do`-`while` loop */
/** @typedef {PrintBase & { type: "ForStatement", init: PrintNode | null, test: PrintNode | null, update: PrintNode | null, body: PrintNode }} PrintForStatement a `for` loop */
/** @typedef {PrintBase & { type: "ForInStatement", left: PrintNode, right: PrintNode, body: PrintNode }} PrintForInStatement a `for`-`in` loop */
/** @typedef {PrintBase & { type: "ForOfStatement", left: PrintNode, right: PrintNode, body: PrintNode, await: boolean }} PrintForOfStatement a `for`-`of` loop */
/** @typedef {PrintBase & { type: "WithStatement", object: PrintNode, body: PrintNode }} PrintWithStatement a `with` statement */
/** @typedef {PrintBase & { type: "ReturnStatement" | "ThrowStatement", argument: PrintNode | null }} PrintExitStatement a `return` or `throw` */
/** @typedef {PrintBase & { type: "BreakStatement" | "ContinueStatement", label: PrintNode | null }} PrintLoopControl a `break` or `continue` */
/** @typedef {PrintBase & { type: "IfStatement", test: PrintNode, consequent: PrintNode, alternate: PrintNode | null }} PrintIfStatement an `if` */
/** @typedef {PrintBase & { type: "SwitchStatement", discriminant: PrintNode, cases: PrintSwitchCase[] }} PrintSwitchStatement a `switch` */
/** @typedef {PrintBase & { type: "SwitchCase", test: PrintNode | null, consequent: PrintNode[] }} PrintSwitchCase a `case`, or the `default` where `test` is null */
/** @typedef {PrintBase & { type: "TryStatement", block: PrintNode, handler: PrintNode | null, finalizer: PrintNode | null }} PrintTryStatement a `try` */
/** @typedef {PrintBase & { type: "CatchClause", param: PrintNode | null, body: PrintBlockStatement }} PrintCatchClause a `catch` */
/** @typedef {PrintBase & { type: "VariableDeclaration", declarations: PrintNode[], kind: string, await?: boolean }} PrintVariableDeclaration a `var`, `let`, `const` or `using` declaration, `await` for an `await using` */
/** @typedef {PrintBase & { type: "VariableDeclarator", id: PrintNode, init: PrintNode | null }} PrintVariableDeclarator one name a declaration declares */
/** @typedef {PrintBase & { type: "FunctionDeclaration" | "FunctionExpression" | "ArrowFunctionExpression", id: PrintIdentifier | null, params: PrintNode[], body: PrintBlockStatement, generator: boolean, async: boolean }} PrintFunction a function, its body always a block, its name's `role` saying how the name is declared */
/** @typedef {PrintBase & { type: "ClassDeclaration" | "ClassExpression", id: PrintIdentifier | null, superClass: PrintNode | null, body: PrintClassBody }} PrintClass a class, its name's `role` as a function's */
/** @typedef {PrintBase & { type: "ClassBody" | "StaticBlock", body: PrintNode[] }} PrintClassBody a class's body, or a static block */
/** @typedef {string | null | undefined} PrintMapName the name a member's source mapping carries where renaming set it, else undefined */
/** @typedef {PrintBase & { type: "PropertyDefinition", key: PrintNode, value: PrintNode | null, computed: boolean, static: boolean, quote: string | undefined, mapName: PrintMapName }} PrintPropertyDefinition a class field */
/** @typedef {PrintBase & { type: "Property", key: PrintNode, value: PrintNode, kind: string, method: boolean, shorthand: boolean, computed: boolean, quote: string | undefined, mapName: PrintMapName }} PrintProperty an object literal's or pattern's property */
/** @typedef {PrintBase & { type: "MethodDefinition", key: PrintNode, value: PrintNode, kind: string, computed: boolean, static: boolean, quote: string | undefined }} PrintMethodDefinition a class's method, getter or setter */
/** @typedef {PrintBase & { type: "ObjectExpression" | "ObjectPattern", properties: PrintNode[] }} PrintObject an object literal or pattern */
/** @typedef {PrintBase & { type: "ArrayExpression" | "ArrayPattern", elements: (PrintNode | null)[] }} PrintArray an array literal or pattern, null for a hole */
/** @typedef {PrintBase & { type: "SpreadElement" | "RestElement", argument: PrintNode }} PrintSpread a spread or rest element */
/** @typedef {PrintBase & { type: "AssignmentPattern", left: PrintNode, right: PrintNode }} PrintAssignmentPattern a default value */
/** @typedef {PrintBase & { type: "TemplateLiteral", segments: PrintNode[] }} PrintTemplateLiteral a template literal, its texts and expressions in order, two texts side by side where the compressor joined them */
/** @typedef {PrintBase & { type: "TemplateElement", value: string, raw: string }} PrintTemplateElement a template literal's text, `value` cooked */
/** @typedef {PrintBase & { type: "TaggedTemplateExpression", tag: PrintNode, quasi: PrintNode }} PrintTaggedTemplateExpression a tagged template */
/** @typedef {PrintBase & { type: "AwaitExpression", argument: PrintNode }} PrintAwaitExpression an `await` */
/** @typedef {PrintBase & { type: "YieldExpression", argument: PrintNode | null, delegate: boolean }} PrintYieldExpression a `yield` */
/** @typedef {PrintBase & { type: "CallExpression", callee: PrintNode, arguments: PrintNode[], optional: boolean }} PrintCallExpression a call */
/** @typedef {PrintBase & { type: "NewExpression", callee: PrintNode, arguments: PrintNode[] }} PrintNewExpression a `new` */
/** @typedef {PrintBase & { type: "ImportExpression", args: PrintNode[], phase: string | null | undefined }} PrintImportExpression a dynamic `import()`, its source then its options */
/** @typedef {PrintBase & { type: "MetaProperty", role: string }} PrintMetaProperty `import.meta` where `role` is "importMeta", else `new.target` */
/** @typedef {PrintBase & { type: "SequenceExpression", expressions: PrintNode[] }} PrintSequenceExpression a comma sequence */
/** @typedef {PrintBase & { type: "MemberExpression", object: PrintNode, property: PrintNode, computed: boolean, optional: boolean, quote: string | undefined }} PrintMemberExpression a property access, `quote` the quote a name read as `.name` had when written as a string */
/** @typedef {PrintBase & { type: "ChainExpression", expression: PrintNode }} PrintChainExpression an optional chain */
/** @typedef {PrintBase & { type: "UnaryExpression" | "UpdateExpression", operator: string, prefix: boolean, argument: PrintNode }} PrintUnary a unary operator or an increment or decrement */
/** @typedef {PrintBase & { type: "BinaryExpression" | "LogicalExpression" | "AssignmentExpression", operator: string, left: PrintNode, right: PrintNode }} PrintBinary a binary operator or an assignment */
/** @typedef {PrintBase & { type: "ConditionalExpression", test: PrintNode, consequent: PrintNode, alternate: PrintNode }} PrintConditionalExpression a conditional */
/** @typedef {PrintBase & { type: "ImportDeclaration", imported_name: PrintIdentifier | null, imported_names: PrintNameMapping[] | null, source: PrintNode, attributes: PrintObject | null, phase: string | null | undefined }} PrintImportDeclaration an `import`, its default name held alone */
/** @typedef {PrintBase & { type: "ImportSpecifier" | "ImportNamespaceSpecifier" | "ExportSpecifier", local: PrintNode, imported: PrintNode | null, exported: PrintNode | null }} PrintNameMapping a name an import takes, `imported` the other module's, or one an export gives, `exported` the other module's */
/** @typedef {PrintBase & { type: "ExportAllDeclaration" | "ExportDefaultDeclaration" | "ExportNamedDeclaration", exported_definition: PrintNode | null, exported_value: PrintNode | null, exported_names: PrintNameMapping[] | null, source: PrintNode | null, attributes: PrintObject | null }} PrintExport an export: of a declaration held as a definition or a value, of names, or of a module's, `export *` as one name mapping */
/** @typedef {PrintBase & { type: "PrivateIdentifier", name: string }} PrintPrivateIdentifier a private name */
/** @typedef {{ name: string, mangled_name?: string | null }} NamedDefinition what every definition of a name holds: the name it declares, and the one mangling gave it */
/**
* What a print recorded for mangling to rewrite in its text: each name it
* printed as written, and each import or export `as` clause.
* @typedef {object} MangleRecording
* @property {Int32Array} starts where each piece starts in the text, `count` of them
* @property {Int32Array} lengths how long each piece is, negated where the frequency count leaves its name out
* @property {number} count how many pieces there are
* @property {NamedDefinition[]} targets the definition whose name replaces each piece
* @property {MangleSpecifier[]} specifiers the pieces that are an import's or export's `as` clause, in order
* @property {string[]} counted text a string print would have written but this one did not
* @property {string[]} uncounted text this print wrote that a string print would not
*/
/**
* @returns {MangleRecording} a recording of nothing yet
*/
const createMangleRecording = () => ({
starts: new Int32Array(1024),
lengths: new Int32Array(1024),
count: 0,
targets: [],
specifiers: [],
counted: [],
uncounted: []
});
/**
* Records a piece of text that mangling rewrites, by where it lies.
* @param {MangleRecording} recorded the recording
* @param {number} start where it starts in the text
* @param {number} length how long it is, negated where the frequency count leaves its name out
* @param {NamedDefinition} target the definition whose name replaces it
* @returns {number} which piece it is
*/
const recordPieceAt = (recorded, start, length, target) => {
const { count } = recorded;
if (count === recorded.starts.length) {
const starts = new Int32Array(count * 2);
starts.set(recorded.starts);
recorded.starts = starts;
const lengths = new Int32Array(count * 2);
lengths.set(recorded.lengths);
recorded.lengths = lengths;
}
recorded.starts[count] = start;
recorded.lengths[count] = length;
recorded.targets.push(target);
recorded.count = count + 1;
return count;
};
/**
* Counts the characters of a recorded print as a print of its own counts
* them, less the names mangling renames; the caller sorts.
* @param {CharacterCounter} identifiers the identifier source counting
* @param {string} text what the print wrote
* @param {MangleRecording} recorded what it recorded
* @returns {void}
*/
const countRecording = (identifiers, text, recorded) => {
identifiers.reset();
identifiers.consider(text, 1);
const { lengths, targets, counted, uncounted } = recorded;
for (let i = 0; i < recorded.count; i++) {
if (lengths[i] < 0) identifiers.consider(targets[i].name, -1);
}
for (let i = 0; i < counted.length; i++) identifiers.consider(counted[i], 1);
for (let i = 0; i < uncounted.length; i++) {
identifiers.consider(uncounted[i], -1);
}
};
/**
* A recorded `as` clause: whether the local's final name matches the foreign
* one decides if it is written.
* @typedef {object} MangleSpecifier
* @property {number} index which piece it is
* @property {string} foreign the other module's name
* @property {string} text that name as printed
* @property {boolean} isImport whether the clause precedes the local name
*/
/** @typedef {PrintBase & { type: "Identifier", name: string, definition: MinifierVariable | MinifierLabel | NamedDefinition | null | undefined, atom?: boolean, role?: string }} PrintIdentifier a name as written, `atom` where terser prints it as a constant, `definition` what the compressor's or the mangler's scope analysis found it names */
/** @typedef {PrintBase & { type: "Literal", value: string | number | boolean | RegExp | null, raw: string | undefined, quote: string | undefined, role: string | undefined, name: string, _annotations: number | undefined }} PrintLiteral a literal, `quote` the quote a string had, `role` "bigint" where `value` is a bigint's digits and "regexp" for a regular expression, `name` a quoted module name's, `_annotations` holding `KEY` where a `@__KEY__` annotation marks a string a property name */
/**
* A node of the compressed tree as the printer and the mangler read it, by its
* ESTree type; an absent child reads null or undefined, which the types leave
* out of a child's type, as `NO_NODE` does terser's.
* @typedef {PrintProgram | PrintExpressionStatement | PrintBlockStatement | PrintLeaf | PrintLabeledStatement | PrintWhileStatement | PrintForStatement | PrintForInStatement | PrintForOfStatement | PrintWithStatement | PrintExitStatement | PrintLoopControl | PrintIfStatement | PrintSwitchStatement | PrintSwitchCase | PrintTryStatement | PrintCatchClause | PrintVariableDeclaration | PrintVariableDeclarator | PrintFunction | PrintClass | PrintClassBody | PrintPropertyDefinition | PrintProperty | PrintMethodDefinition | PrintObject | PrintArray | PrintSpread | PrintAssignmentPattern | PrintTemplateLiteral | PrintTemplateElement | PrintTaggedTemplateExpression | PrintAwaitExpression | PrintYieldExpression | PrintCallExpression | PrintNewExpression | PrintImportExpression | PrintMetaProperty | PrintSequenceExpression | PrintMemberExpression | PrintChainExpression | PrintUnary | PrintBinary | PrintConditionalExpression | PrintImportDeclaration | PrintNameMapping | PrintExport | PrintPrivateIdentifier | PrintIdentifier | PrintLiteral} PrintNode
*/
/**
* @template {PrintNode["type"]} T
* @typedef {PrintNode extends infer N ? N extends PrintNode ? T extends N["type"] ? N & { type: T } : never : never : never} PrintNodeOf the ESTree node of a type
*/
/** @typedef {PrintLabeledStatement | PrintIfStatement | PrintWithStatement | PrintForStatement | PrintForInStatement | PrintForOfStatement | PrintWhileStatement} PrintStatementWithBody a statement holding another, terser's `StatementWithBody` */
/** @typedef {PrintImportDeclaration | PrintExport} PrintModuleDeclaration an import or an export that may name a source and attributes */
/** @typedef {PrintBinary | PrintAssignmentPattern} PrintBinaryLike what `kindOf.Binary` finds: a binary, logical or assignment expression, or an assignment pattern */
/** @typedef {PrintNode | (PrintBase & { type: "" })} PrintParent a node holding the one printed, or past the root one of no type */
/** @typedef {Record<string, PrintNode | (PrintNode | null)[] | null>} PrintFields a node's fields as a walk reads them, its children and lists of them beside scalars no type test matches */
/**
* terser's node classes the printer asks of an ESTree node, by name without
* `Node`; a class matching only some nodes of a type answers a bare boolean.
* @typedef {object} PrintKinds
* @property {(node: PrintParent) => node is PrintNodeOf<"ArrayExpression">} Array terser's `Array`
* @property {(node: PrintParent) => node is PrintNodeOf<"ArrowFunctionExpression">} Arrow terser's `Arrow`
* @property {(node: PrintParent) => node is PrintNodeOf<"AssignmentExpression">} Assign terser's `Assign`
* @property {(node: PrintParent) => node is PrintAwaitExpression} Await terser's `Await`
* @property {(node: PrintParent) => boolean} Binary terser's `Binary`, which `#x in y` is not
* @property {(node: PrintParent) => node is PrintBlockStatement} BlockStatement terser's `BlockStatement`
* @property {(node: PrintParent) => node is PrintCallExpression | PrintNewExpression} Call terser's `Call`
* @property {(node: PrintParent) => node is PrintChainExpression} Chain terser's `Chain`
* @property {(node: PrintParent) => node is PrintClass} Class terser's `Class`
* @property {(node: PrintParent) => node is PrintNodeOf<"ClassExpression">} ClassExpression terser's `ClassExpression`
* @property {(node: PrintParent) => node is PrintConditionalExpression} Conditional terser's `Conditional`
* @property {(node: PrintParent) => node is PrintAssignmentPattern} DefaultAssign terser's `DefaultAssign`
* @property {(node: PrintParent) => boolean} Definitions terser's `Definitions`, which `using` is not
* @property {(node: PrintParent) => node is PrintVariableDeclaration} DefinitionsLike terser's `DefinitionsLike`
* @property {(node: PrintParent) => node is PrintNodeOf<"FunctionDeclaration">} Defun terser's `Defun`
* @property {(node: PrintParent) => node is PrintNodeOf<"ArrayPattern" | "ObjectPattern">} Destructuring terser's `Destructuring`
* @property {(node: PrintParent) => boolean} Directive terser's `Directive`
* @property {(node: PrintParent) => node is PrintNodeOf<"DoWhileStatement">} Do terser's `Do`
* @property {(node: PrintParent) => boolean} Dot terser's `Dot`
* @property {(node: PrintParent) => boolean} DotHash terser's `DotHash`
* @property {(node: PrintParent) => node is PrintNodeOf<"EmptyStatement">} EmptyStatement terser's `EmptyStatement`
* @property {(node: PrintParent) => node is PrintSpread} Expansion terser's `Expansion`
* @property {(node: PrintParent) => node is PrintExport} Export terser's `Export`
* @property {(node: PrintParent) => node is PrintForStatement} For terser's `For`
* @property {(node: PrintParent) => node is PrintForInStatement | PrintForOfStatement} ForIn terser's `ForIn`
* @property {(node: PrintParent) => node is PrintForOfStatement} ForOf terser's `ForOf`
* @property {(node: PrintParent) => node is PrintNodeOf<"FunctionExpression">} Function terser's `Function`
* @property {(node: PrintParent) => node is PrintIfStatement} If terser's `If`
* @property {(node: PrintParent) => node is PrintImportDeclaration} Import terser's `Import`
* @property {(node: PrintParent) => node is PrintFunction} Lambda terser's `Lambda`
* @property {(node: PrintParent) => node is PrintNewExpression} New terser's `New`
* @property {(node: PrintParent) => node is PrintNodeOf<"ObjectExpression">} Object terser's `Object`
* @property {(node: PrintParent) => node is PrintProperty | PrintMethodDefinition | PrintPropertyDefinition} ObjectProperty terser's `ObjectProperty`
* @property {(node: PrintParent) => node is PrintTaggedTemplateExpression} PrefixedTemplateString terser's `PrefixedTemplateString`
* @property {(node: PrintParent) => boolean} PrivateIn terser's `PrivateIn`, a `#x in y`
* @property {(node: PrintParent) => node is PrintMemberExpression} PropAccess terser's `PropAccess`
* @property {(node: PrintParent) => node is PrintNodeOf<"ReturnStatement">} Return terser's `Return`
* @property {(node: PrintParent) => node is PrintProgram | PrintFunction | PrintClass | PrintNodeOf<"StaticBlock">} Scope terser's `Scope`
* @property {(node: PrintParent) => node is PrintSequenceExpression} Sequence terser's `Sequence`
* @property {(node: PrintParent) => boolean} SimpleStatement terser's `SimpleStatement`
* @property {(node: PrintParent) => node is PrintStatementWithBody} StatementWithBody terser's `StatementWithBody`
* @property {(node: PrintParent) => boolean} Sub terser's `Sub`
* @property {(node: PrintParent) => node is PrintTemplateElement} TemplateSegment terser's `TemplateSegment`
* @property {(node: PrintParent) => node is PrintUnary} Unary terser's `Unary`
* @property {(node: PrintParent) => boolean} UnaryPostfix terser's `UnaryPostfix`
* @property {(node: PrintParent) => boolean} UnaryPrefix terser's `UnaryPrefix`
* @property {(node: PrintParent) => boolean} Var terser's `Var`
* @property {(node: PrintParent) => node is PrintVariableDeclarator} VarDefLike terser's `VarDefLike`
* @property {(node: PrintParent) => node is PrintYieldExpression} Yield terser's `Yield`
*/
/** @typedef {{ reset: () => void, consider: (chars: string, delta: number) => void, sort: () => void }} CharacterCounter an identifier source that counts the characters printed, as terser's `base54` does */
// What only the compressor reads off a node, dropped from a tree nobody
// keeps once the mangler or the printer has read the node.
const COMPRESSOR_ONLY_FIELDS = [
"scope",
"globals",
"block_scope",
"init",
"references"
];
/**
* Drops what only the compressor reads off a node, a scope's definitions
* among them, leaving the fields its kind's factory made.
* @param {PrintNode | Node} node a node
* @returns {void}
*/
const releaseCompressorFields = (node) => {
const fields =
/** @type {NodeProps & { [NODE_COMPRESSOR_FIELDS]: (keyof NodeFields)[] }} */ (
node
);
const dropped = fields[NODE_COMPRESSOR_FIELDS];
for (let i = 0; i < dropped.length; i++) {
const field = dropped[i];
if (fields[field] !== undefined) fields[field] = undefined;
}
};
/** @typedef {(Comment[] | undefined)[]} CommentGaps the comments of each gap between a parse's tokens, by the token after it, while the parse reads them */
/**
* What a `SourceToken` reads off the parse that read it, by the token's index.
* @internal
* @typedef {object} TokenReader
* @property {string | undefined} fileName the file its tokens name
* @property {(index: number) => number} positionOf where a token starts
* @property {(index: number) => string} kindOf terser's name for a token's kind
* @property {(index: number) => string} valueOf what a token holds
* @property {(index: number) => number} lineOf a token's line, from 1
* @property {(index: number) => number} columnOf a token's column, from 0
* @property {(index: number) => boolean} brokeLineAt whether a line break comes before a token
* @property {(index: number, broke: boolean) => void} setBrokeLineAt records whether one does
* @property {(index: number, after: boolean) => Comment[]} commentsOf the comments before or after a token
* @property {(index: number, after: boolean, comments: Comment[]) => void} bindComments binds a list of comments onto a token
*/
/**
* A token of webpack's parse: where it sits in the source, and the parse that
* read it. terser's `ParsedToken` keeps what this reads off that parse — its
* line, its kind, the comments beside it — in nine fields of its own, which a
* tree of a million nodes cannot afford.
*/
class SourceToken {
/**
* @param {TokenReader} tokens the parse that read it
* @param {number} index which of its tokens this is
*/
constructor(tokens, index) {
this.tokens = tokens;
this.index = index;
}
/** @returns {number} where it starts */
get pos() {
return this.tokens.positionOf(this.index);
}
/** @returns {string} terser's name for its kind */
get type() {
return this.tokens.kindOf(this.index);
}
/** @returns {string} what it holds */
get value() {
return this.tokens.valueOf(this.index);
}
/** @returns {number} its line, from 1 */
get line() {
return this.tokens.lineOf(this.index);
}
/** @returns {number} its column, from 0 */
get col() {
return this.tokens.columnOf(this.index);
}
/** @returns {string | undefined} its file */
get file() {
return this.tokens.fileName;
}
/** @returns {boolean} whether a line break comes before it */
get nlb() {
return this.tokens.brokeLineAt(this.index);
}
/**
* @param {boolean} broke whether a line break comes before it
*/
set nlb(broke) {
this.tokens.setBrokeLineAt(this.index, broke);
}
/** @returns {Comment[]} the comments read before it */
get comments_before() {
return this.tokens.commentsOf(this.index, false);
}
/**
* @param {Comment[]} comments the comments read before it
*/
set comments_before(comments) {
this.tokens.bindComments(this.index, false, comments);
}
/** @returns {Comment[]} the comments read after it */
get comments_after() {
return this.tokens.commentsOf(this.index, true);
}
/**
* @param {Comment[]} comments the comments read after it
*/
set comments_after(comments) {
this.tokens.bindComments(this.index, true, comments);
}
/**
* @param {number} _depth how deep Node's inspector is
* @param {import("util").InspectContext} options its options
* @returns {string} the token, as Node's console shows it
*/
[Symbol.for("nodejs.util.inspect.custom")](_depth, options) {
return `ParsedToken { type: ${options.stylize(JSON.stringify(this.type), "string")}, value: ${options.stylize(JSON.stringify(this.value), "string")}, line: ${options.stylize(String(this.line), "number")}, col: ${options.stylize(String(this.col), "number")} }`;
}
}
/** @typedef {{ base: number, size: () => number, token: (index: number) => SourceToken, commentsOf: (index: number, after: boolean) => Comment[], forEachCommentGap: (callback: (index: number, comments: Comment[]) => void) => void, outerCounts: Map<number, number> }} TokenTable the tokens of one parse, read by their index past `base` */
/**
* The tokens of every parse a tree was read from. Its nodes hold their tokens'
* numbers, which this answers for; a token handed in whole answers for itself.
*/
class TokenTables {
constructor() {
/** @type {TokenTable[]} */
this.tables = [];
// The last parse's table, which a tree of one file reads every token in.
/** @type {TokenTable | undefined} */
this.last = undefined;
this.lastBase = Infinity;
}
/**
* @param {TokenTable} table the table of a parse read onto the tree
* @returns {void}
*/
add(table) {
this.tables.push(table);
this.last = table;
this.lastBase = table.base;
}
/**
* @returns {number} the number the next parse's first token takes, never 0
*/
nextBase() {
const { tables } = this;
if (tables.length === 0) return 1;
const last = tables[tables.length - 1];
return last.base + last.size();
}
/**
* @param {number} number a token's number
* @returns {TokenTable | undefined} the table of the parse that read it
*/
tableOf(number) {
if (number >= this.lastBase) return this.last;
const { tables } = this;
for (let i = tables.length - 1; i >= 0; i--) {
if (number >= tables[i].base) return tables[i];
}
return undefined;
}
/**
* @param {NodeToken | number | null | undefined} token a node's token, or its number
* @returns {NodeToken | null | undefined} the token itself
*/
of(token) {
if (typeof token !== "number") return token;
const table = this.tableOf(token);
return table === undefined ? undefined : table.token(token - table.base);
}
/**
* @param {(token: number, comments: Comment[]) => void} callback handed each gap holding comments, by the number of the token after it
* @returns {void}
*/
forEachCommentGap(callback) {
for (const table of this.tables) {
table.forEachCommentGap((index, comments) =>
callback(table.base + index, comments)
);
}
}
/**
* @param {NodeToken | number | null | undefined} token a node's token, or its number
* @param {boolean} after true for the comments after it
* @returns {Comment[] | undefined} the comments before or after it, the token unbuilt
*/
commentsOf(token, after) {
if (typeof token !== "number") {
if (!token) return undefined;
return after ? token.comments_after : token.comments_before;
}
const table = this.tableOf(token);
return table === undefined
? undefined
: table.commentsOf(token - table.base, after);
}
}
// What a tree read without webpack's parse answers its tokens with.
const NO_TOKEN_TABLES = new TokenTables();
// The parse option a tree's tables are handed in by, for a parse whose nodes
// join that tree: its tokens are numbered after the tree's own.
const PARSE_ONTO_TABLES = Symbol("tables a parse numbers its tokens after");
/**
* @param {NodeToken | number} first a node's token, or its number
* @param {NodeToken | number} second another's, of the same tree
* @returns {boolean} whether the first comes before the second in the source
*/
const tokenPrecedes = (first, second) =>
typeof first === "number" && typeof second === "number"
? first < second
: /** @type {NodeToken} */ (first).pos <
/** @type {NodeToken} */ (second).pos;
/**
* Installs webpack's parse: the printer's tree built by webpack's own parser as
* it finishes each statement, with that parser's tokens and comments. terser's
* parser stays behind it, for the sources it reads more leniently.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installFusedParse = (modules) => {
const { ast, parse } = modules;
const { isSyntaxNode, isToplevelNode } = ast;
const {
SyntaxTree,
WebpackParser,
buildLineStarts,
positionAt,
tokTypes
} = require("./syntax-parser");
// What terser's tokenizer calls each of webpack's token kinds. The printer
// reads a node's own tokens for where they sit, the comments they carry and
// the name a source map maps from, which it takes off the kind.
const PUNCTUATION_LABELS = new Set([
"[",
"]",
"{",
"}",
"(",
")",
",",
";",
":",
".",
"?.",
"`",
"${"
]);
const tables = /** @type {Record<string, string[]>} */ (
require("./syntax-printer-data").parserTables()
);
const KEYWORDS = new Set(tables.KEYWORDS);
const KEYWORDS_ATOM = new Set(tables.KEYWORDS_ATOM);
const WORD_OPERATORS = new Set(
tables.OPERATORS.filter((operator) => KEYWORDS.has(operator))
);
/**
* terser's name for a word's kind, as its `read_word` gives one: a word it
* does not keyword names something, and `new` and `typeof` are operators.
* @param {string} word the word read
* @param {boolean} afterDot whether a `.` comes before it
* @returns {string} terser's name for its kind
*/
const wordKindOf = (word, afterDot) => {
if (afterDot) return "name";
if (KEYWORDS_ATOM.has(word)) return "atom";
if (!KEYWORDS.has(word)) return "name";
return WORD_OPERATORS.has(word) ? "operator" : "keyword";
};
/**
* @param {import("./syntax-parser").ParserInstance["type"]} type one of webpack's token kinds
* @returns {string | null} terser's name for it, null where the word decides
*/
const tokenKindOf = (type) => {
if (type === tokTypes.name || type.keyword) return null;
if (type === tokTypes.privateId) return "privatename";
if (type === tokTypes.num) return "num";
if (type === tokTypes.string) return "string";
if (type === tokTypes.regexp) return "regexp";
if (type === tokTypes.arrow) return "arrow";
if (type === tokTypes.ellipsis) return "expand";
if (type === tokTypes.eof) return "eof";
return PUNCTUATION_LABELS.has(type.label) ? "punc" : "operator";
};
// Every kind a token can have, which one holds as an index into this.
const KINDS = [
"",
"name",
"privatename",
"num",
"string",
"regexp",
"arrow",
"expand",
"eof",
"punc",
"operator",
"atom",
"keyword"
];
/** @type {Map<string, number>} */
const KIND_CODES = new Map(KINDS.map((kind, code) => [kind, code]));
// What `ends` holds for the token past the source, which no node ends at.
const NO_END = 0x7fffffff;
// The list every token with no comments on either side shares. Nothing writes
// into it: a gap gaining a comment takes a list of its own first.
/** @type {Comment[]} */
const NO_COMMENTS = [];
// A token type's kind as a code, so reporting one costs a single lookup; null
// where the kind follows from the word the token spells.
/** @type {Map<import("./syntax-parser").ParserInstance["type"], number | null>} */
const TOKEN_TYPES = new Map(
Object.values(tokTypes).map((type) => {
const named = tokenKindOf(type);
return [
type,
named === null ? null : /** @type {number} */ (KIND_CODES.get(named))
];
})
);
const STRING_CODE = /** @type {number} */ (KIND_CODES.get("string"));
const PRIVATENAME_CODE = /** @type {number} */ (
KIND_CODES.get("privatename")
);
const EOF_CODE = /** @type {number} */ (KIND_CODES.get("eof"));
/**
* The tokens of a parse, as terser's tokenizer leaves them: each one holding
* the comments read since the token before it, in a list it shares with that
* token's `comments_after`. A node holds its tokens' numbers, `base` past its
* own index, and a token is built only where a reader needs one.
* @param {string} source the source being parsed
* @param {string | null} filename what its tokens name as their file
* @param {number} base the number of this parse's first token
* @returns {{ onComment: import("./syntax-parser").OnComment, onToken: NonNullable<import("./syntax-parser").Options["onTokenRead"]>, shebang: (text: string) => void, finish: () => void, at: (pos: number, ending: boolean, forward?: number) => number | undefined, table: TokenTable }} where to report comments and tokens, the token at an offset, what narrows the tables once the parse is over, and the table its numbers are read through
*/
const collectTokens = (source, filename, base) => {
// Where each token sits, its kind as an index into `KINDS`, and whether a
// line break came before it: sized for the tokens a source this long holds,
// doubled where it holds more, and trimmed by `finish` to what it read.
let capacity = 64 + (source.length >> 2);
let count = 0;
let starts = new Int32Array(capacity);
let ends = new Int32Array(capacity);
let kinds = new Uint8Array(capacity);
let breaks = new Uint8Array(capacity);
/**
* @returns {void}
*/
const grow = () => {
capacity *= 2;
const widerStarts = new Int32Array(capacity);
widerStarts.set(starts);
starts = widerStarts;
const widerEnds = new Int32Array(capacity);
widerEnds.set(ends);
ends = widerEnds;
const widerKinds = new Uint8Array(capacity);
widerKinds.set(kinds);
kinds = widerKinds;
const widerBreaks = new Uint8Array(capacity);
widerBreaks.set(breaks);
breaks = widerBreaks;
};
// The value of a token the source does not spell out: a string holds what
// its escapes mean, and a private name is read without its `#`.
/** @type {Map<number, string> | null} */
let cooked = new Map();
/** @type {SourceToken[]} */
let built = [];
// Once the parse is over, a token asked for before the parse read it, and
// never read, is the one past the source.
let finished = false;
/** @type {CommentGaps | Map<number, Comment[]>} */
let lists = [];
// Once the parse is over, which gaps hold comments, so the rest are
// answered without a lookup: most nodes are asked, and few gaps hold any.
/** @type {Uint8Array | null} */
let commentedGaps = null;
/** @type {number[] | null} */
let lineStarts = null;
let previousEnd = 0;
// Whether a line break has been read since the last token, which reading
// one clears, as terser's tokenizer has it.
let brokenLine = false;
// Where `at` last found its token: nodes are asked for in the order they
// sit, so the next is mostly a few tokens away.
let lastFound = 0;
// Whether the token before was a `.` or `?.`, after which terser reads a
// word as a name whatever else it is.
let afterDot = false;
/**
* @param {number} index the token the comments were read before
* @returns {Comment[]} them, as terser's tokens
*/
const listAt = (index) => {
if (lists instanceof Map) {
const kept = lists.get(index);
return kept === undefined ? NO_COMMENTS : kept;
}
const known = lists[index];
if (known !== undefined) return known;
// A gap already read past holds none: it shares the empty list, which is
// not kept, as most gaps are asked for once. One not read past gains its
// comments after this, so it is given a list of its own to gain them in.
if (index < count) return NO_COMMENTS;
/** @type {Comment[]} */
const list = [];
lists[index] = list;
return list;
};
/**
* Keeps a comment in the list of the gap it was read in, which the tokens on
* either side of that gap both read.
* @param {number} index the token it was read before
* @param {string} type terser's name for its kind
* @param {string} text what it holds
* @param {number} start where it starts
* @param {boolean} nlb whether a line break comes before it
* @returns {void}
*/
const read = (index, type, text, start, nlb) => {
let list = /** @type {CommentGaps} */ (lists)[index];
if (list === undefined || list === NO_COMMENTS) {
list = [];
/** @type {CommentGaps} */ (lists)[index] = list;
}
if (lineStarts === null) lineStarts = buildLineStarts(source);
const { line, column } = /** @type {{ line: number, column: number }} */ (
positionAt(lineStarts, start)
);
list.push(
new ParsedToken(
type,
text,
line,
column,
start,
nlb,
NO_COMMENTS,
NO_COMMENTS,
filename === null ? undefined : filename
)
);
};
/**
* @param {number} index which token
* @returns {SourceToken} it, built once
*/
const build = (index) => {
if (finished && index >= count) index = count - 1;
const known = built[index];
if (known !== undefined) return known;
const token = new SourceToken(reader, index);
built[index] = token;
return token;
};
/**
* @param {number} index which token
* @returns {{ line: number, column: number }} where it sits
*/
const placeOf = (index) => {
if (lineStarts === null) lineStarts = buildLineStarts(source);
return /** @type {{ line: number, column: number }} */ (
positionAt(lineStarts, starts[index])
);
};
// A list bound onto one token by a parenthesis, which the gap it was read
// in keeps out of: one token to an index, so the index names it.
/** @type {Map<number, Comment[]> | null} */
let boundBefore = null;
/** @type {Map<number, Comment[]> | null} */
let boundAfter = null;
// A bit per token with a list bound, so the many with none skip the maps,
// whose lookup costs more than the rest of the read.
let boundTokens = new Uint8Array(0);
// What a `SourceToken` of this parse reads off it. The arrays it reads are
// the ones `finish` narrows; the rest go when the parse is over.
/** @type {TokenReader} */
const reader = {
fileName: filename === null ? undefined : filename,
positionOf: (/** @type {number} */ index) => starts[index],
kindOf: (/** @type {number} */ index) => KINDS[kinds[index]],
valueOf: (/** @type {number} */ index) =>
cooked !== null && cooked.has(index)
? /** @type {string} */ (cooked.get(index))
: source.slice(starts[index], ends[index]),
lineOf: (/** @type {number} */ index) => placeOf(index).line,
columnOf: (/** @type {number} */ index) => placeOf(index).column,
brokeLineAt: (/** @type {number} */ index) => breaks[index] === 1,
setBrokeLineAt: (
/** @type {number} */ index,
/** @type {boolean} */ broke
) => {
breaks[index] = broke ? 1 : 0;
},
commentsOf: (
/** @type {number} */ index,
/** @type {boolean} */ after
) => {
const byte = index >> 3;
if (
byte < boundTokens.length &&
(boundTokens[byte] & (1 << (index & 7))) !== 0
) {
const bound = after ? boundAfter : boundBefore;
if (bound !== null) {
const own = bound.get(index);
if (own !== undefined) return own;
}
}
const gap = after ? index + 1 : index;
if (commentedGaps !== null && commentedGaps[gap] === 0) {
return NO_COMMENTS;
}
return listAt(gap);
},
bindComments: (
/** @type {number} */ index,
/** @type {boolean} */ after,
/** @type {Comment[]} */ comments
) => {
const byte = index >> 3;
if (byte >= boundTokens.length) {
const wider = new Uint8Array(
Math.max(byte + 1, boundTokens.length * 2)
);
wider.set(boundTokens);
boundTokens = wider;
}
boundTokens[byte] |= 1 << (index & 7);
if (after) {
if (boundAfter === null) boundAfter = new Map();
boundAfter.set(index, comments);
} else {
if (boundBefore === null) boundBefore = new Map();
boundBefore.set(index, comments);
}
}
};
return {
onComment: (
/** @type {boolean} */ isBlock,
/** @type {string} */ text,
/** @type {number} */ start,
/** @type {number} */ end
) => {
// The parser reports the hashbang line too, which `shebang` reads.
if (start === 0 && source.startsWith("#!")) return;
let nlb = brokenLine;
for (let i = previousEnd; !nlb && i < start; i++) {
const code = source.charCodeAt(i);
nlb =
code === 10 || code === 13 || code === 0x2028 || code === 0x2029;
}
// A block comment is read with every line break in it spelled `\n`,
// which is what terser's tokenizer leaves for the printer to write.
const value = isBlock
? text.replace(/\r\n|[\r\u2028\u2029]/g, "\n")
: text;
// WHY: terser's tokenizer steps over a comment's own characters before
// it makes the token, so one holding a line break reads as having a
// break before it as well as breaking the line for what follows.
brokenLine = isBlock && value.includes("\n");
read(
count,
isBlock ? "comment2" : "comment1",
value,
start,
nlb || brokenLine
);
previousEnd = end;
},
onToken: (
/** @type {import("./syntax-parser").ParserInstance} */ token
) => {
let nlb = brokenLine;
const from = token.start;
for (let i = previousEnd; !nlb && i < from; i++) {
const code = source.charCodeAt(i);
nlb =
code === 10 || code === 13 || code === 0x2028 || code === 0x2029;
}
brokenLine = false;
const type = token.type;
const named = TOKEN_TYPES.get(type);
const value = token.value === undefined ? "" : token.value;
const kind =
named === undefined || named === null
? /** @type {number} */ (
KIND_CODES.get(wordKindOf(String(value), afterDot))
)
: named;
afterDot = type === tokTypes.dot || type === tokTypes.questionDot;
if (kind === STRING_CODE || kind === PRIVATENAME_CODE) {
/** @type {Map<number, string>} */ (cooked).set(
count,
/** @type {string} */ (value)
);
}
if (count === capacity) grow();
starts[count] = from;
// A node ends at the last token it holds, which is never the one past
// the source: terser leaves the comments after it on the token before.
ends[count] = kind === EOF_CODE ? NO_END : token.end;
kinds[count] = kind;
breaks[count] = nlb ? 1 : 0;
count++;
previousEnd = token.end;
},
shebang: (/** @type {string} */ text) => {
read(0, "comment5", text, 0, false);
},
finish: () => {
// The parse is over: the room it was given beyond what it read goes,
// and so does everything only the parse needed.
const narrowStarts = starts.slice(0, count);
const narrowEnds = ends.slice(0, count);
const narrowKinds = kinds.slice(0, count);
const narrowBreaks = breaks.slice(0, count);
/** @type {Map<number, Comment[]>} */
const kept = new Map();
let gaps = count + 2;
for (let i = 0; i < /** @type {CommentGaps} */ (lists).length; i++) {
const list = /** @type {CommentGaps} */ (lists)[i];
if (list !== undefined && list.length !== 0) {
kept.set(i, list);
if (i >= gaps) gaps = i + 1;
}
}
const flags = new Uint8Array(gaps);
for (const index of kept.keys()) flags[index] = 1;
commentedGaps = flags;
finished = true;
starts = narrowStarts;
ends = narrowEnds;
kinds = narrowKinds;
breaks = narrowBreaks;
lists = kept;
built = [];
if (/** @type {Map<number, string>} */ (cooked).size === 0) {
cooked = null;
}
},
at: (
/** @type {number} */ pos,
/** @type {boolean} */ ending,
/** @type {number=} */ forward
) => {
const offsets = ending ? ends : starts;
let low = 0;
// What the parse has read, which is short of the room it was given.
let high = count - 1;
// Every token before `low` is before `pos`, and none after `high` is.
if (lastFound <= high) {
if (ending ? offsets[lastFound] <= pos : offsets[lastFound] < pos) {
low = lastFound + 1;
const far = lastFound + 32;
if (
far <= high &&
!(ending ? offsets[far] <= pos : offsets[far] < pos)
) {
high = far - 1;
}
} else {
high = lastFound - 1;
const far = lastFound - 32;
if (
far >= 0 &&
(ending ? offsets[far] <= pos : offsets[far] < pos)
) {
low = far + 1;
}
}
}
// The last token ending by `pos`, or the first one starting from it.
while (low <= high) {
const middle = (low + high) >> 1;
if (ending ? offsets[middle] <= pos : offsets[middle] < pos) {
low = middle + 1;
} else {
high = middle - 1;
}
}
lastFound = low;
let index = ending ? high : low;
if (forward !== undefined && index >= 0) {
// What terser's `peek()` holds: a token the parse may not have read
// when a node is given it, and the one past the source where the
// parse ends before reaching that far.
index += forward;
if (index >= count) return base + index;
}
return index < 0 || index >= count ? undefined : base + index;
},
table: {
base,
// The tokens past what the parse read, a forward look's at most.
size: () => count + 3,
token: build,
commentsOf: (
/** @type {number} */ index,
/** @type {boolean} */ after
) =>
reader.commentsOf(
finished && index >= count ? count - 1 : index,
after
),
// Read once the parse is over, when only gaps holding comments are kept.
forEachCommentGap: (
/** @type {(index: number, comments: Comment[]) => void} */ callback
) => {
for (const [gap, list] of /** @type {Map<number, Comment[]>} */ (
lists
)) {
callback(gap, list);
}
},
outerCounts: new Map()
}
};
};
/**
* webpack's parser, converting each statement it finishes into the printer's
* node. An export reads the declaration it holds, so that one waits.
*/
class PrinterParser extends WebpackParser {
/**
* @param {ConstructorParameters<typeof WebpackParser>[0]} options what to parse it as
* @param {string} source the source
* @param {((pos: number, ending: boolean) => number | undefined)=} tokenAt the token at an offset
* @param {TokenTable=} tokenTable the table its numbers are read through
*/
constructor(options, source, tokenAt, tokenTable) {
super(options, source);
this.webpackTokenAt = tokenAt;
this.webpackTokenTable = tokenTable;
this.webpackSource = source;
this.webpackKeepsEstree = false;
}
/**
* Read one statement. A statement head that is only a keyword by position —
* `let`, `async`, `using` — is settled by the probes above.
* @param {string | null=} context what encloses the statement
* @param {boolean=} topLevel whether it sits at the top level
* @param {ExportedNames=} exports where module exports are tracked
* @returns {Statement | ModuleDeclaration} the statement
*/
parseStatement(context, topLevel, exports) {
const statement = super.parseStatement(context, topLevel, exports);
return this.webpackKeepsEstree
? statement
: /** @type {Statement | ModuleDeclaration} */ (
/** @type {Statement | ModuleDeclaration | Node} */ (
ast.adoptEstree(
statement,
this.webpackTokenAt,
this.webpackSource,
this.webpackTokenTable
)
)
);
}
// A statement already converted is none: its conversion reads the
// directives, which ESTree's `type` on terser's nodes must not repeat.
/**
* @param {NodeLike & { expression?: ExpressionField }} statement the statement to test
* @returns {boolean} whether it may be a directive
*/
isDirectiveCandidate(statement) {
return !isSyntaxNode(statement) && super.isDirectiveCandidate(statement);
}
/**
* @param {UnfinishedNode} node the export node
* @param {ExportedNames | undefined} exports where module exports are tracked
* @returns {NodeOf<"ExportAllDeclaration"> | NodeOf<"ExportDefaultDeclaration"> | NodeOf<"ExportNamedDeclaration">} the finished export
*/
parseExport(node, exports) {
const was = this.webpackKeepsEstree;
this.webpackKeepsEstree = true;
try {
return super.parseExport(node, exports);
} finally {
this.webpackKeepsEstree = was;
}
}
}
/**
* webpack's parser, writing each function body it finishes into a tree, so
* the ESTree nodes of the body go before the rest of the source is read.
*/
class TreeParser extends WebpackParser {
/**
* @param {ConstructorParameters<typeof WebpackParser>[0]} options what to parse it as
* @param {string} source the source
* @param {SyntaxTree} tree the tree written into
*/
constructor(options, source, tree) {
super(options, source);
this.webpackTree = tree;
}
/**
* @param {UnfinishedNode} node the function node
* @param {boolean=} isArrowFunction whether it is an arrow
* @param {boolean=} isMethod whether it is a method
* @param {boolean | string=} forInit whether `in` is held back as an operator
* @returns {void}
*/
parseFunctionBody(node, isArrowFunction, isMethod, forInit) {
super.parseFunctionBody(node, isArrowFunction, isMethod, forInit);
// Nothing reads a finished body again: it stands as its id.
if (/** @type {AnyNode} */ (node.body).type === "BlockStatement") {
/** @type {NodeLike & { body: AnyNode | number }} */ (node).body =
this.webpackTree.addEstree(
/** @type {import("./syntax-parser").BlockStatement} */ (node.body)
);
}
}
}
// The parse options read, each at its default; any other is refused.
/** @type {{ bare_returns: boolean, ecma: number | null, filename: string | null, html5_comments: boolean, module: boolean, shebang: boolean, toplevel: Node | null }} */
const PARSE_DEFAULTS = {
bare_returns: false,
ecma: null,
filename: null,
html5_comments: true,
module: false,
shebang: true,
toplevel: null
};
/**
* @param {string} source the source
* @param {typeof PARSE_DEFAULTS} options terser's parse options
* @returns {import("./syntax-parser").Options & { importPhases: boolean }} what to ask webpack's parser for
*/
const parserOptions = (source, options) => ({
ecmaVersion: "latest",
sourceType: options.module ? "module" : "script",
sourceFile: options.filename === null ? undefined : options.filename,
// terser's parser keeps a parenthesis in the tokens of the expression it
// holds, which the printer and the source map both read.
preserveParens: true,
// WHY: `await` outside a module is an ordinary name a script may call, so
// reading it as the operator turns `await(x)` into `await x`, which no
// longer parses. The other two cannot change what a name means, and a
// bundled asset holds `super` outside a method.
allowReturnOutsideFunction: Boolean(options.bare_returns),
allowAwaitOutsideFunction: Boolean(options.module),
allowSuperOutsideMethod: true,
allowHashBang: Boolean(options.shebang) && source.startsWith("#!"),
// terser reads module syntax in a script too, and `import defer`.
allowImportExportEverywhere: !options.module,
importPhases: true
});
/**
* Parses a source with webpack's parser, its tokens and comments kept as
* terser's tokenizer leaves them, and converts what it read.
* @template T
* @param {string} text the source
* @param {typeof PARSE_DEFAULTS} options terser's parse options, defaulted
* @param {string | null} filename what its tokens name as their file
* @param {number} base the number of its first token
* @param {(options: import("./syntax-parser").Options & { importPhases: boolean }, tokenAt: (pos: number, ending: boolean, forward?: number) => number | undefined, table: TokenTable) => { parse(): import("./syntax-parser").Program }} createParser makes the parser to read it with, given its options
* @param {(program: import("./syntax-parser").Program, tokenAt: (pos: number, ending: boolean, forward?: number) => number | undefined, table: TokenTable) => T} convert what to make of the parsed program, before the tokens are narrowed
* @returns {{ result: T, table: TokenTable }} what `convert` made, and the table the tokens' numbers are read through
*/
const readSource = (text, options, filename, base, createParser, convert) => {
/** @type {T | undefined} */
let result;
/** @type {TokenTable | undefined} */
let table;
try {
const tokens = collectTokens(text, filename, base);
table = tokens.table;
if (options.shebang !== false && text.startsWith("#!")) {
// The hashbang ends at the first line terminator, whichever it is.
const line = text.search(/[\n\r\u2028\u2029]/);
tokens.shebang(text.slice(2, line === -1 ? text.length : line));
}
const parser = createParser(
{
...parserOptions(text, options),
onComment: tokens.onComment,
// The parser itself at each token, which keeps its token fast path
// and spares a token object a reader would only copy from.
onTokenRead: tokens.onToken
},
tokens.at,
table
);
result = convert(parser.parse(), tokens.at, table);
tokens.finish();
} catch (error) {
const { loc, pos } =
/** @type {{ loc?: { line: number, column: number }, pos: number }} */ (
error
);
if (!(error instanceof SyntaxError) || !loc) throw error;
// The message without the position webpack's parser appends to it.
const { message } = error;
throwParseError(
message.slice(0, message.lastIndexOf(` (${loc.line}:${loc.column})`)),
filename,
loc.line,
loc.column,
pos
);
}
return {
result,
table
};
};
/**
* terser's `parse` of one source into a `SyntaxTree` rather than terser's
* nodes, with the tokens and comments `parse` reads.
* @template T
* @param {string} text the source
* @param {ParseOptions} givenOptions terser's parse options
* @param {(tree: SyntaxTree, program: number, tokenAt: (pos: number, ending: boolean, forward?: number) => number | undefined, table: TokenTable) => T} readTokens what to read off the tree's tokens, before they are narrowed
* @returns {{ tree: SyntaxTree, program: number, tables: TokenTables, read: T, module: boolean }} the tree, its program, the tables its tokens' numbers are read through, what `readTokens` read, and whether it imports or exports
*/
parse.parseTree = (text, givenOptions, readTokens) => {
const options = defaults(givenOptions, PARSE_DEFAULTS, true);
const filename =
typeof options.filename === "string" ? options.filename : null;
const tables = new TokenTables();
const tree = new SyntaxTree();
let program = 0;
let module = false;
const { result: read, table } = readSource(
text,
options,
filename,
tables.nextBase(),
(parserOptions) => new TreeParser(parserOptions, text, tree),
(estree, tokenAt, ownTable) => {
module = estree.body.some((statement) =>
/^(?:Import|Export)/.test(statement.type)
);
program = tree.addEstree(estree);
return readTokens(tree, program, tokenAt, ownTable);
}
);
tables.add(table);
return { tree, program, tables, read, module };
};
/**
* terser's `parse`, reading the source with webpack's own parser. Its tree is
* terser's but for the parentheses a comment sits inside, which terser's own
* ESTree conversion drops too.
* @param {string} text the source
* @param {ParseOptions=} givenOptions terser's parse options
* @returns {Node} the tree
*/
parse.parse = (text, givenOptions) => {
const options = defaults(givenOptions, PARSE_DEFAULTS, true);
const filename =
typeof options.filename === "string" ? options.filename : null;
const previous = /** @type {Node} */ (options.toplevel);
// A file read onto another's tree numbers its tokens after that tree's.
/** @type {TokenTables} */
const tables =
(givenOptions &&
/** @type {Record<symbol, TokenTables | undefined>} */ (givenOptions)[
PARSE_ONTO_TABLES
]) ||
(isToplevelNode(previous) && previous.tokenTables
? previous.tokenTables
: new TokenTables());
const base = tables.nextBase();
const { result: toplevel, table } = readSource(
text,
options,
filename,
base,
(parserOptions, tokenAt, ownTable) =>
new PrinterParser(parserOptions, text, tokenAt, ownTable),
(program, tokenAt, ownTable) =>
ast.adoptEstree(program, tokenAt, text, ownTable)
);
tables.add(table);
if (isToplevelNode(previous)) {
previous.body = [
.../** @type {Node[]} */ (previous.body),
.../** @type {Node[]} */ (toplevel.body)
];
previous.endToken = toplevel.endToken;
previous.tokenTables = tables;
return previous;
}
toplevel.tokenTables = tables;
return toplevel;
};
};
// The alphabet terser names mangled identifiers from: a name opens with one of
// the first, and continues with those or a digit.
const IDENTIFIER_LEADING =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_";
const IDENTIFIER_DIGITS = "0123456789";
/**
* webpack's character counter for mangled names. terser's keys a `Map` by
* every character it is shown; this counts the ones a name can hold.
* @param {(items: string[], compare: (a: string, b: string) => number) => string[]} mergeSort terser's stable sort
* @returns {{ reset: () => void, consider: (str: string, delta: number) => void, sort: () => void, get: (num: number) => string }} the counter
*/
const createFrequency = (mergeSort) => {
const leading = [...IDENTIFIER_LEADING];
const digits = [...IDENTIFIER_DIGITS];
const counts = new Float64Array(128);
/** @type {string[]} */
let chars = [];
/**
* @param {string} a a character
* @param {string} b another
* @returns {number} which is counted more
*/
const compare = (a, b) => counts[b.charCodeAt(0)] - counts[a.charCodeAt(0)];
const frequency = {
reset() {
counts.fill(0);
},
/**
* @param {string} str text a name may be read against
* @param {number} delta how much each character counts
* @returns {void}
*/
consider(str, delta) {
for (let i = str.length; --i >= 0;) {
const code = str.charCodeAt(i);
if (code < 128) counts[code] += delta;
}
},
sort() {
chars = [...mergeSort(leading, compare), ...mergeSort(digits, compare)];
},
/**
* @param {number} num which name
* @returns {string} the name
*/
get(num) {
let name = "";
let base = 54;
num++;
do {
num--;
name += chars[num % base];
num = Math.floor(num / base);
base = 64;
} while (num > 0);
return name;
}
};
frequency.reset();
frequency.sort();
return frequency;
};
/**
* Installs webpack's character counter into terser's `base54`, the object
* `minify` hands the mangler, so its identity — which the `mangle` phase
* reads — is unchanged.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installFrequency = ({ scope, utils }) => {
Object.assign(scope.base54, createFrequency(mergeSort));
};
/**
* terser's compressor: the methods the phases call typed, the rest open.
* @typedef {{
* options: Partial<CompressOptions>,
* stack: NodeShape[],
* option<K extends keyof CompressOptions>(name: K): CompressOptions[K],
* has_directive(directive: string): NodeShape | true | undefined,
* parent(level?: number): NodeShape,
* self(): NodeShape,
* find_parent(test: NodeTest): NodeShape | undefined,
* in_boolean_context(): boolean | undefined,
* [key: string]: EXPECTED_ANY,
* }} CompressorShape
*/
/**
* terser's compress options, defaulted, as the compressor reads them.
* @internal
* @typedef {object} CompressOptions
* @property {boolean} arguments whether `arguments[i]` is read as the parameter
* @property {boolean} arrows whether functions become arrows
* @property {boolean} booleans whether booleans are simplified
* @property {boolean} booleans_as_integers whether booleans are written as 0 and 1
* @property {boolean} collapse_vars whether single-use variables are collapsed
* @property {boolean} comparisons whether comparisons are simplified
* @property {boolean} computed_props whether constant computed keys become plain
* @property {boolean} conditionals whether conditionals are simplified
* @property {boolean} dead_code whether unreachable code is dropped
* @property {boolean} defaults whether the options default to on
* @property {boolean} directives whether redundant directives are dropped
* @property {boolean | string[]} drop_console whether `console` calls are dropped, or which of its methods
* @property {boolean} drop_debugger whether `debugger` statements are dropped
* @property {number} ecma which edition the output may use
* @property {number} builtins_ecma which edition's builtins may be assumed
* @property {boolean} builtins_pure whether builtins are assumed unmodified
* @property {boolean} evaluate whether constant expressions are evaluated
* @property {boolean} expression whether the tree is one expression, its value kept
* @property {false | { [key: string]: EvaluatedValue | Node }} global_defs the globals replaced, by name, each any value a node is built from
* @property {boolean} hoist_funs whether function declarations are hoisted
* @property {boolean} hoist_props whether object properties are hoisted into variables
* @property {boolean} hoist_vars whether `var` declarations are hoisted
* @property {boolean} ie8 whether the output works around that engine
* @property {boolean} if_return whether `if`s returning are simplified
* @property {boolean | number} inline how far calls are inlined, `true` read as 3
* @property {boolean} join_vars whether neighboring declarations are joined
* @property {boolean | RegExp} keep_classnames which class names survive
* @property {boolean | "strict"} keep_fargs whether unused parameters are kept
* @property {boolean | RegExp} keep_fnames which function names survive
* @property {boolean} keep_infinity whether `Infinity` is kept as written
* @property {boolean} lhs_constants whether constants move to the left of comparisons
* @property {boolean} loops whether loops are simplified
* @property {boolean} module whether the source is a module
* @property {boolean} negate_iife whether an immediately called function is negated
* @property {number} passes how many times the compressor runs
* @property {boolean} properties whether property reads are rewritten as dots
* @property {boolean | "strict"} pure_getters whether reading a property has no side effect
* @property {string[] | ((node: Node) => boolean) | null} pure_funcs the calls assumed pure, or what answers whether one is not
* @property {boolean} pure_conversions whether conversions are assumed to have no side effect
* @property {boolean} pure_heritage whether a class's heritage is assumed a constructor
* @property {boolean} pure_new whether `new` of a pure constructor is pure
* @property {boolean} reduce_funcs whether single-use functions are inlined
* @property {boolean} reduce_vars whether variables assigned constants are replaced
* @property {boolean | number} sequences whether statements are joined into sequences, or how many
* @property {boolean} side_effects whether side-effect-free statements are dropped
* @property {boolean} switches whether `switch`es are simplified
* @property {string | string[] | RegExp | ((definition: Definition) => boolean) | null} top_retain the top-level names kept
* @property {boolean | string} toplevel whether top-level names may be dropped, or which kinds
* @property {boolean} typeofs whether `typeof` comparisons are simplified
* @property {boolean} unsafe whether unsafe transforms run
* @property {boolean} unsafe_arrows whether functions become arrows where `this` is unread
* @property {boolean} unsafe_comps whether comparisons are reversed unsafely
* @property {boolean} unsafe_Function whether `Function` calls are compressed
* @property {boolean} unsafe_math whether arithmetic is folded unsafely
* @property {boolean} unsafe_symbols whether symbol descriptions are dropped
* @property {boolean | RegExp} unsafe_methods whether methods become arrows, or which
* @property {boolean} unsafe_proto whether prototype reads are shortened
* @property {boolean} unsafe_regexp whether regular expressions in variables are inlined
* @property {boolean} unsafe_undefined whether `undefined` is replaced by a variable
* @property {boolean | "keep_assign"} unused whether unused names are dropped
* @property {boolean | "verbose"} warnings whether warnings are reported
*/
/** @typedef {CompressOptions[keyof CompressOptions]} CompressOptionValue any one compress option's value */
/**
* The methods the phases install into each compressor.
* @internal
* @typedef {object} CompressorMethods
* @property {TransformBefore} before what the compressor hands each node to on entering it
* @property {(this: Compressor, toplevel: Node) => Node} compress compresses a tree
* @property {<K extends keyof CompressOptions>(this: Compressor, name: K) => CompressOptions[K]} option an option's value
* @property {(this: Compressor, definition: Definition) => boolean} exposed whether code outside the output can read a variable
* @property {(this: Compressor) => boolean | undefined} in_boolean_context whether only the truthiness of the node visited is read
* @property {(this: Compressor, otherOperandMustBeNumber?: boolean) => boolean | undefined} in_32_bit_context whether the node visited is read as a 32-bit integer
* @property {(this: Compressor) => Node | undefined} get_toplevel the tree being compressed
* @property {(this: Compressor) => Node | undefined} is_lhs what the node visited's parent writes to, where it writes the node
* @property {(this: Compressor) => boolean} in_computed_key whether the node visited is an object property's key
* @property {(this: Compressor) => ScopeMangleOptions} mangle_options the options scopes are figured out with
*/
/**
* What the compressor holds besides its walk and its methods.
* @internal
* @typedef {object} CompressorFields
* @property {CompressOptions} options the options, defaulted
* @property {TokenTables} tokenTables what the tree's token numbers are read through
* @property {(node: Node) => boolean} pure_funcs whether a call may be dropped where its value is unread
* @property {((definition: Definition) => boolean) | undefined} top_retain whether a top-level definition is kept
* @property {{ funcs: boolean, vars: boolean }} toplevel whether top-level functions and variables may be dropped
* @property {number} sequences_limit how many expressions a sequence may join
* @property {Map<RegExpValue, RegExp | null>} evaluated_regexps each regular expression literal's value built, null where it throws
* @property {Node | undefined} _toplevel the tree being compressed
* @property {MangleOptions | false} _mangle_options the mangle options, defaulted, or false where names are kept
* @property {(globalName: string) => boolean} pure_access_globals which globals read without a side effect
* @property {(globalName: string) => boolean} is_pure_native_fn which globals call without a side effect
* @property {(globalName: string, name: PropertyKey) => boolean} is_pure_native_method which prototype methods call without a side effect
* @property {(globalName: string, name: PropertyKey) => boolean} is_pure_native_static_fn which static methods call without a side effect
* @property {(globalName: string, name: PropertyKey) => boolean} is_pure_native_static_property which static properties read without a side effect
*/
/**
* terser's `Compressor`: a transformer holding the compress options, with the
* methods the phases install.
* @internal
* @typedef {Walker & CompressorFields & CompressorMethods} Compressor
*/
/** @typedef {CompressorMethods[keyof CompressorMethods]} CompressorMethod one of the methods the phases install into each compressor */
/**
* terser's output stream, as webpack's `MinifiedOutput` implements it: what it
* has written, where it is, and the members terser's code generators call. It
* prints ESTree nodes, or a parsed tree's nodes by their index.
* @internal
* @template [N=PrintNode]
* @typedef {OutputStreamText & OutputStreamNodes<N>} OutputStream
*/
/**
* The members of an output stream that read the printed nodes, as methods so
* a helper taking either kind of node prints into either kind of stream.
* @internal
* @template N
* @typedef {{ nodeAccess: NodeAccess<N>, stack: N[], use_asm: N | null, active_scope: N | null, prepend_comments(node: N): void, append_comments(node: N, tail?: boolean): void, push_node(node: N): void, pop_node(): N, parent(n?: number): N | undefined }} OutputStreamNodes
*/
/**
* The members of an output stream that read no printed node.
* @internal
* @typedef {object} OutputStreamText
* @property {FormatSettings} options the format options, defaulted
* @property {boolean} readonly whether it is printed for its text alone, which drops every comment
* @property {CommentFilter} commentFilter which comments are printed
* @property {boolean} appendsComments whether comments after a node are printed
* @property {SourceMapBuilder | null} sourceMap where mappings are recorded, null for none
* @property {string[]} parts the text written, in parts not yet joined
* @property {number} partsLength the length of the parts
* @property {string} current the text written since the parts
* @property {((part: string) => void) | undefined} consumePart takes the parts before the last two in batches, where set
* @property {boolean} wroteComments whether a comment, shebang or preamble was written
* @property {number} currentColumn the column written to, from 0
* @property {number} currentLine the line written to, from 1
* @property {number} currentPosition the offset written to
* @property {boolean} hasParentheses whether the last thing printed opened a parenthesis
* @property {boolean} mightNeedSpace whether a space may be needed before what is printed next
* @property {boolean} mightNeedSemicolon whether a semicolon may be needed before what is printed next
* @property {boolean} needNewlineIndented whether a line break is needed before what is printed next
* @property {boolean} needSpace whether a space is needed before what is printed next
* @property {string} lastPrinted the last text printed
* @property {NodeToken | number | false | null | undefined} mappingToken the token the next text maps back to, or its number, false or nothing for none
* @property {TokenTables} tokenTables what the printed tree's token numbers are read through
* @property {string | false | undefined} mappingName the name the next mapping carries, false for none
* @property {boolean} in_directive whether a directive is printed
* @property {Set<Comment | Comment[]>} printed_comments the comments and comment lists printed so far
* @property {(str: string, identifier?: boolean, regexp?: boolean) => string} to_utf8 a string as the options let it print
* @property {(str: string, identifier?: boolean, regexp?: boolean) => string} identifierToUtf8 a name as the options let it print
* @property {(str: string, quote?: string) => string} encode_string a string's value written as a literal
* @property {(str: string, quote?: string) => string} makeString a string's value written as a literal
* @property {(str: string) => void} append writes text as it is
* @property {(index: number) => number} codeAt the code of a character written, by its offset
* @property {() => number} writtenLength how much was written
* @property {() => boolean} expectDirective whether what is printed next may read as a directive
* @property {() => boolean} hasNLB whether a line break comes last
* @property {(value: string | number) => void} print prints text, keeping the tokens apart
* @property {(token: NodeToken | number, name: string | false | undefined) => void} addMapping records a mapping
* @property {(token: NodeToken | number | null | undefined, name?: string | false) => void} add_mapping the token the next text maps back to, none for a node the compressor made
* @property {() => string} get what was written
* @property {() => string} toString what was written
* @property {() => void} indent writes nothing, as minified output does not indent
* @property {() => void} newline writes nothing, as minified output breaks no line
* @property {() => number} indentation 0, as minified output does not indent
* @property {() => number} current_width the column written to
* @property {() => number | boolean} should_break truthy once the line reaches `width`
* @property {() => boolean} has_parens whether the last thing printed opened a parenthesis
* @property {() => void} star prints `*`
* @property {() => void} space prints a space where one is needed
* @property {() => void} comma prints `,`
* @property {() => void} colon prints `:`
* @property {() => string} last the last text printed
* @property {() => void} semicolon prints `;` where one is needed
* @property {() => void} force_semicolon prints `;`
* @property {(name: string) => void} print_name prints an identifier
* @property {(str: string, quote?: string, escapeDirective?: boolean) => void} print_string prints a string's value as a literal
* @property {(str: string) => void} print_template_string_chars prints a template's text
* @property {() => number} next_indent 0, as minified output does not indent
* @property {<T>(column: number, content: () => T) => T} with_indent runs `content`, as minified output does not indent
* @property {<T>(content: () => T) => T} with_block prints braces around what `content` prints
* @property {<T>(content: () => T) => T} with_parens prints parentheses around what `content` prints
* @property {<T>(content: () => T) => T} with_square prints brackets around what `content` prints
* @property {<K extends keyof FormatSettings>(name: K) => FormatSettings[K]} option a format option's value
* @property {(scope: Node) => void} gc_scope frees a function just printed, where `_destroy_ast` asks
* @property {(comment: string) => string} filterComment what is printed of a comment's text
* @property {() => number} line the line written to
* @property {() => number} col the column written to
* @property {() => number} pos the offset written to
*/
/**
* terser's `format` options, defaulted, as the stream reads them.
* @internal
* @typedef {object} FormatSettings
* @property {boolean} ascii_only whether only ASCII is written
* @property {boolean} beautify whether the output is laid out to read, always false here
* @property {boolean} braces whether every body is braced, always false here
* @property {boolean | string | RegExp | ((node: CommentedNode, comment: KeptComment) => boolean)} comments which comments are kept
* @property {number} ecma which edition the output may use
* @property {boolean} ie8 whether the output works around that engine
* @property {number} indent_level how far a level indents, unread here
* @property {number} indent_start how far the output indents, unread here
* @property {boolean} inline_script whether `</script` is escaped
* @property {boolean} keep_numbers whether numbers are written as read
* @property {boolean} keep_quoted_props whether quoted keys stay quoted
* @property {number | false} max_line_len how long a line may be, unread here
* @property {string | null} preamble text written first
* @property {boolean} preserve_annotations whether annotation comments are kept
* @property {boolean} quote_keys whether every key is quoted
* @property {number} quote_style which quote strings are written with
* @property {boolean} safari10 whether the output works around that engine
* @property {boolean} semicolons whether statements end in semicolons rather than line breaks
* @property {boolean} shebang whether a shebang is kept
* @property {boolean | undefined} shorthand whether properties are written in shorthand, undefined until read from `ecma`
* @property {SourceMapBuilder | null} source_map where mappings are recorded
* @property {boolean} webkit whether the output works around that engine
* @property {number} width how wide a line may be, unread here
* @property {boolean} wrap_iife whether an immediately called function is parenthesized
* @property {boolean} wrap_func_args whether a function passed as an argument is parenthesized
* @property {boolean} _destroy_ast whether a function printed is freed
*/
/** @typedef {FormatSettings[keyof FormatSettings]} FormatSettingValue any one format option's value */
/** @typedef {(self: Node, compressor: Compressor) => Node} Optimize an optimizer, terser's `optimize` of the nodes it is named for */
/** @typedef {(self: Node, compressor: Compressor, depth: number, inChain?: boolean) => EvaluatedValue | Node} Evaluate a node's value, or the node where it has none known */
/** @typedef {{ enabled: boolean }} Corrections the switch the corrections read */
/**
* Runs `run` with some of the compressor's options off, so the transforms they
* gate are skipped for this node alone.
* @template T
* @param {Compressor} compressor the compressor
* @param {string[]} names the options
* @param {() => T} run what to run
* @returns {T} what it returned
*/
const withOptionsOff = (compressor, names, run) => {
const { options } =
/** @type {{ options: Record<string, CompressOptionValue> }} */ (
compressor
);
const saved = names.map((name) => options[name]);
for (const name of names) options[name] = false;
try {
return run();
} finally {
for (let i = 0; i < names.length; i++) options[names[i]] = saved[i];
}
};
/**
* Installs webpack's corrections to terser: each wraps the terser method
* producing a wrong output, and changes its answer only where it was wrong.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installCorrect = (modules) => {
const { ast } = modules;
const A = modules.ast;
const { is_empty: isEmpty, make_sequence: makeSequence } = modules.common;
/** @type {Corrections} */
const corrections = { enabled: true };
modules.corrections = corrections;
/**
* Wraps an optimizer, the correction applying only while corrections are on.
* @param {string} name the nodes it optimizes, by terser's class
* @param {(original: Optimize) => Optimize} wrap builds the replacement
* @returns {boolean} whether there was one to wrap
*/
const wrapOptimize = (name, wrap) =>
ast.wrapOptimizer(name, (original) => {
const corrected = wrap(original);
return (self, compressor) =>
corrections.enabled
? corrected(self, compressor)
: original(self, compressor);
});
// The operators converting both sides to numbers, which throw on a BigInt
// beside a Number, or on a Symbol.
const NUMERIC_OPERATORS = new Set([
"+",
"-",
"*",
"/",
"%",
"**",
"<<",
">>",
">>>",
"&",
"|",
"^"
]);
/**
* @param {Node} node an expression
* @param {string} name a global's name
* @returns {boolean} whether it calls that global, not as a constructor
*/
const callsGlobal = (node, name) =>
A.isCallNode(node) &&
!A.isNewNode(node) &&
A.isSymbolRefNode(node.callee) &&
node.callee.name === name &&
node.callee.definition.undeclared;
/**
* @param {Node} node an expression
* @returns {boolean} whether it is a BigInt literal, negated or wrapped by `Object`
*/
const isBigIntLiteral = (node) =>
A.isBigIntNode(node) ||
(A.isUnaryPrefixNode(node) &&
node.operator === "-" &&
A.isBigIntNode(node.argument)) ||
(callsGlobal(node, "Object") &&
node.arguments.length === 1 &&
isBigIntLiteral(node.arguments[0]));
/**
* Whether a constant operation throws instead of producing a value: a BigInt
* beside a Number, a BigInt divided by zero, or a Symbol converted.
* @param {Node} self an expression
* @param {Compressor} compressor the compressor
* @param {number=} depth how deep the evaluation is
* @returns {boolean} whether it throws
*/
const throwsForBigInt = (self, compressor, depth = 1) => {
if (self.operator === "+" && A.isUnaryPrefixNode(self)) {
return (
typeof ast.evaluateNode(self.argument, compressor, depth + 1) ===
"bigint"
);
}
if (!A.isBinaryNode(self) || !NUMERIC_OPERATORS.has(self.operator)) {
return false;
}
const { left, right, operator } = self;
if (callsGlobal(left, "Symbol") || callsGlobal(right, "Symbol")) {
return true;
}
const leftLiteral = isBigIntLiteral(left);
const rightLiteral = isBigIntLiteral(right);
// Only a literal makes the operation worth evaluating, or a remainder.
if (!leftLiteral && !rightLiteral && operator !== "%") return false;
const leftValue = ast.evaluateNode(left, compressor, depth + 1);
const rightValue = ast.evaluateNode(right, compressor, depth + 1);
const leftBigInt = leftLiteral || typeof leftValue === "bigint";
const rightBigInt = rightLiteral || typeof rightValue === "bigint";
if (leftBigInt && rightBigInt) {
if (operator === ">>>") return true;
return (
typeof rightValue === "bigint" &&
(operator === "/" || operator === "%"
? Number(rightValue) === 0
: operator === "**" && Number(rightValue) < 0)
);
}
if (leftBigInt === rightBigInt) return false;
const other = leftBigInt ? rightValue : leftValue;
// A string concatenates with `+`; an object may convert to a BigInt.
return (
other === null ||
typeof other === "number" ||
typeof other === "boolean" ||
typeof other === "undefined" ||
(typeof other === "string" && operator !== "+")
);
};
// The operators beside `+` converting both sides with ToNumeric.
const NUMERIC_ONLY_OPERATORS = new Set([
"-",
"*",
"/",
"%",
"**",
"<<",
">>",
">>>",
"&",
"|",
"^"
]);
// The operators yielding a boolean, whatever their sides are.
const RELATION_OPERATORS = new Set([
"==",
"!=",
"===",
"!==",
"<",
">",
"<=",
">=",
"in",
"instanceof"
]);
// The comparisons converting both sides to primitives, in their order.
const COMPARISON_OPERATORS = new Set(["<", ">", "<=", ">="]);
// The globals' properties holding a number, read-only.
const NUMBER_CONSTANTS = new Map([
[
"Math",
new Set(["E", "LN10", "LN2", "LOG10E", "LOG2E", "PI", "SQRT1_2", "SQRT2"])
],
[
"Number",
new Set([
"EPSILON",
"MAX_SAFE_INTEGER",
"MAX_VALUE",
"MIN_SAFE_INTEGER",
"MIN_VALUE",
"NaN",
"NEGATIVE_INFINITY",
"POSITIVE_INFINITY"
])
]
]);
/** @type {Set<string>} */
const EMPTY_NAMES = new Set();
// The keys through which an object literal converts its own way.
const OWN_CONVERSIONS = new Set(["toString", "valueOf", "__proto__"]);
/**
* The primitive an expression yields as its nodes tell, read a few levels
* deep: `numeric` is a number or a BigInt, `primitive` a string too.
* @param {Node} node an expression
* @param {number} depth how deep the read is
* @returns {"number" | "bigint" | "string" | "numeric" | "primitive" | undefined} undefined where it may be an object or a symbol
*/
const primitiveKind = (node, depth) => {
if (
A.isNumberNode(node) ||
ast.isBooleanNode(node) ||
ast.isNullNode(node) ||
ast.isUndefinedNode(node) ||
A.isNaNNode(node) ||
A.isInfinityNode(node)
) {
return "number";
}
if (A.isStringNode(node) || A.isTemplateStringNode(node)) return "string";
if (A.isBigIntNode(node)) return "bigint";
if (A.isUnaryPostfixNode(node)) return "numeric";
if (A.isUnaryPrefixNode(node)) {
switch (node.operator) {
case "typeof":
return "string";
case "!":
case "void":
case "delete":
case "+":
return "number";
case "-":
case "~": {
const kind =
depth < 3 ? convertedKind(node.argument, depth + 1) : undefined;
return kind === "bigint"
? "bigint"
: kind === "number" || kind === "string"
? "number"
: "numeric";
}
default:
return "numeric";
}
}
if (depth >= 3) return undefined;
if (
A.isDotNode(node) &&
A.isSymbolRefNode(node.object) &&
node.object.definition.undeclared &&
(NUMBER_CONSTANTS.get(node.object.name) || EMPTY_NAMES).has(
node.property.name
)
) {
return "number";
}
if (A.isSymbolRefNode(node)) {
const value = ast.fixedValue(node);
return value && A.isConstantNode(value) && !A.isRegExpNode(value)
? primitiveKind(value, depth + 1)
: undefined;
}
if (A.isSequenceNode(node)) {
return primitiveKind(ast.tailNode(node), depth + 1);
}
if (A.isConditionalNode(node)) {
const kind = primitiveKind(
/** @type {Node} */ (node.consequent),
depth + 1
);
return kind === primitiveKind(node.alternate, depth + 1)
? kind
: undefined;
}
if (!A.isBinaryNode(node)) return undefined;
const { operator } = node;
if (A.isAssignNode(node)) {
return operator === "="
? primitiveKind(node.right, depth + 1)
: undefined;
}
if (RELATION_OPERATORS.has(operator)) return "number";
if (operator === "&&" || operator === "||" || operator === "??") {
const kind = primitiveKind(node.left, depth + 1);
return kind === primitiveKind(node.right, depth + 1) ? kind : undefined;
}
const left = convertedKind(node.left, depth + 1);
const right = convertedKind(node.right, depth + 1);
if (operator === "+") {
if (left === "string" || right === "string") return "string";
return left === right && (left === "number" || left === "bigint")
? left
: "primitive";
}
if (operator === ">>>") return "number";
return becomesNumber(left) && becomesNumber(right)
? "number"
: left === "bigint" && right === "bigint"
? "bigint"
: "numeric";
};
/**
* @param {string | undefined} kind a primitive kind
* @returns {boolean} whether ToNumeric makes it a number
*/
const becomesNumber = (kind) => kind === "number" || kind === "string";
/**
* Whether a literal converts with the builtins alone: an array of what so
* converts, or an object with no `toString`, `valueOf` or prototype of its own.
* @param {Node} node an expression
* @param {number} depth how deep the read is
* @returns {boolean} true for `[]`, `[1]` or `{ a: f }`
*/
const convertsWithBuiltins = (node, depth) => {
if (depth >= 3) return false;
if (A.isArrayNode(node)) {
return node.elements.every(
(/** @type {Node | null} */ element) =>
element === null ||
(!A.isExpansionNode(element) &&
convertedKind(element, depth + 1) !== undefined)
);
}
if (!A.isObjectNode(node)) return false;
return node.properties.every((/** @type {Node} */ property) => {
if (A.isExpansionNode(property)) {
const spread = property.argument;
return (
A.isArrayNode(spread) ||
A.isStringNode(spread) ||
A.isTemplateStringNode(spread) ||
convertsWithBuiltins(spread, depth + 1)
);
}
return !property.computed && !OWN_CONVERSIONS.has(property.key.name);
});
};
/**
* The primitive an expression converts to, as oxc reads it: a literal not
* overriding a conversion converts to a string.
* @param {Node} node an expression
* @param {number} depth how deep the read is
* @returns {"number" | "bigint" | "string" | "numeric" | "primitive" | undefined} undefined where converting may run code or throw
*/
const convertedKind = (node, depth) =>
A.isRegExpNode(node) || convertsWithBuiltins(node, depth)
? "string"
: primitiveKind(node, depth);
/**
* Whether an operation converts a side that may be an object or a symbol,
* running its `valueOf` or `toString` or throwing, as oxc reads it.
* @param {Node} self an expression
* @returns {boolean} true for `o + 1`, `x - 1`, `-x` or `${o}`
*/
const convertsUnknown = (self) => {
if (A.isTemplateStringNode(self)) {
const { segments } = self;
for (let i = 1; i < segments.length; i += 2) {
if (convertedKind(segments[i], 0) === undefined) return true;
}
return false;
}
if (A.isUnaryPrefixNode(self)) {
if (self.operator === "-" || self.operator === "~") {
return convertedKind(self.argument, 0) === undefined;
}
if (self.operator !== "+") return false;
const kind = convertedKind(self.argument, 0);
return kind !== "number" && kind !== "string";
}
if (!A.isBinaryNode(self) || A.isAssignNode(self)) return false;
const { operator } = self;
const isPlus = operator === "+";
if (!isPlus && !NUMERIC_ONLY_OPERATORS.has(operator)) return false;
const left = convertedKind(self.left, 0);
const right = convertedKind(self.right, 0);
if (isPlus && left === "string") return right === undefined;
if (isPlus && right === "string") return left === undefined;
return !(
(isPlus
? left === "number" && right === "number"
: becomesNumber(left) && becomesNumber(right)) ||
(left === "bigint" && right === "bigint")
);
};
/**
* @param {Node} node a node
* @returns {boolean} whether it is a string literal
*/
const isStringLiteral = (node) => A.isStringNode(node);
/**
* A concatenation yielding a string, its literals emptied: what converts
* is left to convert, in the same order.
* @param {Node} self an expression
* @returns {Node} `"" + b + d` for `"a" + b + "c" + d`
*/
const emptyLiterals = (self) => {
if (
!A.isBinaryNode(self) ||
A.isAssignNode(self) ||
self.operator !== "+" ||
convertedKind(self, 0) !== "string"
) {
return self;
}
const left = isStringLiteral(self.left)
? makeNode(A.StringNode, self.left, { value: "" })
: emptyLiterals(self.left);
const right = isStringLiteral(self.right)
? makeNode(A.StringNode, self.right, { value: "" })
: emptyLiterals(self.right);
if (isStringLiteral(right) && convertedKind(left, 0) === "string") {
return left;
}
if (isStringLiteral(left) && convertedKind(right, 0) === "string") {
return right;
}
return left === self.left && right === self.right
? self
: makeNode(A.BinaryNode, self, { operator: "+", left, right });
};
/**
* What of a template converting unknowns is left to run: those conversions,
* in templates of their own, and the other substitutions' effects.
* @param {Node} self a template literal
* @param {Compressor} compressor the compressor
* @returns {Node} `${b}${d}` for `a${b}c${d}e`
*/
const keepConverting = (self, compressor) => {
/** @type {Node[]} */
const expressions = [];
/** @type {Node | undefined} */
let template;
const { segments } = self;
for (let i = 1; i < segments.length; i += 2) {
const segment = segments[i];
const empty = makeNode(A.TemplateSegmentNode, segments[i + 1], {
value: "",
raw: ""
});
if (convertedKind(segment, 0) === undefined) {
if (template === undefined) {
template = makeNode(A.TemplateStringNode, self, {
segments: [
makeNode(A.TemplateSegmentNode, segments[i - 1], {
value: "",
raw: ""
})
]
});
expressions.push(template);
}
template.segments.push(segment, empty);
continue;
}
const kept = ast.dropSideEffectFree(segment, compressor);
if (kept) {
expressions.push(kept);
template = undefined;
}
}
return makeSequence(self, expressions);
};
// These exceptions belong to the running program, including when nobody
// reads the result; evaluating or dropping the operation loses its throw.
for (const type of ["UnaryExpression", "BinaryExpression"]) {
ast.wrapEvaluate(type, (/** @type {Evaluate} */ original) =>
/** @type {Evaluate} */
(self, compressor, depth, inChain) => {
if (!corrections.enabled) {
return original(self, compressor, depth, inChain);
}
// Folding the constant throws where the program would: keep it
// unevaluated, rather than checking first and evaluating twice.
try {
return original(self, compressor, depth, inChain);
} catch (error) {
if (error instanceof TypeError || error instanceof RangeError) {
return self;
}
throw error;
}
}
);
ast.wrapDropSideEffectFree(
type,
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) =>
corrections.enabled &&
(throwsForBigInt(self, compressor) ||
convertsUnknown(self) ||
comparesUnknown(self, compressor))
? emptyLiterals(self)
: original(self, compressor, firstInStatement)
);
}
/**
* Whether an evaluated key may convert to `"prototype"`; an object may, its
* conversion running code, and a node of no known value is taken not to.
* @param {EvaluatedValue | Node} value what the key evaluated to
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it may
*/
const mayKeyPrototype = (value, compressor) => {
if (A.isSymbolRefNode(value)) {
const fixed = ast.fixedValue(/** @type {Node} */ (value));
if (A.isArrayNode(fixed) || A.isObjectNode(fixed)) value = fixed;
}
// An array converts to its elements joined by commas, so only one of a
// single element can, as that element would.
if (A.isArrayNode(value)) {
const [element] = /** @type {Node} */ (value).elements;
return (
/** @type {Node} */ (value).elements.length === 1 &&
element !== null &&
!A.isExpansionNode(element) &&
mayKeyPrototype(ast.evaluateNode(element, compressor, 1), compressor)
);
}
if (Array.isArray(value)) {
return value.length === 1 && mayKeyPrototype(value[0], compressor);
}
if (A.isSyntaxNode(value)) return A.isObjectNode(value);
return (
(typeof value === "object" && value !== null) ||
String(value) === "prototype"
);
};
/**
* @param {Node} key a computed key of no known value
* @returns {boolean} whether it is a number or a well-known symbol, never `"prototype"`
*/
const neverKeysPrototype = (key) => {
if (
A.isDotNode(key) &&
A.isSymbolRefNode(key.object) &&
key.object.name === "Symbol" &&
key.object.definition.undeclared
) {
return true;
}
const kind = primitiveKind(key, 0);
return kind === "number" || kind === "bigint";
};
/**
* Whether a class member is a static `prototype`, which throws a TypeError
* as the class is defined; a computed key of no known value may be one.
* @param {Node} self a node
* @param {Compressor} compressor the compressor
* @returns {boolean} whether defining it throws
*/
const definesStaticPrototype = (self, compressor) => {
if (self.static !== true) return false;
const value = ast.evaluateNode(self.key, compressor, 1);
return self.computed && A.isSyntaxNode(value) && !A.isObjectNode(value)
? !neverKeysPrototype(value)
: mayKeyPrototype(value, compressor);
};
ast.wrapDropSideEffectFree(
"TemplateLiteral",
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) =>
corrections.enabled && convertsUnknown(self)
? keepConverting(self, compressor)
: original(self, compressor, firstInStatement)
);
// An optional read runs a getter as a plain one does, where `pure_getters` is off.
ast.wrapDropSideEffectFree(
"MemberExpression",
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) =>
corrections.enabled &&
self.optional &&
!compressor.option("pure_getters") &&
ast.mayThrowOnAccess(self.object, compressor)
? self
: original(self, compressor, firstInStatement)
);
/**
* Whether a comparison converts what may be an object or a symbol, where
* `pure_conversions` is off; terser assumes it runs no code.
* @param {Node} self an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `0 == o` or `a < b`
*/
const comparesUnknown = (self, compressor) => {
if (
!A.isBinaryNode(self) ||
A.isAssignNode(self) ||
compressor.option("pure_conversions")
) {
return false;
}
const { operator, left, right } = self;
if (operator === "==" || operator === "!=") {
// Nothing equals `null` loosely but `undefined`, with no conversion.
if (ast.isNullNode(left) || ast.isNullNode(right)) return false;
if (ast.isUndefinedNode(left) || ast.isUndefinedNode(right)) return false;
} else if (!COMPARISON_OPERATORS.has(operator)) {
return false;
}
return (
convertedKind(left, 0) === undefined ||
convertedKind(right, 0) === undefined
);
};
/**
* Whether a member's computed key converts what may be an object, where
* `pure_conversions` is off; a symbol is a key as it is.
* @param {Node} self a class member or an object property
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `[o]` in `{ [o]: 1 }`
*/
const keysUnknown = (self, compressor) =>
self.computed === true &&
!compressor.option("pure_conversions") &&
convertedKind(self.key, 0) === undefined &&
!neverKeysPrototype(self.key);
/**
* Whether a computed read converts a key that may be an object, where
* `pure_conversions` is off; terser drops the read with its conversion.
* @param {Node} self an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `a[o]`
*/
const readsKeyUnknown = (self, compressor) =>
A.isSubNode(self) &&
!compressor.option("pure_conversions") &&
convertedKind(self.property, 0) === undefined;
ast.wrapDropSideEffectFree(
"MemberExpression",
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) =>
corrections.enabled && readsKeyUnknown(self, compressor)
? self
: original(self, compressor, firstInStatement)
);
for (const name of /** @type {("hasSideEffects" | "mayThrow")[]} */ ([
"hasSideEffects",
"mayThrow"
])) {
const original = ast[name];
/**
* @param {Node} self an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} whether the expression has effects or may throw
*/
ast[name] = (self, compressor) =>
original(self, compressor) ||
(corrections.enabled &&
(throwsForBigInt(self, compressor) ||
convertsUnknown(self) ||
comparesUnknown(self, compressor) ||
keysUnknown(self, compressor) ||
readsKeyUnknown(self, compressor) ||
definesStaticPrototype(self, compressor)));
}
for (const type of ["ClassDeclaration", "ClassExpression"]) {
ast.wrapDropSideEffectFree(
type,
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) =>
corrections.enabled &&
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body).some(
(/** @type {Node} */ member) =>
definesStaticPrototype(member, compressor) ||
keysUnknown(member, compressor)
)
? self
: original(self, compressor, firstInStatement)
);
}
// A string beside `+` makes it truthy, yet the other side still converts.
wrapOptimize(
"Binary",
(original) => (self, compressor) =>
self.operator === "+" && convertsUnknown(self)
? withOptionsOff(compressor, ["booleans"], () =>
original(self, compressor)
)
: original(self, compressor)
);
// Moving `+` into a BigInt operation can throw before its operands run.
wrapOptimize(
"UnaryPrefix",
(original) => (self, compressor) =>
self.operator === "+" && ast.isBigInt(self.argument, compressor)
? self
: original(self, compressor)
);
// The ES2015 constructors with an object `prototype` and the inherited
// `Symbol.hasInstance`: oxc's list, less `Proxy` and what later added.
const INSTANCEOF_CONSTRUCTORS = new Set([
"Array",
"ArrayBuffer",
"Boolean",
"DataView",
"Date",
"Error",
"EvalError",
"Float32Array",
"Float64Array",
"Function",
"Int16Array",
"Int32Array",
"Int8Array",
"Map",
"Number",
"Object",
"Promise",
"RangeError",
"ReferenceError",
"RegExp",
"Set",
"String",
"Symbol",
"SyntaxError",
"TypeError",
"URIError",
"Uint16Array",
"Uint32Array",
"Uint8Array",
"Uint8ClampedArray",
"WeakMap",
"WeakSet"
]);
/**
* Whether a value is an object no proxy's trap sits behind: a literal
* array, function, class extending nothing, or object setting no prototype.
* @param {Node} node an expression
* @returns {boolean} true for `[]`, `{}`, `() => {}` and `class {}`
*/
const isOrdinaryObject = (node) =>
A.isArrayNode(node) ||
A.isLambdaNode(node) ||
(A.isClassNode(node) && !node.superClass) ||
(A.isObjectNode(node) && !node.properties.some(setsPrototype));
/**
* Whether a global names a built-in the program never assigns and no
* `with` or direct `eval` can rebind; `reduce_vars` counts the assignments.
* @param {Node} node an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `Object` read where nothing shadows it
*/
const isBuiltInConstructor = (node, compressor) => {
if (
!A.isSymbolRefNode(node) ||
!node.definition.undeclared ||
node.definition.assignments !== 0 ||
!compressor.option("reduce_vars") ||
!INSTANCEOF_CONSTRUCTORS.has(node.name)
) {
return false;
}
for (
let scope = node.scope;
scope;
scope = /** @type {Scope} */ (scope.upper)
) {
if (scope.isPinned()) return false;
}
return true;
};
/**
* Whether a relation runs code of its right side's or throws: `instanceof`
* reads `Symbol.hasInstance` and `prototype` and walks a proxy's prototypes,
* `in` reaches a proxy's `has`, and either throws on a right side no object.
* @param {Node} self an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `a instanceof b` and `a in b`
*/
const runsRelation = (self, compressor) =>
self.operator === "in" ||
(self.operator === "instanceof" &&
!(
isBuiltInConstructor(self.right, compressor) &&
(A.isConstantNode(self.left) || isOrdinaryObject(self.left))
));
// terser reads both as pure, and drops one nobody reads.
for (const wrapAnswer of [ast.wrapHasSideEffects, ast.wrapMayThrow]) {
wrapAnswer(
"BinaryExpression",
(
/** @type {(self: Node, compressor: Compressor) => boolean} */ original
) =>
(/** @type {Node} */ self, /** @type {Compressor} */ compressor) =>
original(self, compressor) ||
(corrections.enabled && runsRelation(self, compressor))
);
}
ast.wrapDropSideEffectFree(
"BinaryExpression",
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) =>
corrections.enabled && runsRelation(self, compressor)
? self
: original(self, compressor, firstInStatement)
);
/**
* Whether spreading a list element runs an iterator the program can observe,
* as a generator's body; an array literal's or a string's cannot be.
* @param {Node | null} element an array element or call argument
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `...it` spreading anything else
*/
const runsIterator = (element, compressor) =>
element !== null &&
A.isExpansionNode(element) &&
!A.isArrayNode(element.argument) &&
!ast.isString(element.argument, compressor) &&
!isArgumentsObject(element.argument);
/**
* @param {Node} node an expression
* @returns {boolean} whether it is a function's own `arguments` object, never reassigned
*/
const isArgumentsObject = (node) => {
if (!A.isSymbolRefNode(node) || node.name !== "arguments") return false;
const { definition } = node;
if (
!definition ||
definition.identifiers.length !== 1 ||
definition.assignments
) {
return false;
}
const lambda = definition.scope.node;
// The object's symbol starts where its function does, as no parameter can.
return (
A.isLambdaNode(lambda) &&
!A.isArrowNode(lambda) &&
definition.identifiers[0].startToken === lambda.startToken
);
};
A.isArgumentsObject = isArgumentsObject;
/**
* @param {Node} node a callee
* @returns {boolean} whether a link of it is optional, which may skip the arguments
*/
const hasOptionalLink = (node) =>
(A.isPropAccessNode(node) || A.isCallNode(node)) &&
(node.optional ||
hasOptionalLink(A.isCallNode(node) ? node.callee : node.object));
/**
* What of a list is left to run: each element's effects, and the spreads
* that iterate, kept as they are.
* @param {Node} self the array or call
* @param {(Node | null)[]} list its elements or arguments
* @param {Compressor} compressor the compressor
* @returns {Node | null} an array of what is left, null where it is the list unchanged
*/
const keepIterating = (self, list, compressor) => {
/** @type {Node[]} */
const elements = [];
let changed = false;
for (const element of list) {
// A hole is dropped, as `trim` drops it.
const kept =
element === null
? null
: runsIterator(element, compressor)
? element
: ast.dropSideEffectFree(element, compressor);
if (kept !== element || element === null) changed = true;
if (kept) elements.push(kept);
}
return changed ? makeNode(A.ArrayNode, self, { elements }) : null;
};
/**
* Whether a property is an accessor that throws in strict code: a strict
* `arguments` object's `callee`, or a function's `caller` or `arguments`.
* @param {Node} self a property read or assignment target
* @returns {boolean} true for one
*/
const isPoisonedProperty = (self) => {
if (!A.isDotNode(self)) return false;
const { name } = self.property;
const { object } = self;
if (name === "callee") {
return A.isSymbolRefNode(object) && object.name === "arguments";
}
if (name !== "caller" && name !== "arguments") return false;
const value = A.isSymbolRefNode(object) ? ast.fixedValue(object) : object;
return Boolean(value) && (A.isLambdaNode(value) || A.isClassNode(value));
};
/**
* Whether writing to a target runs code even where nothing reads it: a key
* that may convert an object, or a property of a primitive, which a setter
* on its prototype receives.
* @param {Node} target an assignment's target
* @returns {boolean} true for `b[o] = 1` or `0..x = 1`
*/
const writesObservably = (target) =>
A.isPropAccessNode(target) &&
((A.isSubNode(target) && convertedKind(target.property, 0) === undefined) ||
primitiveKind(target.object, 0) !== undefined ||
callsGlobal(target.object, "Symbol"));
// A literal handed to a call, as to `Object.defineProperty`, may have gained
// a getter there, which terser's reading of the literal does not see.
const dotThrows = ast.dotThrows;
/**
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {boolean} whether reading a property of its value can throw
*/
ast.dotThrows = (node, compressor) => {
if (dotThrows(node, compressor)) return true;
if (!corrections.enabled || !A.isSymbolRefNode(node)) return false;
const { definition } = node;
if (!definition || !definition.leaked) return false;
const value = ast.fixedValue(node);
return Boolean(
value &&
(A.isObjectNode(value) ||
A.isArrayNode(value) ||
A.isLambdaNode(value) ||
A.isClassNode(value))
);
};
/**
* Whether the node visited sits in a derived class's constructor, past
* arrows only, where reading `this` before `super()` throws.
* @param {Compressor} compressor the compressor
* @returns {boolean} true there
*/
const inDerivedConstructor = (compressor) => {
let parent;
let viaArrow = false;
for (let i = 0; (parent = compressor.parent(i)); i++) {
if (A.isArrowNode(parent)) viaArrow = true;
if (!A.isLambdaNode(parent) || A.isArrowNode(parent)) continue;
const method = compressor.parent(i + 1);
if (!method || method.kind !== "constructor") return false;
for (let j = i + 2; (parent = compressor.parent(j)); j++) {
if (A.isClassNode(parent)) {
return (
Boolean(parent.superClass) &&
(viaArrow || !superCalledBefore(compressor, i))
);
}
}
return false;
}
return false;
};
/**
* Whether the constructor at a parent level has run `super()` before the
* node visited: in a statement of its body, or a sequence, ahead of it.
* @param {Compressor} compressor the compressor
* @param {number} level how far up the constructor's function is
* @returns {boolean} true when `this` is bound by then
*/
const superCalledBefore = (compressor, level) => {
if (modules.improvements === undefined || !modules.improvements.enabled) {
return false;
}
const callsSuper = (/** @type {Node} */ node) =>
A.isCallNode(node) && A.isSuperNode(node.callee);
let child = compressor.self();
for (let i = 0; i < level + 1; i++) {
const parent = compressor.parent(i);
const body = A.isSequenceNode(parent)
? parent.expressions
: A.bodyOf(parent);
/** @type {Node[] | undefined} */
const list = Array.isArray(body) ? body : undefined;
if (list !== undefined) {
const index = list.indexOf(child);
for (let k = 0; k < index; k++) {
const earlier = list[k];
if (
callsSuper(earlier) ||
(A.isSimpleStatementNode(earlier) &&
(callsSuper(earlier.expression) ||
(A.isSequenceNode(earlier.expression) &&
earlier.expression.expressions.some(callsSuper))))
) {
return true;
}
}
}
child = parent;
}
return false;
};
// terser reads `this` as pure, where a derived constructor's throws early.
for (const wrapAnswer of [ast.wrapHasSideEffects, ast.wrapMayThrow]) {
wrapAnswer(
"ThisExpression",
(
/** @type {(self: Node, compressor: Compressor) => boolean} */ original
) =>
(/** @type {Node} */ self, /** @type {Compressor} */ compressor) =>
original(self, compressor) ||
(corrections.enabled && inDerivedConstructor(compressor))
);
}
ast.wrapDropSideEffectFree(
"ThisExpression",
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) =>
corrections.enabled && inDerivedConstructor(compressor)
? self
: original(self, compressor, firstInStatement)
);
/**
* Whether a name is read where a `with` object answers first, through a
* getter or a proxy's `has` trap.
* @param {Node} self a node
* @returns {boolean} true for a reference in a scope a `with` reaches
*/
const readsThroughWith = (self) =>
A.isSymbolRefNode(self) && Boolean(self.scope && self.scope.usesWith);
for (const wrapAnswer of [ast.wrapHasSideEffects, ast.wrapMayThrow]) {
wrapAnswer(
"Identifier",
(
/** @type {(self: Node, compressor: Compressor) => boolean} */ original
) =>
(/** @type {Node} */ self, /** @type {Compressor} */ compressor) =>
original(self, compressor) ||
(corrections.enabled && readsThroughWith(self))
);
}
/**
* Whether a read of a lexical binding may run before its declaration, which
* throws: it comes first in the source, or sits in its own loop's head.
* @param {Node} self a read
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `x` in `{ x; let x; }`
*/
const readsBeforeDeclaration = (self, compressor) => {
if (!A.isSymbolRefNode(self) || !self.definition) return false;
const declaration = self.definition.identifiers[0];
if (
!declaration ||
!(
ast.isSymbolLetNode(declaration) ||
ast.isSymbolConstNode(declaration) ||
ast.isSymbolUsingNode(declaration) ||
ast.isSymbolDefClassNode(declaration)
)
) {
return false;
}
if (
self.startToken &&
declaration.startToken &&
tokenPrecedes(self.startToken, declaration.startToken)
) {
return true;
}
let child = compressor.self();
for (let i = 0, parent; (parent = compressor.parent(i)); i++) {
if (
A.isForInNode(parent) &&
parent.right === child &&
// Inlining can copy the declaration node while keeping its definition.
A.walk(parent.left, (/** @type {Node} */ node) =>
A.isSymbolDeclarationNode(node) && node.definition === self.definition
? A.walk_abort
: undefined
)
) {
return true;
}
child = parent;
}
return false;
};
ast.wrapDropSideEffectFree(
"Identifier",
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) =>
corrections.enabled &&
(readsThroughWith(self) || readsBeforeDeclaration(self, compressor))
? self
: original(self, compressor, firstInStatement)
);
// terser drops `typeof x` as never throwing, which holds only before no declaration.
wrapOptimize(
"UnaryPrefix",
(original) => (self, compressor) =>
self.operator === "typeof" &&
readsBeforeDeclaration(self.argument, compressor)
? self
: original(self, compressor)
);
ast.wrapDropSideEffectFree(
"UnaryExpression",
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) =>
corrections.enabled &&
self.operator === "typeof" &&
readsBeforeDeclaration(self.argument, compressor)
? self
: original(self, compressor, firstInStatement)
);
// terser reads `arguments` and functions as holding no getters.
for (const wrapAnswer of [ast.wrapHasSideEffects, ast.wrapMayThrow]) {
wrapAnswer(
"MemberExpression",
(
/** @type {(self: Node, compressor: Compressor) => boolean} */ original
) =>
(/** @type {Node} */ self, /** @type {Compressor} */ compressor) =>
original(self, compressor) ||
(corrections.enabled && isPoisonedProperty(self))
);
}
for (const type of ["MemberExpression", "AssignmentExpression"]) {
ast.wrapDropSideEffectFree(
type,
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) =>
corrections.enabled &&
(type === "MemberExpression"
? isPoisonedProperty(self)
: isPoisonedProperty(self.left) || writesObservably(self.left))
? self
: original(self, compressor, firstInStatement)
);
}
// An async generator awaits a returned value, `undefined` included, where
// a bare `return` does not: dropping the value takes a tick out.
wrapOptimize("Return", (original) => (self, compressor) => {
const lambda = compressor.find_parent(A.isLambdaNode);
return lambda && lambda.async && lambda.generator
? self
: original(self, compressor);
});
// The same tick parts `if (a) return b; else return;` from `return a ? b : void 0`.
wrapOptimize("If", (original) => (self, compressor) => {
const lambda = compressor.find_parent(A.isLambdaNode);
return lambda &&
lambda.async &&
lambda.generator &&
A.isReturnNode(self.consequent) &&
A.isReturnNode(self.alternate) &&
!(/** @type {Node} */ (self.consequent).argument) !==
!self.alternate.argument
? self
: original(self, compressor);
});
/**
* Whether spreading a value may run code as it is copied: a getter, or a
* proxy's traps; an array, a string, a function or a getter-free literal cannot.
* @param {Node} spread what is spread
* @returns {boolean} true for `p` in `{ ...p }`
*/
const spreadRunsCode = (spread) =>
!A.isArrayNode(spread) &&
!A.isStringNode(spread) &&
!A.isTemplateStringNode(spread) &&
!A.isLambdaNode(spread) &&
!(
A.isObjectNode(spread) &&
spread.properties.every(
(/** @type {Node} */ inner) =>
A.isObjectKeyValNode(inner) ||
A.isConciseMethodNode(inner) ||
ast.isObjectSetterNode(inner)
)
);
/**
* @param {Node} self an object literal
* @returns {boolean} whether it spreads what may run code as it is copied
*/
const spreadsUnknown = (self) =>
self.properties.some(
(/** @type {Node} */ property) =>
A.isExpansionNode(property) && spreadRunsCode(property.argument)
);
/**
* What of an object literal is left to run: each property's effects, and
* the spreads that may run code, kept in literals of their own.
* @param {Node} self an object literal
* @param {Compressor} compressor the compressor
* @returns {Node} the expressions left, in order
*/
const keepSpreading = (self, compressor) => {
/** @type {Node[]} */
const expressions = [];
/** @type {Node | undefined} */
let spreads;
for (const property of self.properties) {
if (A.isExpansionNode(property) && spreadRunsCode(property.argument)) {
if (spreads === undefined) {
spreads = makeNode(A.ObjectNode, property, { properties: [] });
expressions.push(spreads);
}
spreads.properties.push(property);
continue;
}
const kept = ast.dropSideEffectFree(property, compressor);
if (kept) {
expressions.push(kept);
spreads = undefined;
}
}
return makeSequence(self, expressions);
};
/**
* Whether a property names an anonymous class whose definition runs code,
* which reads that name; under `keep_classnames`, the name has to stay.
* @param {Node} property an object property
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `default: class { static f = this.name }`
*/
const namesClassKept = (property, compressor) =>
A.isObjectKeyValNode(property) &&
!property.computed &&
A.isClassNode(property.value) &&
!(/** @type {Node} */ (property.value).id) &&
Boolean(compressor.option("keep_classnames")) &&
ast.hasSideEffects(/** @type {Node} */ (property.value), compressor);
// terser copies a spread object as reading its properties, which it holds pure.
for (const wrapAnswer of [ast.wrapHasSideEffects, ast.wrapMayThrow]) {
wrapAnswer(
"ObjectExpression",
(
/** @type {(self: Node, compressor: Compressor) => boolean} */ original
) =>
(/** @type {Node} */ self, /** @type {Compressor} */ compressor) =>
original(self, compressor) ||
(corrections.enabled && spreadsUnknown(self))
);
}
ast.wrapDropSideEffectFree(
"ObjectExpression",
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) =>
corrections.enabled &&
self.properties.some(
(/** @type {Node} */ property) =>
keysUnknown(property, compressor) ||
namesClassKept(property, compressor)
)
? self
: corrections.enabled && spreadsUnknown(self)
? keepSpreading(self, compressor)
: original(self, compressor, firstInStatement)
);
// terser drops a spread for what it spreads, which skips the iteration.
for (const type of ["ArrayExpression", "CallExpression", "NewExpression"]) {
ast.wrapDropSideEffectFree(
type,
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) => {
const result = original(self, compressor, firstInStatement);
if (!corrections.enabled || result === self) return result;
const isArray = A.isArrayNode(self);
const list = isArray ? self.elements : self.arguments;
if (
!list.some((/** @type {Node | null} */ element) =>
runsIterator(element, compressor)
) ||
(!isArray && hasOptionalLink(self))
) {
return result;
}
if (isArray) return keepIterating(self, list, compressor) || self;
const pureCallee = ast.isCalleePure(self, compressor);
if (!pureCallee && !ast.isCallPure(self.callee, compressor)) {
return result;
}
const kept =
keepIterating(self, list, compressor) ||
makeNode(A.ArrayNode, self, { elements: list });
if (pureCallee) return kept;
// A pure method keeps what its object runs, ahead of the arguments.
const object = ast.dropSideEffectFree(self.callee.object, compressor);
return object ? makeSequence(self, [object, kept]) : kept;
}
);
}
/**
* Whether a property is `__proto__` written as a shorthand, an own property
* rather than the prototype: its one token is both its start and its end.
* @param {Node} property an object literal's property
* @returns {boolean} true for `{ __proto__ }`
*/
const isShorthandProto = (property) =>
A.terserKey(property.key) === "__proto__" &&
Boolean(property.startToken) &&
property.startToken === property.endToken;
/**
* Whether a property sets the object's prototype: `__proto__: value`.
* @param {Node} property an object literal's property
* @returns {boolean} true for a prototype setter
*/
const setsPrototype = (property) =>
A.isObjectKeyValNode(property) &&
A.terserKey(property.key) === "__proto__" &&
!isShorthandProto(property);
// An assignment runs a setter a computed accessor or the prototype may hold
// for its key, where a property written into the literal defines it.
modules.webpackCollapse.keepsAssignmentsOut = (literal) =>
corrections.enabled &&
literal.properties.some(
(/** @type {Node} */ property) =>
setsPrototype(property) ||
(property.computed &&
(property.kind === "get" || property.kind === "set"))
);
const estreeCodegen = modules.estreeCodegen;
const estreeNeedsParens = modules.estreeNeedsParens;
const kindOf = modules.kindOf;
/**
* @param {PrintNode} node a node
* @param {OutputStream} output the stream printed into
* @returns {boolean} whether it is printed as the right side of `#x in`
*/
const isPrivateInRight = (node, output) => {
const parent = output.parent();
return (
corrections.enabled &&
parent !== undefined &&
parent.type === "BinaryExpression" &&
parent.left.type === "PrivateIdentifier" &&
parent.right === node
);
};
// terser holds `#x in y` apart from binary expressions, so what a binary's
// right side wraps in parentheses printed bare after a private `in`, and
// read as another expression or stopped parsing.
const parensByType = /** @type {Record<string, EstreeParens>} */ (
estreeNeedsParens
);
for (const type of [
"YieldExpression",
"AssignmentExpression",
"ConditionalExpression",
"SequenceExpression"
]) {
const parens = parensByType[type];
parensByType[type] = (node, output) =>
isPrivateInRight(node, output) || parens(node, output);
}
// V8 reads the arguments before a bare destructuring assignment as one
// pattern and refuses `f(a += 1, [b] = c)`, which the spec allows.
const assignmentParens = parensByType.AssignmentExpression;
parensByType.AssignmentExpression = (node, output) => {
if (assignmentParens(node, output)) return true;
if (!corrections.enabled) return false;
const { left } = /** @type {PrintBinaryLike} */ (node);
if (left.type !== "ArrayPattern" && left.type !== "ObjectPattern") {
return false;
}
const parent = /** @type {PrintParent | undefined} */ (output.parent());
return (
parent !== undefined &&
(parent.type === "CallExpression" || parent.type === "NewExpression") &&
parent.arguments.indexOf(node) > 0
);
};
// terser reads `import()` as a call, so it holds the parentheses a call's
// arguments and callee owe: without them `import(a, (b, c))` gains an
// argument and `new (import(a))` stops parsing.
const sequenceParens = parensByType.SequenceExpression;
parensByType.SequenceExpression = (node, output) => {
if (sequenceParens(node, output)) return true;
const parent = /** @type {PrintParent | undefined} */ (output.parent());
return (
corrections.enabled &&
parent !== undefined &&
parent.type === "ImportExpression"
);
};
// terser has none for it, so `new (import(a))` printed `new import(a)`.
parensByType.ImportExpression = (node, output) => {
const parent = /** @type {PrintParent | undefined} */ (output.parent());
return (
corrections.enabled &&
parent !== undefined &&
parent.type === "NewExpression" &&
parent.callee === node
);
};
const printArrow =
/** @type {NonNullable<EstreeGenerators["ArrowFunctionExpression"]>} */ (
estreeCodegen.ArrowFunctionExpression
);
estreeCodegen.ArrowFunctionExpression = (node, output) => {
if (!isPrivateInRight(node, output)) {
printArrow(node, output);
return;
}
output.print("(");
printArrow(node, output);
output.print(")");
};
// terser's list of tags it parenthesizes misses these, and each printed
// bare reads the template as part of its operand, or stops parsing.
const printTagged =
/** @type {NonNullable<EstreeGenerators["TaggedTemplateExpression"]>} */ (
estreeCodegen.TaggedTemplateExpression
);
estreeCodegen.TaggedTemplateExpression = (node, output) => {
const { tag } = node;
if (
!corrections.enabled ||
!(
tag.type === "YieldExpression" ||
tag.type === "AwaitExpression" ||
tag.type === "ChainExpression" ||
kindOf.PrivateIn(tag)
)
) {
printTagged(node, output);
return;
}
modules.printEstree(tag, output, true);
modules.printEstree(node.quasi, output);
};
// terser spaces a regular expression only from a word operator it is the
// left side of, so one closing a deeper operand, `!/a/ in b`, took the
// operator for its flags; a number kept as `25.` runs into it too.
/** @type {WeakMap<OutputStream, number>} */
const literalEnds = new WeakMap();
const printLiteralCorrected =
/** @type {NonNullable<EstreeGenerators["Literal"]>} */ (
estreeCodegen.Literal
);
estreeCodegen.Literal = (node, output) => {
printLiteralCorrected(node, output);
if (
corrections.enabled &&
((node.value !== null && typeof node.value === "object") ||
typeof node.value === "number")
) {
literalEnds.set(output, output.currentPosition);
}
};
const printBinaryCorrected =
/** @type {NonNullable<EstreeGenerators["BinaryExpression"]>} */ (
estreeCodegen.BinaryExpression
);
estreeCodegen.BinaryExpression = (node, output) => {
if (
!corrections.enabled ||
(node.operator !== "in" && node.operator !== "instanceof") ||
node.left.type === "PrivateIdentifier"
) {
printBinaryCorrected(node, output);
return;
}
modules.printEstree(node.left, output);
if (
literalEnds.get(output) === output.currentPosition &&
/[/.]$/.test(output.last())
) {
output.print(" ");
} else {
output.space();
}
output.print(node.operator);
output.space();
modules.printEstree(node.right, output);
};
/**
* @param {PrintParent} parent a node
* @param {PrintParent} child one of its children
* @returns {boolean} whether the child's first token is the parent's
*/
const startsParent = (parent, child) =>
(parent.type === "SequenceExpression" && parent.expressions[0] === child) ||
(parent.type === "CallExpression" && parent.callee === child) ||
(parent.type === "TaggedTemplateExpression" && parent.tag === child) ||
(parent.type === "MemberExpression" && parent.object === child) ||
(parent.type === "ChainExpression" && parent.expression === child) ||
(parent.type === "ConditionalExpression" && parent.test === child) ||
(kindOf.Binary(parent) &&
/** @type {PrintBinaryLike} */ (parent).left === child) ||
(kindOf.UnaryPostfix(parent) &&
/** @type {PrintUnary} */ (parent).argument === child);
// A statement or loop head may not start with `let [`, a for-of head with
// `let` or a bare `async`: each reads as a declaration or an arrow.
estreeNeedsParens.Identifier = (node, output) => {
if (
!corrections.enabled ||
(node.name !== "let" && node.name !== "async")
) {
return false;
}
const parent = /** @type {PrintParent} */ (output.parent(0));
/** @type {PrintParent} */
let child = node;
/** @type {PrintParent | undefined} */
let head;
for (let i = 0; (head = output.parent(i)); i++) {
if (!startsParent(head, child)) break;
child = head;
}
if (head === undefined) return false;
if (head.type === "ForOfStatement" && head.left === child) {
return node.name === "let" || (child === node && !head.await);
}
return (
node.name === "let" &&
((head.type === "ExpressionStatement" && head.expression === child) ||
(head.type === "ForStatement" && head.init === child) ||
(head.type === "ForInStatement" && head.left === child)) &&
parent.type === "MemberExpression" &&
parent.computed &&
!parent.optional
);
};
// `{ __proto__ }` is an own property and `{ __proto__: x }` the prototype, a
// difference terser's tree keeps only in the tokens, so print by them; an
// import attribute is never a shorthand, so its `__proto__` prints alike.
const printProperty =
/** @type {NonNullable<EstreeGenerators["Property"]>} */ (
estreeCodegen.Property
);
estreeCodegen.Property = (node, output) => {
const { key, value } = node;
const parent = output.parent();
if (
!corrections.enabled ||
node.kind !== "init" ||
node.method ||
node.computed ||
/** @type {PrintIdentifier} */ (key).name !== "__proto__" ||
!parent ||
parent.type !== "ObjectExpression"
) {
printProperty(node, output);
return;
}
const asShorthand =
value.type === "Identifier" &&
value.atom !== true &&
printedNameOf(value) === "__proto__" &&
output.option("shorthand");
if (node.startToken && node.startToken === node.endToken) {
if (asShorthand) {
output.print_name("__proto__");
} else {
output.with_square(() => output.print_string("__proto__"));
output.colon();
modules.printEstree(value, output);
}
return;
}
if (!asShorthand) {
printProperty(node, output);
return;
}
output.print_name("__proto__");
output.colon();
modules.printEstree(value, output);
};
// A tag reads each segment's raw text, which two segments of one value can
// spell apart: `String.raw` of `\x41` is not of `A`, and an invalid escape has no value.
ast.wrapShallowEquivalent(
"PrefixedTemplateString",
(/** @type {(self: Node, other: Node) => boolean} */ original) =>
(/** @type {Node} */ self, /** @type {Node} */ other) => {
// terser compares nothing of the tagged template itself.
if (!corrections.enabled) return original(self, other);
const first = self.quasi.segments;
const second = other.quasi.segments;
if (first.length !== second.length) return false;
for (let i = 0; i < first.length; i++) {
if (
A.isTemplateSegmentNode(first[i]) &&
first[i].raw !== second[i].raw
) {
return false;
}
}
return true;
}
);
// Spreading `{ __proto__: p }` copies none of `p`, where writing its
// properties into the outer literal would set that literal's prototype.
wrapOptimize("Object", (original) => (self, compressor) => {
/** @type {[Node, Node][]} */
const kept = [];
for (const property of self.properties) {
if (
A.isExpansionNode(property) &&
A.isObjectNode(property.argument) &&
property.argument.properties.some(setsPrototype)
) {
kept.push([property, property.argument]);
property.argument = A.SequenceNode({
startToken: property.argument.startToken,
expressions: [property.argument],
endToken: property.argument.endToken
});
}
}
try {
return original(self, compressor);
} finally {
for (const [property, expression] of kept) {
property.argument = expression;
}
}
});
// A class's `static ["prototype"]` throws once the class runs, where the
// literal key is an early error; V8 also refuses a literal `"#constructor"`.
for (const name of ["ObjectProperty", "ConciseMethod"]) {
wrapOptimize(
name,
(original) => (self, compressor) =>
A.isClassNode(compressor.parent()) &&
A.isStringNode(self.key) &&
(self.key.value === "#constructor" ||
(self.static && self.key.value === "prototype"))
? self
: original(self, compressor)
);
}
// A literal `__proto__: v` sets the object's prototype where a method of
// that name defines a property, so neither may be rewritten as the other.
for (const [name, option] of [
["ConciseMethod", "arrows"],
["ObjectKeyVal", "unsafe_methods"]
]) {
wrapOptimize(
name,
(original) => (self, compressor) =>
!self.computed &&
(self.key.name === "__proto__" || self.key.value === "__proto__")
? withOptionsOff(compressor, [option], () =>
original(self, compressor)
)
: original(self, compressor)
);
}
// Moving `new.target` changes its value or leaves it outside a function.
// A direct `eval` can read the function's `this` without naming it.
const containsThis = A.containsThis;
/**
* @param {Node} self a node
* @returns {boolean} whether it reads `this` or `new.target` outside a nested function
*/
A.containsThis = (self) =>
containsThis(self, corrections.enabled) ||
(corrections.enabled && A.isScopeNode(self) && ast.usesEval(self));
wrapOptimize(
"Function",
(original) => (self, compressor) =>
compressor.option("unsafe_arrows") && ast.containsThis(self)
? withOptionsOff(compressor, ["unsafe_arrows"], () =>
original(self, compressor)
)
: original(self, compressor)
);
// A returned arrow still reads its enclosing function's `new.target`.
const isConstantExpression = ast.isConstantExpression;
/**
* @param {Node} self an expression
* @param {Scope=} scope the scope it would move to
* @returns {boolean | "f"} whether its value is fixed in that scope
*/
ast.isConstantExpression = (self, scope) => {
const result = isConstantExpression(self, scope);
return result &&
corrections.enabled &&
A.isArrowNode(self) &&
ast.containsThis(self)
? false
: result;
};
// A class dropped for its effects leaves its computed keys and static values
// outside it, where a private name it declares is no longer declared: terser's
// output no longer parses, so a class whose effects read one of its own stays.
/**
* @param {Node} classNode a class
* @returns {Set<string>} the private names it declares
*/
const privateNamesDeclaredBy = (classNode) => {
/** @type {Set<string>} */
const names = new Set();
for (const member of /** @type {Node[]} */ (
/** @type {Node} */ (classNode.body).body
)) {
if (
A.isClassPrivatePropertyNode(member) ||
A.isPrivateMethodNode(member) ||
A.isPrivateGetterNode(member) ||
A.isPrivateSetterNode(member)
) {
names.add(member.key.name);
}
}
return names;
};
/**
* Whether a tree reads one of the private names, past a class nested in it
* that declares the same name for itself.
* @param {Node} tree the tree
* @param {Set<string>} names the names
* @returns {boolean} true where it reads one
*/
const readsPrivateName = (tree, names) =>
A.walk(tree, (/** @type {Node} */ node) => {
if (node !== tree && A.isClassNode(node)) {
const own = privateNamesDeclaredBy(node);
if (![...own].some((name) => names.has(name))) return undefined;
// Its `extends` is read where the class sits, its body past its names.
if (node.superClass && readsPrivateName(node.superClass, names)) {
return A.walk_abort;
}
const outer = new Set([...names].filter((name) => !own.has(name)));
return outer.size > 0 &&
readsPrivateName(/** @type {Node} */ (node.body), outer)
? A.walk_abort
: true;
}
return (A.isDotHashNode(node) && names.has(node.property.name)) ||
(A.isPrivateInNode(node) && names.has(node.left.name))
? A.walk_abort
: undefined;
});
for (const type of ["ClassDeclaration", "ClassExpression"]) {
A.wrapDropSideEffectFree(
type,
(
/** @type {(self: Node, compressor: Compressor) => Node | null} */ original
) =>
(/** @type {Node} */ self, /** @type {Compressor} */ compressor) => {
const left = original(self, compressor);
if (left === null || left === self || !corrections.enabled) {
return left;
}
const declared = privateNamesDeclaredBy(self);
return declared.size > 0 && readsPrivateName(left, declared)
? self
: left;
}
);
}
/**
* @param {Node} value an expression
* @returns {boolean} whether it is an arrow, or an async or generator function
*/
const isNoConstructor = (value) =>
A.isArrowNode(value) ||
((A.isFunctionNode(value) || A.isDefunNode(value)) &&
(value.async || value.generator));
/**
* Whether defining a class throws: it extends what is no constructor or
* reads its own name.
* @param {Node} self a class
* @returns {boolean} true for `class extends (() => {}) {}`
*/
const extendsNoConstructor = (self) => {
const heritage = self.superClass;
if (!heritage) return false;
// The class's own name is uninitialized while its heritage runs.
const name = self.id;
if (
name &&
A.walk(heritage, (/** @type {Node} */ node) =>
A.isLambdaNode(node)
? true
: A.isSymbolRefNode(node) && node.definition === name.definition
? A.walk_abort
: undefined
)
) {
return true;
}
const value = A.isSymbolRefNode(heritage)
? ast.fixedValue(heritage)
: heritage;
if (!value) return false;
return (
isNoConstructor(value) ||
(A.isConstantNode(value) && !ast.isNullNode(value)) ||
A.isObjectNode(value) ||
A.isArrayNode(value)
);
};
// terser writes `static ["prototype"]` as `static prototype`, an early error.
for (const name of ["ConciseMethod", "ObjectProperty"]) {
wrapOptimize(
name,
(original) => (self, compressor) =>
self.static &&
self.computed &&
A.isStringNode(self.key) &&
self.key.value === "prototype"
? self
: original(self, compressor)
);
}
// `{ default: class {} }.default` names the class `default`, which flattening
// the literal loses; `keep_classnames` and `keep_fnames` keep that name.
const flattenObject = ast.flattenObject;
/**
* @param {Node} self a property read
* @param {string} key the property read
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the array read, where one fits
*/
ast.flattenObject = (self, key, compressor) =>
corrections.enabled &&
A.isObjectNode(self.object) &&
self.object.properties.some((/** @type {Node} */ property) => {
const { value } = property;
return (
Boolean(value) &&
!(/** @type {Node} */ (value).id) &&
(A.isClassNode(value)
? Boolean(compressor.option("keep_classnames"))
: A.isLambdaNode(value) && Boolean(compressor.option("keep_fnames")))
);
})
? undefined
: flattenObjectCalled(self, key, compressor);
/**
* terser's `flatten_object`, kept from a read called as a method unless the
* value is a function never reading the literal it receives as `this`.
* @param {Node} self a property read
* @param {string} key the property read
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the array read, where one fits
*/
const flattenObjectCalled = (self, key, compressor) => {
const flattened = flattenObject(self, key, compressor);
const parent = compressor.parent();
// A computed read becomes a dot read optimized in its place.
if (
!corrections.enabled ||
!flattened ||
!A.isCallNode(parent) ||
(parent.callee !== self && parent.callee !== compressor.self())
) {
return flattened;
}
const value =
flattened.object.elements[
/** @type {number} */ (flattened.property.value)
];
const fixed = A.isSymbolRefNode(value)
? ast.fixedValue(/** @type {Node} */ (value))
: value;
// Calling a constant throws whatever `this` is.
return fixed &&
(A.isConstantNode(fixed) ||
(A.isLambdaNode(fixed) &&
(A.isArrowNode(fixed) || !ast.containsThis(fixed))))
? flattened
: undefined;
};
/**
* Whether a class extends what may be no constructor, where `pure_heritage`
* is off; a class, a plain function or `null` is one.
* @param {Node} self a class
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `class extends f() {}`
*/
const extendsUnknown = (self, compressor) => {
const heritage = self.superClass;
if (!heritage || compressor.option("pure_heritage")) return false;
const value = A.isSymbolRefNode(heritage)
? ast.fixedValue(heritage)
: heritage;
return !(
value &&
(ast.isNullNode(value) ||
A.isClassNode(value) ||
(A.isLambdaNode(value) &&
!A.isArrowNode(value) &&
!value.async &&
!value.generator))
);
};
/**
* Whether a class extends `new.target` where it is `undefined`: in a static
* block or a field initializer, outside any function of its own.
* @param {Node} self a class
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `static { class extends new.target {} }`
*/
const extendsUnsetNewTarget = (self, compressor) => {
if (!self.superClass || !A.isNewTargetNode(self.superClass)) return false;
let child = self;
// Dropping unused names, the compressor is at the scope holding the class.
for (let i = -1; ; i++) {
const node = i < 0 ? compressor.self() : compressor.parent(i);
if (node === self) continue;
if (!node || (A.isLambdaNode(node) && !A.isArrowNode(node))) {
return false;
}
if (
A.isClassStaticBlockNode(node) ||
((A.isClassPropertyNode(node) || A.isClassPrivatePropertyNode(node)) &&
node.value === child)
) {
return true;
}
child = node;
}
};
/**
* Whether evaluating a class runs a static block or static field
* initializer holding a class that extends `new.target`, `undefined` there.
* @param {Node} self a class
* @returns {boolean} true for `class { static x = (0, class extends new.target {}) }`
*/
const initializesUnsetNewTarget = (self) => {
/** @type {Node[]} */
const toVisit = [];
for (const property of /** @type {Node[]} */ (
/** @type {Node} */ (self.body).body
)) {
if (A.isClassStaticBlockNode(property)) {
toVisit.push(.../** @type {Node[]} */ (property.body));
} else if (
(A.isClassPropertyNode(property) ||
A.isClassPrivatePropertyNode(property)) &&
property.static &&
property.value
) {
toVisit.push(/** @type {Node} */ (property.value));
}
}
return ast.walk(
self,
(/** @type {Node} */ node) => {
if (A.isLambdaNode(node) && !A.isArrowNode(node)) return true;
return A.isClassNode(node) &&
node.superClass &&
A.isNewTargetNode(node.superClass)
? ast.walk_abort
: undefined;
},
toVisit
);
};
/**
* Whether a name read is its class's own name, read in that class's
* heritage or a computed key, where the binding is not yet set.
* @param {Node} self a name read
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `x` in `class x { [x] }`
*/
const readsUnsetClassName = (self, compressor) => {
const { definition } = self;
if (!definition) return false;
let child = compressor.self();
let inKey = false;
for (let i = 0; ; i++) {
const node = compressor.parent(i);
if (!node || A.isLambdaNode(node)) return false;
if (A.isClassNode(node)) {
return (
(inKey || child === node.superClass) &&
Boolean(node.id) &&
node.id.name === self.name &&
definition.identifiers.includes(node.id)
);
}
if (node.key === child && node.computed) inKey = true;
else if (node.key !== undefined) return false;
child = node;
}
};
for (const wrapAnswer of [ast.wrapHasSideEffects, ast.wrapMayThrow]) {
wrapAnswer(
"Identifier",
(
/** @type {(self: Node, compressor: Compressor) => boolean} */ original
) =>
(/** @type {Node} */ self, /** @type {Compressor} */ compressor) =>
original(self, compressor) ||
(corrections.enabled &&
A.isSymbolRefNode(self) &&
readsUnsetClassName(self, compressor))
);
}
ast.wrapDropSideEffectFree(
"Identifier",
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) =>
corrections.enabled &&
A.isSymbolRefNode(self) &&
readsUnsetClassName(self, compressor)
? self
: original(self, compressor, firstInStatement)
);
/**
* Whether a class's heritage or a computed key reads the class's own name,
* which throws, the binding being set only after both run.
* @param {Node} self a class
* @returns {boolean} true for `class x { [x] }`
*/
const readsOwnNameEarly = (self) => {
const { id } = self;
if (!id) return false;
/** @type {Node[]} */
const toVisit = [];
if (self.superClass) toVisit.push(self.superClass);
for (const property of /** @type {Node[]} */ (
/** @type {Node} */ (self.body).body
)) {
if (property.computed) toVisit.push(property.key);
}
return ast.walk(
self,
(/** @type {Node} */ node, /** @type {Node[]} */ rest) => {
if (A.isLambdaNode(node)) return true;
if (A.isClassNode(node)) {
if (node.superClass) rest.push(node.superClass);
for (const property of /** @type {Node[]} */ (
/** @type {Node} */ (node.body).body
)) {
if (property.computed) rest.push(property.key);
}
return true;
}
return A.isSymbolRefNode(node) &&
node.definition &&
node.definition.identifiers.includes(id)
? ast.walk_abort
: undefined;
},
toVisit
);
};
for (const type of ["ClassDeclaration", "ClassExpression"]) {
for (const wrapAnswer of [ast.wrapHasSideEffects, ast.wrapMayThrow]) {
wrapAnswer(
type,
(
/** @type {(self: Node, compressor: Compressor) => boolean} */ original
) =>
(/** @type {Node} */ self, /** @type {Compressor} */ compressor) =>
original(self, compressor) ||
(corrections.enabled &&
(extendsNoConstructor(self) ||
extendsUnknown(self, compressor) ||
extendsUnsetNewTarget(self, compressor) ||
initializesUnsetNewTarget(self) ||
readsOwnNameEarly(self)))
);
}
A.wrapDropSideEffectFree(
type,
(
/** @type {(self: Node, compressor: Compressor) => Node | null} */ original
) =>
(/** @type {Node} */ self, /** @type {Compressor} */ compressor) =>
corrections.enabled &&
(extendsNoConstructor(self) ||
extendsUnknown(self, compressor) ||
extendsUnsetNewTarget(self, compressor) ||
initializesUnsetNewTarget(self) ||
readsOwnNameEarly(self))
? self
: original(self, compressor)
);
}
/**
* Whether a parameter has a default whose evaluation has effects.
* @param {Node} parameter the parameter
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `(a = f()) => …`
*/
const hasDefaultWithEffects = (parameter, compressor) =>
A.isDefaultAssignNode(parameter) &&
ast.hasSideEffects(parameter.right, compressor);
/**
* Whether destructuring a value runs code a pattern holds: a default or a
* computed key with effects, outside the functions it holds.
* @param {Node} pattern a destructuring pattern
* @param {Compressor} compressor the compressor
* @returns {boolean} true when one has effects
*/
const patternHasEffects = (pattern, compressor) => {
let effects = false;
A.walkNode(
pattern,
A.createWalker((/** @type {Node} */ node) => {
if (effects || A.isLambdaNode(node)) return true;
if (
(A.isDefaultAssignNode(node) &&
ast.hasSideEffects(node.right, compressor)) ||
(A.isObjectKeyValNode(node) &&
node.computed &&
ast.hasSideEffects(node.key, compressor))
) {
effects = true;
}
})
);
return effects;
};
/**
* Whether a parameter defaulting to a pattern has effects where a call
* passes nothing for it: only a literal of the pattern's kind, an object
* one without spread, getter or prototype, is unpacked without running code.
* @param {Node} parameter a default whose left side is a pattern
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `({ y = f() } = {})` or `({ y } = o)`
*/
const destructuredDefaultHasEffects = (parameter, compressor) => {
const { left, right } = parameter;
// An array's spread already counts among its effects.
const plain = left.is_array
? A.isArrayNode(right)
: A.isObjectNode(right) &&
right.properties.every(
(/** @type {Node} */ property) =>
A.isObjectKeyValNode(property) &&
(property.computed || A.terserKey(property.key) !== "__proto__")
);
return !plain || patternHasEffects(left, compressor);
};
/**
* The options that would drop effects a call's parameters or arguments
* still have: inlining or dropping it loses a default or a pattern, and
* dropping an empty function's call loses a spread's iteration.
* @param {Node} call the call
* @param {Compressor} compressor the compressor
* @returns {string[]} the options to skip for this call
*/
const optionsLosingEffects = (call, compressor) => {
let fn = call.callee;
if (A.isSymbolRefNode(fn)) {
fn = /** @type {Node} */ (ast.fixedValue(fn));
}
if (!A.isLambdaNode(fn)) return [];
const spreads = call.arguments.some((/** @type {Node} */ arg) =>
A.isExpansionNode(arg)
);
if (
fn.params.some(
(/** @type {Node} */ parameter, /** @type {number} */ index) =>
A.isDestructuringNode(parameter) ||
// A pattern unpacks what is passed, as a bare one does.
(A.isDefaultAssignNode(parameter) &&
A.isDestructuringNode(parameter.left) &&
(spreads ||
index < call.arguments.length ||
destructuredDefaultHasEffects(parameter, compressor))) ||
hasDefaultWithEffects(parameter, compressor) ||
(A.isExpansionNode(parameter) &&
!A.isSymbolFunargNode(parameter.argument))
)
) {
return ["inline", "side_effects"];
}
return /** @type {Node[]} */ (/** @type {Node} */ (fn.body).body).every(
isEmpty
) && spreads
? ["side_effects"]
: [];
};
/**
* Whether a node is what a call or a tag reads its function from.
* @param {Node} parent the node's parent
* @param {Node} node the node
* @returns {boolean} true for `node()` and `` node`…` ``
*/
const isCallee = (parent, node) =>
(A.isCallNode(parent) && parent.callee === node) ||
(A.isPrefixedTemplateStringNode(parent) && parent.tag === node);
/**
* Whether calling a node binds `this` to the object it was read from, or
* makes a direct `eval`.
* @param {Node} node the node
* @returns {boolean} true for `a.b`, `a?.b` and `eval`
*/
const bindsThis = (node) =>
A.isPropAccessNode(node) ||
A.isChainNode(node) ||
(A.isSymbolRefNode(node) && node.name === "eval");
/**
* `(0, result)`, which calls `result` unbound.
* @param {Node} result what took a callee's place
* @returns {Node} the sequence
*/
const detached = (result) =>
A.SequenceNode({
startToken: result.startToken,
expressions: [
A.NumberNode({
startToken: result.startToken,
value: 0,
endToken: result.startToken
}),
result
],
endToken: result.endToken
});
/**
* Whether `delete` acts on a node, deleting what it reads instead of
* returning `true` as it does for a call.
* @param {Node} node the node
* @returns {boolean} true for `a`, `a.b` and `a?.b`
*/
const isDeletable = (node) =>
A.isSymbolRefNode(node) || A.isPropAccessNode(node) || A.isChainNode(node);
// A callee replaced by `a.b` must not bind `a`, which the callee it replaces
// never did; terser keeps the `0,` for a call but not for a tag. A call
// `delete` takes, replaced by `a.b`, must not delete it.
wrapOptimize("Call", (original) => (self, compressor) => {
const parent = compressor.parent();
const result = optimizeKeepingEffects(self, compressor, original);
if (result === self) return result;
if (isCallee(parent, self) && bindsThis(result)) return detached(result);
return A.isUnaryPrefixNode(parent) &&
parent.operator === "delete" &&
parent.argument === self &&
isDeletable(result)
? detached(result)
: result;
});
for (const name of ["Sequence", "Conditional"]) {
wrapOptimize(name, (original) => (self, compressor) => {
const parent = compressor.parent();
if (!A.isPrefixedTemplateStringNode(parent) || parent.tag !== self) {
return original(self, compressor);
}
if (
A.isSequenceNode(self) &&
self.expressions.length === 2 &&
A.isNumberNode(self.expressions[0]) &&
bindsThis(self.expressions[1])
) {
return self;
}
const result = original(self, compressor);
return result !== self && bindsThis(result) ? detached(result) : result;
});
}
/**
* Optimizes a call, keeping the effects its parameters or arguments have.
* @param {Node} call the call
* @param {Compressor} compressor the compressor
* @param {Optimize} original terser's `optimize`
* @returns {Node} the optimized call
*/
function optimizeKeepingEffects(call, compressor, original) {
const names = optionsLosingEffects(call, compressor);
return names.length === 0
? original(call, compressor)
: withOptionsOff(compressor, names, () => original(call, compressor));
}
/**
* Whether a destructuring assignment in a function writes a parameter, which
* the analysis leaves out of the parameter's assignments.
* @param {Node} lambda the function
* @param {Definition} definition the parameter's definition
* @returns {boolean} true where one does
*/
const assignsByDestructuring = (lambda, definition) =>
Boolean(
A.walk(lambda, (/** @type {Node} */ node) =>
A.isDestructuringNode(node) &&
A.walk(node, (/** @type {Node} */ inner) =>
A.isSymbolRefNode(inner) && inner.definition === definition
? A.walk_abort
: undefined
)
? A.walk_abort
: undefined
)
);
// Writing a `const` or `using` binding throws, even writing it its own value.
wrapOptimize("Assign", (original) => (self, compressor) => {
const { left } = self;
return corrections.enabled &&
self.operator === "=" &&
A.isSymbolRefNode(left) &&
left.definition &&
left.definition.identifiers.some(
(/** @type {Node} */ identifier) =>
A.isSymbolConstNode(identifier) || A.isSymbolUsingNode(identifier)
)
? self
: original(self, compressor);
});
// A sloppy function's simple parameters are its `arguments` object's
// indices, so a value reading that object reads them too.
modules.webpackCollapse.argumentsAliases = (node, compressor) => {
if (
!corrections.enabled ||
!A.isArgumentsObject(node) ||
compressor.has_directive("use strict")
) {
return [];
}
const { params } = node.definition.scope.node;
return params.every(A.isSymbolFunargNode) ? params : [];
};
// A sloppy `arguments[i]` follows its parameter only where an argument was
// passed, so it is the parameter only while nothing reassigns that.
wrapOptimize("Sub", (original) => (self, compressor) => {
const { object: expression, property } = self;
if (
compressor.option("arguments") &&
A.isSymbolRefNode(expression) &&
expression.name === "arguments" &&
A.isLambdaNode(expression.scope.node) &&
!A.isArrowNode(expression.scope.node) &&
A.isNumberNode(property) &&
!compressor.has_directive("use strict")
) {
const parameter =
expression.scope.node.params[
/** @type {number} */ (ast.constantValue(property))
];
if (A.isSymbolFunargNode(parameter)) {
const definition = parameter.definition;
if (
!compressor.option("reduce_vars") ||
definition.assignments ||
definition.identifiers.length > 1 ||
assignsByDestructuring(expression.scope.node, definition)
) {
return withOptionsOff(compressor, ["arguments"], () =>
original(self, compressor)
);
}
}
}
return original(self, compressor);
});
/**
* Whether a case test reads a binding the switch's block declares, which the
* rewrites to `if` and to a block move it out of.
* @param {Node} self a switch
* @returns {boolean} true where one does
*/
const readsSwitchBinding = (self) => {
const scope = self.block_scope;
if (!scope || scope.variables.length === 0) return false;
return self.cases.some(
(/** @type {Node} */ branch) =>
branch.test &&
A.walk(branch.test, (/** @type {Node} */ node) =>
A.isSymbolRefNode(node) && node.definition.scope === scope
? A.walk_abort
: undefined
)
);
};
wrapOptimize(
"Switch",
(original) => (self, compressor) =>
readsSwitchBinding(self) ? self : original(self, compressor)
);
// `===` between two readings of one expression is `==` only where both
// readings give one value, which a call or property read need not.
wrapOptimize("Binary", (original) => (self, compressor) => {
const parent = compressor.parent();
if (A.isPrefixedTemplateStringNode(parent) && parent.tag === self) {
const result = original(self, compressor);
return result !== self && bindsThis(result) ? detached(result) : result;
}
if (
(self.operator === "===" || self.operator === "!==") &&
compressor.option("comparisons") &&
!A.isSymbolRefNode(self.left) &&
!A.isConstantNode(self.left) &&
ast.isEquivalent(self.left, self.right)
) {
return withOptionsOff(compressor, ["comparisons"], () =>
original(self, compressor)
);
}
return original(self, compressor);
});
// The three sets a function reserves, shared so reading the parent chain of
// every call allocates nothing.
const AWAIT_ONLY = ["await"];
const YIELD_ONLY = ["yield"];
const AWAIT_AND_YIELD = ["await", "yield"];
/**
* The names the scope around a node reserves: `await` inside an async
* function, `yield` inside a generator, both inside an async generator. A
* plain arrow reserves neither, which engines read as a fresh context for the
* name though not for the operator.
* @param {Walker} compressor the compressor or another walker, for the parent chain
* @returns {string[] | undefined} the names, or undefined where none is reserved
*/
const reservedAround = (compressor) => {
for (let level = 0; ; level++) {
const parent = compressor.parent(level);
if (!parent) return undefined;
// A static block is strict and awaiting: `await` is a keyword there
// whatever surrounds it, and no input can name `yield` inside one.
if (A.isClassStaticBlockNode(parent)) return AWAIT_ONLY;
if (A.isLambdaNode(parent)) {
if (parent.async) {
return parent.generator ? AWAIT_AND_YIELD : AWAIT_ONLY;
}
return parent.generator ? YIELD_ONLY : undefined;
}
}
};
/**
* Whether a function names a word where its destination reserves it. A nested
* function sets a context of its own, except that an arrow's parameters read
* the scope around it, so those answer to the destination too.
* @param {Node} lambda the function
* @param {string[]} reserved the names
* @returns {boolean} true where one of them would land where it is a keyword
*/
const namesInOwnContext = (lambda, reserved) => {
let names = false;
const walker = A.createWalker((/** @type {Node} */ inner) => {
if (names) return true;
if (A.isSymbolNode(inner) && reserved.includes(inner.name)) {
names = true;
return true;
}
if (inner !== lambda && A.isLambdaNode(inner)) {
// Only an arrow's parameters read the scope around it; no body does.
if (A.isArrowNode(inner)) {
for (const parameter of inner.params) {
A.walkNode(parameter, walker);
}
}
return true;
}
});
A.walkNode(lambda, walker);
return names;
};
modules.reservedAround = reservedAround;
modules.namesInOwnContext = namesInOwnContext;
/**
* The function a call would inline, read through a reference whose value the
* compressor knows.
* @param {Node} call the call
* @returns {Node | undefined} the function, or undefined where there is none
*/
const inlinedLambda = (call) => {
const { callee } = call;
if (A.isLambdaNode(callee)) return callee;
if (A.isSymbolRefNode(callee)) {
const fixed = ast.fixedValue(callee);
if (fixed && A.isLambdaNode(fixed)) return fixed;
}
return undefined;
};
/**
* Whether a function is sloppy code: no `"use strict"` in it or around it,
* no class around it, and no module.
* @param {Node} lambda the function
* @param {Compressor} compressor the compressor
* @returns {boolean} true where its code would read otherwise as strict
*/
const isSloppy = (lambda, compressor) => {
if (compressor.option("module")) return false;
for (
let scope = /** @type {Scope | null} */ (lambda.block_scope);
scope;
scope = scope.upper
) {
const { node } = scope;
if (A.isClassNode(node)) return false;
if (A.isLambdaNode(node) || A.isToplevelNode(node)) {
for (const statement of /** @type {Node[]} */ (A.bodyOf(node))) {
if (!A.isDirectiveNode(statement)) break;
if (statement.directive === "use strict") return false;
}
}
}
return true;
};
/**
* Whether code would run otherwise as strict: it reads `this`, `arguments`
* or `with`, deletes, or writes a property or an undeclared name.
* @param {Node} lambda the function
* @returns {boolean} true where one of them is in it
*/
const readsStrictness = (lambda) =>
Boolean(
A.walk(lambda, (/** @type {Node} */ node) => {
if (
A.isThisNode(node) ||
A.isWithNode(node) ||
(A.isSymbolRefNode(node) && node.name === "arguments")
) {
return A.walk_abort;
}
if (A.isUnaryNode(node) && node.operator === "delete") {
return A.walk_abort;
}
const target =
A.isAssignNode(node) || A.isForInNode(node) || A.isForOfNode(node)
? node.left
: A.isUnaryNode(node) &&
(node.operator === "++" || node.operator === "--")
? node.argument
: undefined;
return target !== undefined &&
(A.isPropAccessNode(target) ||
A.isDestructuringNode(target) ||
(A.isSymbolRefNode(target) && target.definition.undeclared))
? A.walk_abort
: undefined;
})
);
/**
* @param {Node} lambda a function
* @param {Compressor} compressor the compressor at strict code or not
* @returns {boolean | undefined} whether moving it there would run sloppy code as strict
*/
const runsSloppyAsStrict = (lambda, compressor) =>
corrections.enabled &&
compressor.has_directive("use strict") &&
isSloppy(lambda, compressor) &&
readsStrictness(lambda);
// terser moves a sloppy function into strict code, a class above all, where
// writing a primitive's property or an undeclared name throws, and `this`
// is no longer the global object.
modules.webpackInline.keepsSloppyLambda = runsSloppyAsStrict;
// A function expression's own name is read-only, so a declaration writing
// its name throws in strict code, or keeps its old value, once inlined.
modules.webpackInline.keepsReassignedDeclaration = (
/** @type {Node} */ value,
/** @type {Definition} */ definition
) =>
corrections.enabled && A.isDefunNode(value) && definition.assignments > 0;
// An anonymous function or class is named after its binding, a name
// `keep_fnames` or `keep_classnames` keeps, so it stays bound.
/**
* @param {Node} value a value stored in a binding
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it is an anonymous function or class whose kept name is the binding's
*/
const keepsInferredName = (value, compressor) =>
corrections.enabled &&
!value.id &&
(A.isClassNode(value)
? Boolean(compressor.option("keep_classnames"))
: A.isLambdaNode(value) && Boolean(compressor.option("keep_fnames")));
modules.webpackInline.keepsInferredName = keepsInferredName;
modules.webpackCollapse.keepsNamedValue = keepsInferredName;
// terser inlines a function naming `await` or `yield` into an async function
// or a generator, where the name is a keyword, so its output does not parse.
// Only the inline is held back, so the call still optimizes otherwise.
wrapOptimize("Call", (original) => (self, compressor) => {
const lambda = inlinedLambda(self);
if (lambda !== undefined && runsSloppyAsStrict(lambda, compressor)) {
return withOptionsOff(compressor, ["inline"], () =>
original(self, compressor)
);
}
const reserved = reservedAround(compressor);
if (reserved === undefined) return original(self, compressor);
if (lambda === undefined || !namesInOwnContext(lambda, reserved)) {
return original(self, compressor);
}
return withOptionsOff(compressor, ["inline"], () =>
original(self, compressor)
);
});
/**
* Whether `s.charAt(i)` may read outside `s`, where `s[i]` reads `undefined`
* instead of `""`.
* @param {Node} call the call
* @param {Compressor} compressor the compressor
* @returns {boolean} false where `s` and `i` are constants and `i` is inside `s`
*/
const readsCharOutside = (call, compressor) => {
const { callee } = call;
if (!A.isDotNode(callee) || callee.property.name !== "charAt") {
return false;
}
const argument = call.arguments[0];
const index = argument ? ast.evaluate(argument, compressor) : 0;
if (index === argument) return false;
const string = ast.evaluate(callee.object, compressor);
if (
typeof string !== "string" ||
(typeof index === "object" && index !== null) ||
typeof index === "bigint"
) {
return true;
}
const position = Math.trunc(Number(index)) || 0;
return (
position < 0 ||
position >= string.length ||
(0 | /** @type {number} */ (index)) !== position
);
};
/**
* Whether a call is `Array(n)` for a fractional `n`, which throws a
* `RangeError` where terser writes holes.
* @param {Node} call the call
* @returns {boolean} true for such a call
*/
const sizesArrayByFraction = (call) =>
callsGlobal(call, "Array") &&
call.arguments.length === 1 &&
A.isNumberNode(call.arguments[0]) &&
!Number.isInteger(call.arguments[0].value);
// terser writes `s.charAt(i)` as `s[i | 0]`, which reads `undefined` outside
// the string and wraps an `i` past 2 ** 31 back into it, and `Array(0.5)`,
// which throws, as holes.
wrapOptimize(
"Call",
(original) => (self, compressor) =>
compressor.option("unsafe") &&
(readsCharOutside(self, compressor) || sizesArrayByFraction(self))
? withOptionsOff(compressor, ["unsafe"], () =>
original(self, compressor)
)
: original(self, compressor)
);
/** @type {WeakMap<Compressor, Set<Node>>} */
const defaultsWithEffects = new WeakMap();
// terser moves what follows an `if (…) return` into the `if`, leaving `let`
// and `const` where they are but not `class`, which is block-scoped too.
for (const name of [
"Block",
"BlockStatement",
"Lambda",
"Arrow",
"Function",
"ClassStaticBlock"
]) {
wrapOptimize(name, (original) => (self, compressor) => {
if (
A.isLambdaNode(self) &&
self.params.some((/** @type {Node} */ parameter) =>
hasDefaultWithEffects(parameter, compressor)
)
) {
let lambdas = defaultsWithEffects.get(compressor);
if (lambdas === undefined) {
lambdas = new Set();
defaultsWithEffects.set(compressor, lambdas);
}
lambdas.add(self);
}
const statements = A.bodyOf(self);
return Array.isArray(statements) &&
statements.some((/** @type {Node} */ statement) =>
A.isDefClassNode(statement)
)
? withOptionsOff(compressor, ["if_return"], () =>
original(self, compressor)
)
: original(self, compressor);
});
}
// terser trims an unused trailing parameter with its default, whose effects
// a call passing no argument still runs; keep such a parameter.
if (modules.webpackUnused) {
/**
* @param {Node} lambda a function whose trailing parameter is trimmed
* @param {Node} parameter that parameter, its default included
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it stays, as a call passing nothing runs its default
*/
modules.webpackUnused.keepsParameter = (lambda, parameter, compressor) => {
if (!corrections.enabled) return false;
const lambdas = defaultsWithEffects.get(compressor);
return (
lambdas !== undefined &&
lambdas.has(lambda) &&
hasDefaultWithEffects(parameter, compressor)
);
};
// A `for` head's `let` dropped of an unused name keeps it, if it has effects.
modules.webpackUnused.keepsLoopBinding = () => corrections.enabled;
// terser drops an array element, moving the ones after it.
modules.webpackUnused.keepsListPosition = () => corrections.enabled;
// terser drops a write nothing reads, though a constant's throws.
modules.webpackUnused.keepsImmutableWrite = () => corrections.enabled;
/**
* terser drops a compound write nothing reads, though a BigInt beside
* another value throws, as may converting a parameter no write here gave.
* @param {Node} assign the compound assignment
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it stays
*/
modules.webpackUnused.keepsConvertingWrite = (assign, compressor) => {
if (!corrections.enabled) return false;
const { left, right } = assign;
const isBigInt = (/** @type {Node} */ node) =>
isBigIntLiteral(node) ||
typeof ast.evaluateNode(node, compressor, 1) === "bigint";
return (
isBigInt(left) !== isBigInt(right) ||
(A.isSymbolRefNode(left) &&
A.isSymbolFunargNode(left.definition.identifiers[0]))
);
};
return;
}
// terser's own `drop_unused` trims first, so the parameter is put back.
const dropUnused = A.dropUnused;
A.dropUnused = (
/** @type {Node} */ self,
/** @type {Compressor} */ compressor
) => {
const lambdas = corrections.enabled
? defaultsWithEffects.get(compressor)
: undefined;
if (lambdas === undefined) return dropUnused(self, compressor);
/** @type {[Node, Node[]][]} */
const before = [];
for (const lambda of lambdas) before.push([lambda, [...lambda.params]]);
const result = dropUnused(self, compressor);
for (const [lambda, argnames] of before) {
let keep = lambda.params.length;
for (let i = keep; i < argnames.length; i++) {
if (hasDefaultWithEffects(argnames[i], compressor)) keep = i + 1;
}
for (let i = lambda.params.length; i < keep; i++) {
lambda.params.push(argnames[i]);
}
}
return result;
};
};
/**
* A literal with its `\t` or `\n` escapes, as asked, written as the character
* each stands for: one byte where the escape is two. A tab is legal in any
* quoted string, a line break in a template alone.
* @param {string} literal a quoted string or template literal
* @param {boolean} tabs whether its `\t` escapes are written raw
* @param {boolean} lineBreaks whether its `\n` escapes are written raw
* @returns {string} the same literal, those escapes unescaped
*/
const unescapeWhitespace = (literal, tabs, lineBreaks) => {
let result = "";
let start = 0;
// Each backslash takes the character after it, so `\\t` stays as it is.
for (
let i = literal.indexOf("\\");
i !== -1;
i = literal.indexOf("\\", i + 2)
) {
const code = literal.charCodeAt(i + 1);
if ((tabs && code === 116) || (lineBreaks && code === 110)) {
result += `${literal.slice(start, i)}${code === 116 ? "\t" : "\n"}`;
start = i + 2;
}
}
return start === 0 ? literal : result + literal.slice(start);
};
// A program writes its strings' line breaks raw once its source spells `\n`
// this many times, and at least once in this many characters.
const LINE_BREAK_ESCAPES = 1000;
const LINE_BREAK_SPACING = 500;
/** @typedef {{ enabled: boolean, lets: { enabled: boolean }, literalsLast: { enabled: boolean } }} Improvements the switch the improvements read, one for writing `const` as `let` alone, and one for writing a literal after what an equality compares it with */
/**
* Installs webpack's improvements on terser: each writes a program terser
* leaves longer, only where the shorter one provably behaves the same.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installImprove = (modules) => {
const { walkNode } = modules.ast;
const A = modules.ast;
markNodeKinds(modules);
const KIND_BITS = A.nodeKindBits;
/** @type {Improvements} */
const improvements = {
enabled: true,
lets: { enabled: true },
literalsLast: { enabled: true }
};
modules.improvements = improvements;
// terser leaves `0;` where it drops a write from a function it is done with.
modules.webpackUnused.dropsEmptiedStatement = () => improvements.enabled;
// A module binding nothing kept reads is dropped as any other.
modules.webpackUnused.dropsUnusedImport = () => improvements.enabled;
modules.webpackUnused.trimsUncountedParameter = (
lambda,
parameter,
compressor
) =>
improvements.enabled &&
!A.usesArguments(lambda) &&
(A.isExpansionNode(parameter) ||
(A.isDefaultAssignNode(parameter) &&
!A.hasSideEffects(parameter.right, compressor)));
const { prototype: output } = modules.MinifiedOutput;
const { makeString: makeTerserString } = output;
/**
* Whether a string writes its tabs raw: a long one holding several, as a
* short one's `\t` gzips better kept, matching the escapes near it.
* @param {string} str a string's value
* @returns {boolean} true when it does
*/
const writesTabsRaw = (str) =>
str.length >= 100 && str.indexOf("\t") !== str.lastIndexOf("\t");
/**
* terser's string literal, its tabs written raw where `writesTabsRaw` says.
* @param {string} str a string's value
* @param {string=} quote the quote it was written with
* @returns {string} the string literal
*/
output.makeString = function makeString(str, quote) {
const literal = makeTerserString.call(this, str, quote);
return improvements.enabled && quote !== "`" && writesTabsRaw(str)
? unescapeWhitespace(literal, true, false)
: literal;
};
// How many `\n` escapes each tree's source spells, and how long it is, over
// every file read into it.
/** @type {WeakMap<Node, { escapes: number, length: number }>} */
const lineBreakEscapes = new WeakMap();
const parseModule =
/** @type {{ parse: (text: string, options?: ParseOptions) => Node }} */ (
modules.parse
);
const parseTree = parseModule.parse;
/**
* @param {string} text a source
* @returns {number} how many `\n` escapes it spells
*/
const countLineBreakEscapes = (text) => {
let escapes = 0;
for (
let i = text.indexOf("\\n");
i !== -1;
i = text.indexOf("\\n", i + 2)
) {
escapes++;
}
return escapes;
};
/**
* terser's `parse`, counting the `\n` escapes its source spells.
* @param {string} text the source
* @param {ParseOptions=} options terser's parse options
* @returns {Node} the tree
*/
parseModule.parse = (text, options) => {
const toplevel = parseTree(text, options);
if (improvements.enabled) {
const escapes = countLineBreakEscapes(text);
const read = lineBreakEscapes.get(toplevel);
if (read === undefined) {
lineBreakEscapes.set(toplevel, { escapes, length: text.length });
} else {
read.escapes += escapes;
read.length += text.length;
}
}
return toplevel;
};
/**
* Whether strings write their line breaks raw, as templates: only where
* their source spells `\n` often and densely, since gzip codes a raw line
* break short only once it is frequent, and a mix breaks repeats.
* @param {number} escapes how many `\n` escapes the source spells
* @param {number} length how long the source is
* @returns {boolean} true when they do
*/
const spellsLineBreaksDensely = (escapes, length) =>
escapes >= LINE_BREAK_ESCAPES && escapes * LINE_BREAK_SPACING >= length;
/**
* @param {Node} toplevel a tree
* @returns {boolean} whether its strings write their line breaks raw
*/
const writesLineBreaksRaw = (toplevel) => {
const read = lineBreakEscapes.get(toplevel);
return (
read !== undefined && spellsLineBreaksDensely(read.escapes, read.length)
);
};
// Whether the program being printed writes its line breaks raw.
let rawLineBreaks = false;
/**
* A string's value as a template literal, its line breaks written raw.
* @param {OutputStream<PrintNode | number>} stream the stream printed into
* @param {string} str the value
* @param {boolean} tabs whether its tabs are written raw too
* @returns {string} the template literal
*/
const templateOf = (stream, str, tabs) => {
let literal = stream.encode_string(str, "`");
if (literal.includes("${")) literal = literal.replace(/\${/g, "\\${");
return unescapeWhitespace(literal, tabs, true);
};
const { print_template_string_chars: printTemplateChars } = output;
/**
* A template's characters, its line breaks raw where its program's are.
* @param {string} str a template's cooked characters
* @returns {void}
*/
output.print_template_string_chars = function print_template_string_chars(
str
) {
if (rawLineBreaks && str.includes("\n")) {
this.print(templateOf(this, str, false).slice(1, -1));
} else {
printTemplateChars.call(this, str);
}
};
const estreeCodegen = modules.estreeCodegen;
const kindOf = modules.kindOf;
// Decided once per program, as a mix of raw and escaped line breaks breaks
// the repeats between its strings that gzip reuses.
const printProgram = /** @type {NonNullable<EstreeGenerators["Program"]>} */ (
estreeCodegen.Program
);
estreeCodegen.Program = (node, stream) => {
const outer = rawLineBreaks;
rawLineBreaks =
improvements.enabled &&
stream.option("ecma") >= 2015 &&
writesLineBreaksRaw(/** @type {Node} */ (node));
try {
printProgram(node, stream);
} finally {
rawLineBreaks = outer;
}
};
/**
* Prints a program read from `text` as `Program` does, its strings writing
* their line breaks raw where that source says.
* @param {OutputStream<PrintNode | number>} stream the stream printed into
* @param {string} text the program's source
* @param {() => void} print prints the program
* @returns {void}
*/
modules.printWithLineBreaksOf = (stream, text, print) => {
const outer = rawLineBreaks;
rawLineBreaks =
improvements.enabled &&
stream.option("ecma") >= 2015 &&
spellsLineBreaksDensely(countLineBreakEscapes(text), text.length);
try {
print();
} finally {
rawLineBreaks = outer;
}
};
/**
* @param {OutputStream<PrintNode | number>} stream the stream printed into
* @param {string} value a string's value
* @returns {string | undefined} the template it prints as, where its program writes line breaks raw and it holds one
*/
modules.lineBreakTemplateOf = (stream, value) =>
rawLineBreaks && value.includes("\n")
? templateOf(stream, value, writesTabsRaw(value))
: undefined;
/**
* Whether a string printed here may be a template: not a directive, nor
* an import's or export's source or attribute, a tag, or asm.js.
* @param {OutputStream} stream the stream printed into
* @returns {boolean} true when it may
*/
const takesTemplate = (stream) => {
if (stream.in_directive || stream.use_asm) return false;
const parent = /** @type {PrintNode} */ (stream.parent());
// An attribute is a property of the object its import or export holds.
const holder = /** @type {PrintNode} */ (
parent.type === "Property" ? stream.parent(2) : parent
);
return !(
holder.type === "ImportDeclaration" ||
holder.type === "ExportNamedDeclaration" ||
holder.type === "ExportAllDeclaration" ||
parent.type === "ImportExpression" ||
kindOf.PrefixedTemplateString(parent) ||
// After `await` or `yield` a template reads as a tag's where either
// names a variable, and a member's string prints apart from the rest.
parent.type === "AwaitExpression" ||
parent.type === "YieldExpression" ||
parent.type === "MemberExpression"
);
};
// A string holding a line break, as a template where its program says.
const printLiteral = /** @type {NonNullable<EstreeGenerators["Literal"]>} */ (
estreeCodegen.Literal
);
estreeCodegen.Literal = (node, stream) => {
const { value } = node;
if (
rawLineBreaks &&
typeof value === "string" &&
value.includes("\n") &&
takesTemplate(stream)
) {
stream.print(templateOf(stream, value, writesTabsRaw(value)));
} else {
printLiteral(node, stream);
}
};
/**
* Whether a node reads the `this`, `arguments`, `new.target` or `super` of
* the function around it, through the arrows sharing them, or names `yield`
* or `await`, which a generator or async function around it reads as its own.
* @param {Node} node a node
* @returns {boolean} true when it does
*/
const readsFunctionContext = (node) => {
let reads = false;
walkNode(
node,
A.createWalker((/** @type {Node} */ inner) => {
if (reads) return true;
if (A.isLambdaNode(inner) && inner !== node) {
if (!A.isArrowNode(inner)) return true;
reads = A.usesEval(inner);
return;
}
if (
A.isThisNode(inner) ||
A.isNewTargetNode(inner) ||
(A.isSymbolRefNode(inner) && inner.name === "arguments") ||
(A.isSymbolNode(inner) &&
(inner.name === "yield" || inner.name === "await"))
) {
reads = true;
}
})
);
return reads;
};
/**
* Whether a function's body runs the same as a block in its caller's place:
* nothing in it is the function's own, no `var` unless `vars` collects its
* names, function declaration, `return`, label, directive or context.
* @param {Node} fn the function
* @param {string[] | undefined} reserved the names the caller's scope reserves
* @param {string[] | undefined} vars where the names its `var`s declare go
* @param {Node} call the call of it
* @param {Compressor} compressor the compressor whose target permits declarations
* @returns {boolean} true when its body can stand in for the call
*/
const runsAsBlock = (fn, reserved, vars, call, compressor) => {
if (
((fn.params.length > 0 || call.arguments.length > 0) &&
!bindsArguments(call, fn) &&
!declaresArguments(call, fn, compressor)) ||
fn.id ||
fn.async ||
fn.generator ||
A.isPinned(fn)
) {
return false;
}
let own = false;
walkNode(
fn,
A.createWalker((/** @type {Node} */ node) => {
if (own) return true;
if (node === fn) return;
if (vars && A.isVarNode(node)) {
walkNode(
node,
A.createWalker((/** @type {Node} */ symbol) => {
if (A.isSymbolDeclarationNode(symbol)) vars.push(symbol.name);
})
);
return;
}
if (
A.isDefunNode(node) ||
A.isVarNode(node) ||
A.isReturnNode(node) ||
A.isLabeledStatementNode(node) ||
A.isDirectiveNode(node)
) {
own = true;
return true;
}
// A nested function's own declarations and returns are its own.
if (A.isLambdaNode(node)) return true;
})
);
// An arrow shares the `this` around it, so only a name the caller's own
// scope reserves keeps its body from running in place there.
return (
!own &&
(A.isArrowNode(fn)
? reserved === undefined || !namesInOwnContext(fn, reserved)
: !readsFunctionContext(fn))
);
};
/**
* The function an expression calls in place, with nothing passed and its
* value dropped, when its body runs the same as a block there.
* @param {Node} expression an expression whose value is dropped
* @param {string[] | undefined} reserved the names the caller's scope reserves
* @param {Compressor} compressor the compressor whose target permits declarations
* @param {string[]=} vars where the names its `var`s declare go, if allowed
* @returns {Node | undefined} the function
*/
const calledInPlace = (expression, reserved, compressor, vars) => {
let call = expression;
// The value is dropped, so a `!` or `void` before the call reads nothing.
while (
A.isUnaryPrefixNode(call) &&
(call.operator === "!" || call.operator === "void")
) {
call = call.argument;
}
if (
!A.isCallExpressionNode(call) ||
call.optional ||
(call._annotations & A._NOINLINE) !== 0
) {
return undefined;
}
const fn = call.callee;
return (A.isFunctionNode(fn) || A.isArrowNode(fn)) &&
runsAsBlock(fn, reserved, vars, call, compressor)
? fn
: undefined;
};
// Measured: copies of one helper called in place gzip as one repeat.
/** @type {WeakSet<Node>} */
let repeatedInPlace = new WeakSet();
/**
* The functions called in place, passed only variables, that some other
* such call writes alike.
* @param {Node} tree the tree
* @returns {WeakSet<Node>} the functions
*/
const repeatedPassedFunctions = (tree) => {
/** @type {Map<string, Node[]>} */
const byText = new Map();
A.walk(tree, (/** @type {Node} */ node) => {
if (
!A.isCallExpressionNode(node) ||
node.arguments.length === 0 ||
!(A.isFunctionNode(node.callee) || A.isArrowNode(node.callee)) ||
!node.arguments.every((/** @type {Node} */ argument) =>
A.isSymbolRefNode(argument)
)
) {
return;
}
const text = modules.printToString(node.callee);
const functions = byText.get(text);
if (functions) functions.push(node.callee);
else byText.set(text, [node.callee]);
});
/** @type {WeakSet<Node>} */
const repeated = new WeakSet();
for (const functions of byText.values()) {
if (functions.length > 1) {
for (const fn of functions) repeated.add(fn);
}
}
return repeated;
};
/**
* Whether each parameter of a function called in place can be read as the
* variable passed for it: one nothing writes, named nowhere else in the body.
* @param {Node} call the call
* @param {Node} fn the function it calls
* @returns {boolean} true when `bindArguments` may rename them
*/
const bindsArguments = (call, fn) => {
const { params } = fn;
const args = call.arguments;
if (args.length !== params.length || repeatedInPlace.has(fn)) return false;
// A closure the body makes would read a `var` its loop sets again.
const closes = A.walk(
/** @type {Node} */ (fn.body),
(/** @type {Node} */ node) =>
A.isLambdaNode(node) ? A.walk_abort : undefined
);
// Measured: its `var`s hoisted beside the call gzip worse than the call.
if (
A.walk(/** @type {Node} */ (fn.body), (/** @type {Node} */ node) =>
A.isVarNode(node) ? A.walk_abort : undefined
)
) {
return false;
}
/** @type {Set<string>} */
const names = new Set();
for (let i = 0; i < params.length; i++) {
const parameter = params[i];
const value = args[i];
if (
!A.isSymbolFunargNode(parameter) ||
!parameter.definition ||
!A.isSymbolRefNode(value) ||
names.has(value.name)
) {
return false;
}
names.add(value.name);
const variable = value.definition;
if (
!variable ||
variable.undeclared ||
variable.global ||
variable.identifiers.length !== 1 ||
variable.scope.isPinned() ||
(variable.scope.usesArguments &&
A.isSymbolFunargNode(variable.identifiers[0])) ||
(closes && A.isSymbolVarNode(variable.identifiers[0])) ||
writesVariable(variable.scope.node, variable) ||
writesVariable(/** @type {Node} */ (fn.body), parameter.definition) ||
A.walk(fn, (/** @type {Node} */ node) =>
A.isSymbolDeclarationNode(node) &&
node !== parameter &&
(node.name === value.name || node.name === parameter.name)
? A.walk_abort
: undefined
)
) {
return false;
}
}
return true;
};
// The names a value may print as, which a `let` of that name would hide.
const VALUE_NAMES = new Set(["NaN", "Infinity", "undefined"]);
/**
* Whether each parameter of a function called in place can be declared as a
* `let` of the value passed for it, in its body's block, as swc writes it.
* @param {Node} call the call
* @param {Node} fn the function it calls
* @param {Compressor} compressor the compressor whose target permits declarations
* @returns {boolean} true when `declareArguments` may declare them
*/
const declaresArguments = (call, fn, compressor) => {
const { params } = fn;
const args = call.arguments;
if (
compressor.option("ecma") < 2015 ||
args.length > params.length ||
repeatedInPlace.has(fn) ||
A.walk(/** @type {Node} */ (fn.body), (/** @type {Node} */ node) =>
A.isVarNode(node) ? A.walk_abort : undefined
)
) {
return false;
}
/** @type {Set<string>} */
const names = new Set();
for (const parameter of params) {
// Two `let`s may not share a name, nor take one a value prints as.
if (
!A.isSymbolFunargNode(parameter) ||
!parameter.definition ||
names.has(parameter.name) ||
parameter.name === "let" ||
VALUE_NAMES.has(parameter.name)
) {
return false;
}
names.add(parameter.name);
}
// What the body declares the block declares, ahead of its `let`s.
A.walk(/** @type {Node} */ (fn.body), (/** @type {Node} */ node) => {
if (A.isSymbolDeclarationNode(node)) names.add(node.name);
if (!A.isLambdaNode(node)) return;
if (A.isDefunNode(node)) names.add(node.id.name);
return true;
});
// A value naming a parameter or one of those would read it there,
// and a spread passes values to more than one.
return !args.some(
(/** @type {Node} */ value) =>
A.isExpansionNode(value) ||
// A declaration would give an anonymous function or class its name.
((A.isLambdaNode(value) || A.isClassNode(value)) && !value.id) ||
A.walk(value, (/** @type {Node} */ node) =>
A.isSymbolRefNode(node) &&
(names.has(node.name) || node.name === "eval")
? A.walk_abort
: undefined
)
);
};
/**
* Declares each parameter of a function called in place as a `let` of the
* value passed for it, first in its body, as `declaresArguments` allows.
* @param {Node} expression the expression calling it
* @param {Node} fn the function
* @returns {void}
*/
const declareArguments = (expression, fn) => {
let call = expression;
while (A.isUnaryPrefixNode(call)) call = call.argument;
/** @type {Node[]} */
const declarations = [];
for (let i = 0; i < fn.params.length; i++) {
const parameter = fn.params[i];
const value = call.arguments[i] || null;
// A parameter nothing reads, passed a constant or nothing, needs no name.
if (
(value === null || A.isConstantNode(value)) &&
!A.walk(/** @type {Node} */ (fn.body), (/** @type {Node} */ node) =>
A.isSymbolRefNode(node) && node.definition === parameter.definition
? A.walk_abort
: undefined
)
) {
continue;
}
declarations.push(
makeNode(A.VarDefNode, parameter, {
id: makeNode(A.SymbolLetNode, parameter, {
scope: parameter.scope,
name: parameter.name,
definition: parameter.definition
}),
init: value
})
);
}
if (declarations.length === 0) return;
/** @type {Node[]} */ (/** @type {Node} */ (fn.body).body).unshift(
makeNode(A.LetNode, call, { declarations, await: false })
);
};
/**
* Renames each parameter of a function called in place, where it is read,
* to the variable passed for it, as `bindsArguments` allows.
* @param {Node} expression the expression calling it
* @param {Node} fn the function
* @returns {void}
*/
const bindArguments = (expression, fn) => {
let call = expression;
while (A.isUnaryPrefixNode(call)) call = call.argument;
const { params } = fn;
for (let i = 0; i < params.length; i++) {
const { definition } = params[i];
const value = call.arguments[i];
A.walk(/** @type {Node} */ (fn.body), (/** @type {Node} */ node) => {
if (A.isSymbolRefNode(node) && node.definition === definition) {
node.name = value.name;
node.definition = value.definition;
}
});
}
};
// The `correct` phase, installed just before this one, answers what a scope
// reserves and which names a body would write there; both phases hold to one
// reading of the rule.
/** @type {(walker: Walker) => string[] | undefined} */
const reservedAround = modules.reservedAround;
/** @type {(lambda: Node, reserved: string[]) => boolean} */
const namesInOwnContext = modules.namesInOwnContext;
/** @type {(node: Node) => boolean} */
const canBeEvictedFromBlock = modules.common.can_be_evicted_from_block;
/** @type {(orig: Node, expressions: Node[]) => Node} */
const makeSequence = modules.common.make_sequence;
const makeVoid0 = modules.utils.make_void_0;
/**
* The expressions of a statement whose values are dropped: all of an
* expression statement's, and all but the last of a returned sequence.
* @param {Node} statement an expression statement or a `return`
* @returns {Node[]} the expressions
*/
const droppedExpressions = (statement) => {
if (A.isReturnNode(statement)) {
const value = statement.argument;
return value && A.isSequenceNode(value)
? value.expressions.slice(0, -1)
: [];
}
/** @type {Node} */
const value = statement.expression;
return A.isSequenceNode(value) ? value.expressions : [value];
};
/**
* Whether a statement calls a function in place that runs as a block there.
* @param {Node} statement a statement whose value is dropped
* @param {string[] | undefined} reserved the names the caller's scope reserves
* @param {Compressor} compressor the compressor whose target permits declarations
* @param {string[]=} vars where the names its functions' `var`s declare go
* @returns {boolean} true when `runInPlace` rewrites it
*/
const runsInPlace = (statement, reserved, compressor, vars) => {
let runs = false;
for (const expression of droppedExpressions(statement)) {
if (calledInPlace(expression, reserved, compressor, vars) !== undefined) {
runs = true;
}
}
return runs;
};
/**
* Whether the `var`s of the functions a statement calls in place can move
* to the function around it: run once there, and named nowhere else in it.
* @param {Walker} walker a walk standing on the statement
* @param {Node} statement the statement
* @param {string[]} vars the names those `var`s declare
* @param {string[] | undefined} reserved the names the caller's scope reserves
* @param {Compressor} compressor the compressor whose options keep names
* @returns {Node | undefined} the function they move to
*/
const hoistsVarsTo = (walker, statement, vars, reserved, compressor) => {
const scope = walker.find_parent(
(node) =>
A.isLambdaNode(node) ||
A.isClassStaticBlockNode(node) ||
A.isToplevelNode(node)
);
if (
!scope ||
!A.isLambdaNode(scope) ||
walker.is_within_loop() ||
A.isPinned(scope) ||
vars.includes("arguments")
) {
return undefined;
}
// Each moved function and the names it declares itself.
/** @type {Map<Node, Set<string>>} */
const moved = new Map();
/** @type {Set<string>} */
const names = new Set();
// A name also met elsewhere in the scope, and every name met there.
/** @type {Set<string>} */
const clashing = new Set();
/** @type {Set<string>} */
const seen = new Set();
for (const expression of droppedExpressions(statement)) {
/** @type {string[]} */
const own = [];
const fn = calledInPlace(expression, reserved, compressor, own);
if (!fn) continue;
const ownNames = new Set(own);
// Two functions declaring one name would share one binding there.
for (const name of ownNames) {
if (names.has(name)) clashing.add(name);
names.add(name);
}
moved.set(fn, ownNames);
}
/**
* @param {Set<string>} allowed the names a node may read as its own
* @returns {Walker} a walk finding a name it would read from elsewhere
*/
const findClash = (allowed) =>
A.createWalker((/** @type {Node} */ node) => {
const own = moved.get(node);
if (own !== undefined && own !== allowed) {
walkNode(node, findClash(own));
return true;
}
if (!A.isSymbolNode(node)) return;
seen.add(node.name);
if (names.has(node.name) && !allowed.has(node.name)) {
clashing.add(node.name);
}
});
walkNode(scope, findClash(new Set()));
// Mangling names the tree afresh, so a `var` moving out takes a name
// the scope never spells, and binds there apart from the one it met.
// More of them than one cost longer names there than they save.
for (const [fn, own] of moved) {
for (const name of own) {
if (
clashing.has(name) &&
(own.size > 1 ||
// A binding also names an anonymous function or class assigned to it.
keepName(compressor.option("keep_fnames"), name) ||
keepName(compressor.option("keep_classnames"), name) ||
declaresOtherwise(fn, name))
) {
return undefined;
}
}
}
for (const [fn, own] of moved) {
for (const name of own) {
if (clashing.has(name)) renameVar(fn, name, seen);
}
}
return scope;
};
/**
* @param {Node} fn a function called in place
* @param {string} name a name its `var` declares
* @returns {boolean} whether something else there declares the name too, as a block's class
*/
const declaresOtherwise = (fn, name) => {
let found = false;
walkNode(
fn,
A.createWalker((/** @type {Node} */ node) => {
if (found || (A.isLambdaNode(node) && node !== fn)) return true;
if (
A.isSymbolDeclarationNode(node) &&
node.name === name &&
!A.isSymbolVarNode(node)
) {
found = true;
}
})
);
return found;
};
/**
* Gives the `var` a function declares under a name another one never used.
* @param {Node} fn the function
* @param {string} name the name its `var` declares
* @param {Set<string>} seen the names already met, the new one joining them
*/
const renameVar = (fn, name, seen) => {
let fresh = name;
for (let i = 0; seen.has(fresh); i++) fresh = `${name}$${i}`;
seen.add(fresh);
/** @type {Set<Definition>} */
const definitions = new Set();
walkNode(
fn,
A.createWalker((/** @type {Node} */ node) => {
if (A.isLambdaNode(node) && node !== fn) return true;
if (
A.isSymbolDeclarationNode(node) &&
node.name === name &&
node.definition
) {
definitions.add(node.definition);
}
})
);
walkNode(
fn,
A.createWalker((/** @type {Node} */ node) => {
if (A.isSymbolNode(node) && definitions.has(node.definition)) {
node.name = fresh;
}
})
);
for (const definition of definitions) definition.name = fresh;
};
/**
* A statement's expressions as statements, each function it calls in place
* written as its body, or as a block where that declares something scoped.
* @param {Node} statement a statement whose value is dropped
* @param {string[] | undefined} reserved the names the caller's scope reserves
* @param {boolean} hoists whether its functions' `var`s may move out
* @param {Compressor} compressor the compressor whose target permits declarations
* @returns {Node[] | undefined} the statements, or nothing to rewrite
*/
const runInPlace = (statement, reserved, hoists, compressor) => {
const expressions = droppedExpressions(statement);
/** @type {Node[]} */
const statements = [];
/** @type {Node[]} */
let pending = [];
let rewritten = false;
for (const expression of expressions) {
const fn = calledInPlace(
expression,
reserved,
compressor,
hoists ? [] : undefined
);
if (!fn) {
pending.push(expression);
continue;
}
rewritten = true;
let call = expression;
while (A.isUnaryPrefixNode(call)) call = call.argument;
if (bindsArguments(call, fn)) bindArguments(expression, fn);
else declareArguments(expression, fn);
if (pending.length > 0) {
statements.push(
makeNode(A.SimpleStatementNode, statement, {
expression: makeSequence(statement, pending)
})
);
pending = [];
}
/** @type {Node[]} */
const body = /** @type {Node[]} */ (/** @type {Node} */ (fn.body).body);
if (body.every(canBeEvictedFromBlock)) {
statements.push(...body);
} else {
statements.push(makeNode(A.BlockStatementNode, statement, { body }));
}
}
if (!rewritten) return undefined;
if (A.isReturnNode(statement)) {
const { expressions: all } = statement.argument;
pending.push(all[all.length - 1]);
statements.push(
makeNode(A.ReturnNode, statement, {
argument: makeSequence(statement, pending)
})
);
} else if (pending.length > 0) {
statements.push(
makeNode(A.SimpleStatementNode, statement, {
expression: makeSequence(statement, pending)
})
);
}
return statements;
};
/**
* Whether a statement calls one of some functions directly.
* @param {Node} statement a statement whose value is dropped
* @param {Set<Node>} functions the functions
* @returns {boolean} true when one is a callee there
*/
const callsAnyOf = (statement, functions) => {
if (functions.size === 0) return false;
return droppedExpressions(statement).some(
(/** @type {Node} */ expression) => {
let call = expression;
while (A.isUnaryPrefixNode(call)) call = call.argument;
return A.isCallExpressionNode(call) && functions.has(call.callee);
}
);
};
// Compressing visits every statement, so it notes the compressor that met
// one calling a function in place, the walk below being for nothing otherwise.
/** @type {Compressor | undefined} */
let callingInPlace;
/**
* Notes the compressor meeting a statement that calls a function in place.
* @param {(self: Node, compressor: Compressor) => Node} optimizeStatement terser's optimizer
* @returns {(self: Node, compressor: Compressor) => Node} the optimizer noting it
*/
const notingCallsInPlace = (optimizeStatement) => (self, compressor) => {
if (
callingInPlace !== compressor &&
improvements.enabled &&
runsInPlace(self, reservedAround(compressor), compressor, [])
) {
callingInPlace = compressor;
}
return optimizeStatement(self, compressor);
};
A.wrapOptimizer("SimpleStatement", notingCallsInPlace);
A.wrapOptimizer("Return", notingCallsInPlace);
/**
* Whether a call of a function written in place passes, at some position,
* the name of the parameter there, which keeps terser from inlining it.
* @param {Node} call a call
* @returns {boolean} true when one is passed so
*/
const passesParameterName = (call) => {
const fn = call.callee;
if (!A.isFunctionNode(fn) && !A.isArrowNode(fn)) return false;
const { params } = fn;
const args = call.arguments;
const length = Math.min(params.length, args.length);
for (let i = 0; i < length; i++) {
if (A.isSymbolRefNode(args[i]) && args[i].name === params[i].name) {
return true;
}
}
return false;
};
// Compressing visits every call, so it notes the compressor that met one
// passing a parameter's name, the walks below being for nothing otherwise.
/** @type {Compressor | undefined} */
let passingParameterNames;
/**
* `[a].concat([b], 1, x).concat(y)` as `[a, b, 1].concat(x, y)`: an array
* literal takes the arrays and primitives `concat` would append first, and
* one `concat` the arguments of the next, where reading them runs nothing.
* @param {Node} call a `concat` call
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the shorter writing, if any
*/
const concatOfArrays = (call, compressor) => {
let { callee } = call;
let args = call.arguments;
if (call.optional || callee.optional || !A.isDotNode(callee)) return;
const inner = callee.object;
if (
A.isCallNode(inner) &&
!A.isNewNode(inner) &&
!inner.optional &&
A.isDotNode(inner.callee) &&
!inner.callee.optional &&
inner.callee.property.name === "concat" &&
A.isArrayNode(inner.callee.object) &&
args.every(
(/** @type {Node} */ argument) =>
!A.isExpansionNode(argument) &&
!A.hasSideEffects(argument, compressor)
)
) {
callee = inner.callee;
args = [...inner.arguments, ...args];
}
const receiver = callee.object;
// A hole reads an index through `Array.prototype`, which `concat` copies.
if (!A.isArrayNode(receiver) || receiver.elements.includes(null)) return;
const elements = [...receiver.elements];
let taken = 0;
for (const argument of args) {
if (A.isArrayNode(argument)) {
if (argument.elements.includes(null)) break;
elements.push(...argument.elements);
} else {
const value = constantOf(argument);
if (
value === NOT_CONSTANT ||
(value !== null && typeof value === "object")
) {
break;
}
elements.push(argument);
}
taken++;
}
if (callee === call.callee && taken === 0) return;
const array = makeNode(A.ArrayNode, receiver, { elements });
return taken === args.length
? array
: makeNode(A.CallNode, call, {
callee: makeNode(A.DotNode, callee, {
object: array,
property: callee.property,
optional: false
}),
arguments: args.slice(taken),
optional: false
});
};
A.wrapOptimizer("Call", (optimizeCall) => (self, compressor) => {
const optimized = optimizeCall(self, compressor);
if (
passingParameterNames !== compressor &&
improvements.enabled &&
A.isCallExpressionNode(optimized) &&
passesParameterName(optimized)
) {
passingParameterNames = compressor;
}
return optimized;
});
/**
* Adds the definitions a target writes to a set: its names, not the
* objects of the properties it writes to.
* @param {Node} target what an assignment, update or loop head writes
* @param {Set<Definition>} written the definitions written
* @returns {void}
*/
const addWrittenNames = (target, written) => {
A.walk(target, (/** @type {Node} */ node) => {
if (A.isPropAccessNode(node)) return true;
if (A.isSymbolNode(node) && node.definition) {
written.add(node.definition);
}
});
};
/**
* Whether a function reads a definition from a function or class inside
* it, which may run after the call has returned.
* @param {Node} fn the function
* @param {Definition} definition the definition
* @returns {boolean} true when one reads it
*/
const readsLater = (fn, definition) =>
A.walk(/** @type {Node} */ (fn.body), (/** @type {Node} */ node) => {
if (!A.isScopeNode(node)) return undefined;
return A.walk(node, (/** @type {Node} */ inner) =>
A.isSymbolRefNode(inner) && inner.definition === definition
? A.walk_abort
: undefined
)
? A.walk_abort
: true;
});
/**
* Whether the parameter `index` of a function written in place and called
* can read the name passed there: both share the name, and neither binding
* is written, nor read before it is set.
* @param {Node} call the call
* @param {number} index the parameter's position
* @param {Set<Definition>} written the definitions the tree writes
* @returns {boolean} true when it can
*/
const readsPassedName = (call, index, written) => {
const parameter = call.callee.params[index];
const argument = call.arguments[index];
if (!A.isSymbolRefNode(argument) || argument.name !== parameter.name) {
return false;
}
const own = parameter.definition;
const passed = argument.definition;
if (
!own ||
!passed ||
own.identifiers.length !== 1 ||
written.has(own) ||
passed.undeclared ||
passed.global ||
passed.identifiers.length !== 1 ||
written.has(passed) ||
passed.scope.isPinned()
) {
return false;
}
const declaration = passed.identifiers[0];
// A function reading its `arguments` may write a parameter through it.
if (A.isSymbolFunargNode(declaration)) return !passed.scope.usesArguments;
if (A.isSymbolLetNode(declaration) || A.isSymbolConstNode(declaration)) {
return Boolean(passed.fixed);
}
// A `var` or function declared in a loop is set again each time round,
// which a function reading the parameter later would see.
return (
(A.isSymbolDefunNode(declaration) ||
(A.isSymbolVarNode(declaration) && Boolean(passed.fixed))) &&
!readsLater(call.callee, own)
);
};
/**
* The positions, last first, of the parameters of a function written in
* place and called that `readsPassedName` lets read the names passed.
* @param {Node} call the call
* @param {Set<Definition>} written the definitions the tree writes
* @returns {number[]} the positions, none where the function loses none
*/
const droppableParameters = (call, written) => {
const fn = call.callee;
/** @type {number[]} */
const found = [];
if (
fn.id ||
fn.async ||
fn.generator ||
A.isPinned(fn) ||
(!A.isArrowNode(fn) && A.usesArguments(fn)) ||
!fn.params.every((/** @type {Node} */ name) =>
A.isSymbolFunargNode(name)
) ||
call.arguments.some((/** @type {Node} */ arg) => A.isExpansionNode(arg))
) {
return found;
}
for (let index = fn.params.length; --index >= 0;) {
if (readsPassedName(call, index, written)) found.push(index);
}
return found;
};
/**
* Drops parameters of a function written in place and called, with their
* arguments, its reads of each then reading the name passed.
* @param {Node} call the call
* @param {number[]} positions the parameters' positions, last first
* @returns {void}
*/
const dropParameters = (call, positions) => {
const fn = call.callee;
for (const index of positions) {
const own = fn.params[index].definition;
const passed = call.arguments[index].definition;
A.walk(/** @type {Node} */ (fn.body), (/** @type {Node} */ node) => {
if (A.isSymbolRefNode(node) && node.definition === own) {
node.definition = passed;
}
});
fn.params.splice(index, 1);
call.arguments.splice(index, 1);
}
};
/**
* Whether a node sits in the parameter list of a function, where a name
* of the function's may be read before it is set.
* @param {Node} node the node
* @param {{ parent: (n?: number) => Node | undefined }} parents the nodes around it, nearest first
* @returns {boolean} true when it does
*/
const inParameters = (node, parents) => {
let child = node;
for (let level = 0, parent; (parent = parents.parent(level)); level++) {
if (A.isLambdaNode(parent) && parent.params.includes(child)) return true;
child = parent;
}
return false;
};
/**
* The value a call of a function written in place returns from its one
* statement, where the call's place reads it alike once nothing is passed.
* @param {Node} call the call, its every argument a parameter's name
* @param {Node} parent the node holding the call
* @param {string[] | undefined} reserved the names the scope around the call reserves
* @returns {Node | undefined} the value, or undefined
*/
const valueInPlace = (call, parent, reserved) => {
const fn = call.callee;
const body = /** @type {Node} */ (fn.body).body;
if (
call.optional ||
call._annotations ||
/** @type {Node[]} */ (body).length !== 1 ||
!A.isReturnNode(/** @type {Node[]} */ (body)[0]) ||
!(/** @type {Node[]} */ (body)[0].argument)
) {
return undefined;
}
const value = /** @type {Node[]} */ (body)[0].argument;
if (
A.isArrowNode(fn)
? reserved !== undefined && namesInOwnContext(fn, reserved)
: readsFunctionContext(fn)
) {
return undefined;
}
// A method read would be called with its object, a chain would stop the
// read after it, `typeof` would not throw for a global undeclared, and a
// string first in a body would be read as a directive.
if (
(A.isCallNode(parent) && parent.callee === call) ||
(A.isPrefixedTemplateStringNode(parent) && parent.tag === call) ||
(A.isPropAccessNode(parent) && A.isChainNode(value)) ||
(A.isUnaryPrefixNode(parent) && parent.operator === "typeof") ||
(A.isSimpleStatementNode(parent) && A.isStringNode(value))
) {
return undefined;
}
return value;
};
/**
* Puts a node in the place of one of a node's children.
* @param {Node} parent the node holding the child
* @param {Node} child the child
* @param {Node} replacement what takes its place
* @returns {void}
*/
const replaceChild = (parent, child, replacement) => {
for (const key of Object.keys(parent)) {
const value = /** @type {Record<string, NodeFieldValue>} */ (parent)[key];
if (value === child) {
/** @type {Record<string, NodeFieldValue>} */ (parent)[key] =
replacement;
return;
}
if (Array.isArray(value)) {
const index = value.indexOf(child);
if (index !== -1) {
value[index] = replacement;
return;
}
}
}
};
// The nodes the walk below reads, as bits: what writes its `left`, an
// update writing its `argument`, and a call.
const WRITES_LEFT = 1;
const WRITES_ARGUMENT = 2;
const CALLS = 4;
const walkedKinds = A.kindBits([
["Assign", WRITES_LEFT],
["ForIn", WRITES_LEFT],
["UpdatePrefix", WRITES_ARGUMENT],
["UnaryPostfix", WRITES_ARGUMENT],
["CallExpression", CALLS]
]);
// Run on the finished tree, whose names mangling reads afresh: while terser
// compresses, the function's scope still binds each parameter dropped.
const compressKeepingParameters =
/** @type {CompressorMethods["compress"]} */ (
modules.compress.methods.compress
);
/**
* @this {Compressor} the compressor
* @param {Node} toplevel the tree
* @returns {Node} the tree compressed
*/
modules.compress.methods.compress = function compress(toplevel) {
const compressed = compressKeepingParameters.call(this, toplevel);
const met = passingParameterNames === this;
passingParameterNames = undefined;
if (!met || !improvements.enabled || !this.option("inline")) {
return compressed;
}
/** @type {Set<Definition>} */
const written = new Set();
// Each call passing a parameter's name, the node holding it and the
// names the scope around it reserves, outer calls first.
/** @type {[Node, Node, string[] | undefined][]} */
const calls = [];
A.walk_parent(
compressed,
(
/** @type {Node} */ node,
/** @type {{ parent: (n?: number) => Node | undefined }} */ parents
) => {
const kind = node[walkedKinds];
if (kind === WRITES_LEFT) {
addWrittenNames(node.left, written);
} else if (kind === WRITES_ARGUMENT) {
addWrittenNames(node.argument, written);
} else if (
kind === CALLS &&
passesParameterName(node) &&
!inParameters(node, parents)
) {
calls.push([
node,
/** @type {Node} */ (parents.parent()),
reservedAround(/** @type {Compressor} */ (parents))
]);
}
}
);
let dropped = false;
for (let i = calls.length; --i >= 0;) {
const [call, parent, reserved] = calls[i];
const positions = droppableParameters(call, written);
if (positions.length === 0) continue;
const value =
positions.length === call.callee.params.length &&
positions.length === call.arguments.length
? valueInPlace(call, parent, reserved)
: undefined;
// One parameter dropped saves two bytes, which gzip spends again on the
// `function(e){` and `}(e)` runs it breaks, unless the call goes too.
if (positions.length === 1 && value === undefined) continue;
dropParameters(call, positions);
dropped = true;
if (value !== undefined) replaceChild(parent, call, value);
}
// A statement may now call a function in place passing it nothing.
if (dropped) callingInPlace = this;
return compressed;
};
// Run on the finished tree, so terser has already inlined what it would
// from inside the function, which a block at the top level would stop.
const { methods: compressorMethods } = modules.compress;
const compressTree = /** @type {CompressorMethods["compress"]} */ (
compressorMethods.compress
);
/**
* @this {Compressor} the compressor
* @param {Node} toplevel the tree
* @returns {Node} the tree compressed
*/
compressorMethods.compress = function compress(toplevel) {
const compressed = compressTree.call(this, toplevel);
// Under `expression` the last statement is the program's value.
if (
!improvements.enabled ||
!this.option("inline") ||
this.option("expression") ||
callingInPlace !== this
) {
callingInPlace = undefined;
return compressed;
}
callingInPlace = undefined;
repeatedInPlace = repeatedPassedFunctions(compressed);
// Read the tree once, writing nothing, then rewrite only the statements
// found, innermost first, so a body moved out already holds its own.
/** @type {[Node, Node, string[] | undefined, boolean][]} */
const found = [];
// A function taking another's `var`s keeps them only if it stays one.
/** @type {Set<Node>} */
const receivers = new Set();
const walker = A.createWalker(
(/** @type {Node} */ node, /** @type {() => void} */ descend) => {
if (!A.isSimpleStatementNode(node) && !A.isReturnNode(node)) return;
descend();
// The stack is gone once the walk ends, so the names the scope
// around this statement reserves are read while it is still here.
const reserved = reservedAround(walker);
if (runsInPlace(node, reserved, this)) {
if (!callsAnyOf(node, receivers)) {
found.push([node, walker.parent(), reserved, false]);
}
return true;
}
/** @type {string[]} */
const vars = [];
if (
!runsInPlace(node, reserved, this, vars) ||
callsAnyOf(node, receivers)
) {
return true;
}
const scope = hoistsVarsTo(walker, node, vars, reserved, this);
if (scope) {
receivers.add(scope);
found.push([node, walker.parent(), reserved, true]);
}
return true;
}
);
walkNode(compressed, walker);
for (const [statement, parent, reserved, hoists] of found) {
const statements =
/** @type {Node[]} */
(runInPlace(statement, reserved, hoists, this));
/** @type {Node[] | undefined} */
const body = /** @type {Node[] | undefined} */ (A.bodyOf(parent));
const list = Array.isArray(body) ? body : undefined;
const index = list ? list.indexOf(statement) : -1;
// A string first in a list could read as a directive there.
if (
list &&
index !== -1 &&
!(
A.isSimpleStatementNode(statements[0]) &&
A.isStringNode(statements[0].expression)
)
) {
list.splice(index, 1, ...statements);
continue;
}
const block =
statements.length === 1 && A.isBlockStatementNode(statements[0])
? statements[0]
: makeNode(A.BlockStatementNode, statement, { body: statements });
if (list && index !== -1) {
list[index] = block;
continue;
}
for (const key of Object.keys(parent)) {
if (
/** @type {Record<string, NodeFieldValue>} */ (parent)[key] ===
statement
) {
/** @type {Record<string, NodeFieldValue>} */ (parent)[key] = block;
}
}
}
return compressed;
};
/**
* The value a `return` gives, printed, or undefined where it is not a
* constant: `return` alone and `return void 0` print alike.
* @param {Node} statement a `return`
* @returns {string | undefined} the value printed
*/
const returnedConstant = (statement) => {
const value = statement.argument;
if (!value) return "void 0";
return A.isConstantExpression(value)
? modules.printToString(value)
: undefined;
};
/**
* Whether a statement does nothing as it runs: a `var` with no value, or a
* function declaration, hoisted already.
* @param {Node} statement a statement
* @returns {boolean} true when it does nothing
*/
const runsNothing = (statement) =>
A.isEmptyStatementNode(statement) ||
A.isDefunNode(statement) ||
(A.isVarNode(statement) &&
statement.declarations.every(
(/** @type {Node} */ declaration) =>
!declaration.init && A.isSymbolDeclarationNode(declaration.id)
));
/**
* The `return` a statement completing normally runs next, or null where
* the function ends there.
* @param {Node} statement the statement
* @param {{ parent: (n?: number) => Node | undefined }} parents its ancestors
* @returns {Node | null | undefined} the `return`, null at the end, or undefined where unknown
*/
const returnAfter = (statement, parents) => {
let node = statement;
for (let i = 0, parent; (parent = parents.parent(i)); i++) {
if (A.isIfNode(parent) || A.isLabeledStatementNode(parent)) {
node = parent;
continue;
}
const list = A.bodyOf(parent);
if (!Array.isArray(list)) return undefined;
const index = list.indexOf(node);
if (index === -1) return undefined;
for (let j = index + 1; j < list.length; j++) {
if (runsNothing(list[j])) continue;
return A.isReturnNode(list[j]) ? list[j] : undefined;
}
if (A.isLambdaNode(parent)) return null;
if (A.isSwitchBranchNode(parent)) {
// A branch before the last falls into the next one.
const cases = A.bodyOf(/** @type {Node} */ (parents.parent(i + 1)));
if (
/** @type {Node[]} */ (cases)[
/** @type {Node[]} */ (cases).length - 1
] !== parent
) {
return undefined;
}
node = /** @type {Node} */ (parents.parent(++i));
continue;
}
if (!A.isBlockStatementNode(parent)) return undefined;
node = parent;
}
return undefined;
};
/**
* What a statement leaves its function returning when it completes
* normally, printed: the `return` that runs next, or `void 0` at the end.
* @param {Node} statement the statement
* @param {{ parent: (n?: number) => Node | undefined }} parents its ancestors
* @returns {string | undefined} the value, or undefined where unknown
*/
const returnedAfter = (statement, parents) => {
const next = returnAfter(statement, parents);
if (next === null) return "void 0";
return next ? returnedConstant(next) : undefined;
};
// `try { … } catch { return null } return null`: the `catch` completing
// returns the same value, so its own `return` goes.
const compressBeforeCatchReturns = compressorMethods.compress;
/**
* @this {Compressor} the compressor
* @param {Node} toplevel the tree
* @returns {Node} the tree compressed
*/
compressorMethods.compress = function compress(toplevel) {
const compressed = compressBeforeCatchReturns.call(this, toplevel);
if (!improvements.enabled || !this.option("dead_code")) return compressed;
A.walk_parent(
compressed,
(
/** @type {Node} */ node,
/** @type {{ parent: (n?: number) => Node | undefined }} */ parents
) => {
if (!A.isTryNode(node) || !node.handler || node.finalizer) return;
/** @type {Node[]} */
const body = /** @type {Node[]} */ (A.bodyOf(node.handler));
const last = body[body.length - 1];
if (!last || !A.isReturnNode(last)) return;
const value = returnedConstant(last);
if (value !== undefined && value === returnedAfter(node, parents)) {
body.pop();
}
}
);
return compressed;
};
/**
* Whether a statement declares only `var`s without values.
* @param {Node} statement a statement
* @returns {boolean} true for `var a, b`
*/
const declaresOnly = (statement) =>
A.isVarNode(statement) &&
statement.declarations.every(
(/** @type {Node} */ declaration) =>
!declaration.init && A.isSymbolDeclarationNode(declaration.id)
);
/**
* Moves a function's `var`s without values into the first `var` of its
* body, where hoisting declares them all the same.
* @param {Node} lambda a function or the program
* @returns {void}
*/
const joinBareVars = (lambda) => {
/** @type {Node[]} */
const body = /** @type {Node[]} */ (A.bodyOf(lambda));
const first = body.find((statement) => A.isVarNode(statement));
if (!first) return;
/** @type {Set<string>} */
const names = new Set(
first.declarations.map(
(/** @type {Node} */ declaration) => declaration.id.name
)
);
/** @type {[Node, Node, Node][]} */
const moved = [];
const walker = A.createWalker((/** @type {Node} */ node) => {
if (node !== lambda && A.isLambdaNode(node)) return true;
if (A.isClassNode(node)) return true;
if (node === first || !declaresOnly(node)) return;
const parent = walker.parent();
// A loop's head keeps its `var`.
if (!Array.isArray(A.bodyOf(parent))) return true;
moved.push([node, parent, walker.parent(1)]);
return true;
});
walkNode(lambda, walker);
for (const [statement, parent, holder] of moved) {
for (const declaration of statement.declarations) {
if (names.has(declaration.id.name)) continue;
names.add(declaration.id.name);
first.declarations.push(declaration);
}
/** @type {Node[]} */
const list = /** @type {Node[]} */ (A.bodyOf(parent));
list.splice(list.indexOf(statement), 1);
if (list.length === 1 && A.isBlockStatementNode(parent)) {
unwrapBody(holder, parent, list[0]);
}
}
};
/**
* Whether an expression written first in a statement needs parentheses:
* it opens with a function, a class or an object literal.
* @param {Node} node an expression
* @returns {boolean} true when it does
*/
const opensWithBrace = (node) => {
for (;;) {
if (
A.isFunctionNode(node) ||
A.isClassNode(node) ||
A.isObjectNode(node)
) {
return true;
}
if (A.isCallNode(node) && !A.isNewNode(node)) node = node.callee;
else if (A.isPropAccessNode(node)) node = node.object;
else if (A.isBinaryNode(node) || A.isAssignNode(node)) node = node.left;
else if (A.isConditionalNode(node)) node = node.test;
else if (A.isSequenceNode(node)) node = node.expressions[0];
else if (A.isUnaryPostfixNode(node)) node = node.argument;
else if (A.isChainNode(node)) node = node.expression;
else if (A.isPrefixedTemplateStringNode(node)) node = node.tag;
else return false;
}
};
/**
* `var a; a = b, c()` as `var a = b; c()`: the first write of a `var` the
* statement before declares last, without a value, read into that declaration.
* @param {Node[]} statements a body's statements
* @param {Map<Definition | string, number>} plainWrites each name's `=` writes, by definition or name
* @returns {void}
*/
const assignIntoBareVar = (statements, plainWrites) => {
for (let i = 0; i + 1 < statements.length; i++) {
const declaration = statements[i];
if (!A.isVarNode(declaration)) continue;
const next = statements[i + 1];
const returned = A.isReturnNode(next);
if (!returned && !A.isSimpleStatementNode(next)) continue;
const value = returned ? next.argument : next.expression;
if (!value) continue;
const expressions = A.isSequenceNode(value) ? value.expressions : [value];
// `return a = b` would only move its value.
if (returned && expressions.length === 1) continue;
const [write] = expressions;
if (
!A.isAssignNode(write) ||
(!returned &&
expressions.length > 1 &&
opensWithBrace(expressions[1])) ||
write.operator !== "=" ||
!A.isSymbolRefNode(write.left) ||
// `a = …` written again later gzips as one more repeat of the same text.
/** @type {number} */ (
plainWrites.get(write.left.definition || write.left.name)
) > 1
) {
continue;
}
const { declarations } = declaration;
const index = declarations.findIndex(
(/** @type {Node} */ declarator) =>
!declarator.init &&
A.isSymbolDeclarationNode(declarator.id) &&
declarator.id.name === write.left.name &&
(!write.left.definition ||
write.left.definition === declarator.id.definition)
);
if (index === -1) continue;
// Hoisted alike wherever it stands, a bare name moves last to take the value.
const [declarator] = declarations.splice(index, 1);
declarator.init = write.right;
declarations.push(declarator);
if (expressions.length === 1) {
statements.splice(i + 1, 1);
} else {
const rest =
expressions.length === 2 ? expressions[1] : expressions.slice(1);
const remaining = Array.isArray(rest)
? makeNode(A.SequenceNode, value, { expressions: rest })
: rest;
if (returned) next.argument = remaining;
else next.expression = remaining;
}
// The next write may be another of the declaration's bare names.
i--;
}
};
/**
* `let a = b, { c } = a` as `let { c } = b`: a name read only as the value
* the pattern after it unpacks, where nothing runs between the two.
* @param {Node[]} statements a body's statements
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const collapseIntoPattern = (statements, compressor) => {
for (const statement of statements) {
if (!A.isDefinitionsNode(statement)) continue;
const { declarations } = statement;
for (let j = declarations.length - 1; j > 0; j--) {
const pattern = declarations[j];
const named = declarations[j - 1];
const { init } = pattern;
if (
!A.isDestructuringNode(pattern.id) ||
!init ||
!A.isSymbolRefNode(init) ||
!named.init ||
!A.isSymbolDeclarationNode(named.id) ||
init.definition === undefined ||
init.definition !== named.id.definition
) {
continue;
}
const { definition } = init;
if (
definition.identifiers.length !== 1 ||
definition.assignments !== 0 ||
definition.patternWrites !== 0 ||
definition.scope.isPinned() ||
compressor.exposed(definition)
) {
continue;
}
// Counted afresh: a copy an inlining wrote is no reference terser kept.
let reads = 0;
A.walk(definition.scope.node, (/** @type {Node} */ node) => {
if (A.isSymbolRefNode(node) && node.definition === definition) {
reads++;
}
});
if (reads !== 1) continue;
pattern.init = named.init;
declarations.splice(j - 1, 1);
}
}
};
/**
* Writes a body block left holding one statement as that statement, where
* no `else` after it could be read as the statement's own.
* @param {Node | undefined} holder the statement the block is the body of
* @param {Node} block the block
* @param {Node} statement its one statement
* @returns {void}
*/
const unwrapBody = (holder, block, statement) => {
if (!holder) return;
const exits =
A.isSimpleStatementNode(statement) ||
A.isReturnNode(statement) ||
A.isThrowNode(statement) ||
A.isLoopControlNode(statement);
if (A.isIfNode(holder)) {
if (!exits) return;
if (holder.consequent === block) holder.consequent = statement;
else if (holder.alternate === block) holder.alternate = statement;
return;
}
if (
A.isIterationStatementNode(holder) &&
holder.body === block &&
(exits || A.isIterationStatementNode(statement))
) {
holder.body = statement;
}
};
const compressBeforeJoiningVars = compressorMethods.compress;
/**
* @this {Compressor} the compressor
* @param {Node} toplevel the tree
* @returns {Node} the tree compressed
*/
compressorMethods.compress = function compress(toplevel) {
const compressed = compressBeforeJoiningVars.call(this, toplevel);
if (!improvements.enabled || !this.option("join_vars")) {
return compressed;
}
/** @type {Map<Definition | string, number>} */
const plainWrites = new Map();
A.walk(compressed, (/** @type {Node} */ node) => {
if (
(A.isLambdaNode(node) || A.isToplevelNode(node)) &&
Array.isArray(A.bodyOf(node))
) {
joinBareVars(node);
}
if (
A.isAssignNode(node) &&
node.operator === "=" &&
A.isSymbolRefNode(node.left)
) {
const key = node.left.definition || node.left.name;
plainWrites.set(key, (plainWrites.get(key) || 0) + 1);
}
});
A.walk(compressed, (/** @type {Node} */ node) => {
const statements = A.bodyOf(node);
if (Array.isArray(statements) && !A.isClassNode(node)) {
assignIntoBareVar(statements, plainWrites);
collapseIntoPattern(statements, this);
}
});
return compressed;
};
// The delimiters oxc picks among, frequent enough in code to gzip small.
const SPLIT_DELIMITERS = [".", ",", "(", ")", " "];
// Below this a split gzips worse often enough to lose: oxc's 26 left 28 of
// 69 test262 sources bigger, 60 react-dom and yui; 100 none of 21 bundles.
const SPLIT_MIN_ELEMENTS = 100;
/**
* An array of strings written as one string split, as oxc writes it, or
* undefined where no delimiter is free or the array is too short.
* @param {Node} array an array literal
* @returns {Node | undefined} `"a,b".split(",")`
*/
const splitStringArray = (array) => {
const { elements } = array;
if (elements.length < SPLIT_MIN_ELEMENTS) return undefined;
/** @type {string[]} */
const values = [];
let single = true;
for (const element of elements) {
if (element === null || !A.isStringNode(element)) return undefined;
if (/** @type {string} */ (element.value).length !== 1) single = false;
values.push(/** @type {string} */ (element.value));
}
const delimiter = single
? ""
: SPLIT_DELIMITERS.find((candidate) =>
values.every((value) => !value.includes(candidate))
);
if (delimiter === undefined) return undefined;
return makeNode(A.CallNode, array, {
callee: makeNode(A.DotNode, array, {
object: makeNode(A.StringNode, array, {
value: values.join(delimiter)
}),
optional: false,
property: A.SymbolPropertyNode({ name: "split" }),
computed: false
}),
arguments: [makeNode(A.StringNode, array, { value: delimiter })],
optional: false
});
};
// The compressor that met an array of strings long enough to split, so a
// tree without one is never walked again.
/** @type {Compressor | undefined} */
let longArrayMet;
/**
* Whether a class member is a constructor that does what the one a class
* extending nothing gets by default does, and declares no parameter `length` reads.
* @param {Node} member a class member
* @returns {boolean} true for `constructor() {}`
*/
const isEmptyConstructor = (member) =>
A.isConciseMethodNode(member) &&
!member.static &&
!member.computed &&
(member.key.name === "constructor" || member.key.value === "constructor") &&
/** @type {Node} */ (member.value).params.length === 0 &&
/** @type {Node[]} */ (
/** @type {Node} */ (/** @type {Node} */ (member.value).body).body
).length === 0;
/**
* Whether constructing a class runs nothing: it extends nothing, defining it
* runs nothing, its constructor is the default one and its fields set values.
* @param {Node} node a class expression
* @param {Compressor} compressor the compressor
* @returns {boolean} true when `new` of it can go unread
*/
const constructsPurely = (node, compressor) =>
!node.superClass &&
!A.hasSideEffects(node, compressor) &&
/** @type {Node[]} */ (/** @type {Node} */ (node.body).body).every(
(/** @type {Node} */ member) =>
A.isConciseMethodNode(member)
? member.static ||
member.computed ||
!(
member.key.name === "constructor" ||
member.key.value === "constructor"
) ||
isEmptyConstructor(member)
: !(
A.isClassPropertyNode(member) ||
A.isClassPrivatePropertyNode(member)
) ||
member.static ||
!member.value ||
!A.hasSideEffects(/** @type {Node} */ (member.value), compressor)
);
/**
* Whether constructing a function runs nothing: it is a plain function with
* an empty body, whose parameters bind what they are passed.
* @param {Node} node a callee
* @returns {boolean} true for `function (a) {}`
*/
const constructsNothing = (node) =>
A.isFunctionNode(node) &&
!node.async &&
!node.generator &&
/** @type {Node[]} */ (/** @type {Node} */ (node.body).body).length === 0 &&
node.params.every((/** @type {Node} */ param) =>
A.isSymbolFunargNode(param)
);
/**
* What a `Set` or `Map` built from an array literal, `null` or `undefined`
* evaluates, reading it through no code of the program's: a `Map`'s entries
* each an array literal, and the arguments after the first, which it ignores.
* @param {Node} self a `new`
* @param {Compressor} compressor the compressor
* @returns {Node[] | undefined} the values it evaluates, if it is one
*/
const collectionEntries = (self, compressor) => {
if (
!improvements.enabled ||
!isBuiltInGlobal(self.callee) ||
!readsUnassignedGlobal(self.callee, compressor) ||
(self.callee.name !== "Set" && self.callee.name !== "Map") ||
// A patched `Array.prototype` may run code as the list is iterated.
patchedPrototypes.has("Array") ||
self.arguments.some(A.isExpansionNode)
) {
return;
}
if (self.arguments.length === 0) return [];
const [list, ...ignored] = self.arguments;
if (
A.isNullNode(list) ||
(A.isUnaryPrefixNode(list) && list.operator === "void")
) {
return [list, ...ignored];
}
if (!A.isArrayNode(list)) return;
/** @type {Node[]} */
const values = [];
for (const element of list.elements) {
// Iterating a hole reads its index through `Array.prototype`.
if (!element || A.isExpansionNode(element)) return;
if (self.callee.name === "Map") {
// An entry reads its key and value at 0 and 1, holes included.
if (!A.isArrayNode(element) || element.elements.length < 2) return;
for (const value of element.elements) {
if (!value || A.isExpansionNode(value)) return;
values.push(value);
}
} else {
values.push(element);
}
}
return [...values, ...ignored];
};
A.wrapDropSideEffectFree(
"NewExpression",
(
/** @type {(self: Node, compressor: Compressor) => Node | null} */ original
) =>
(/** @type {Node} */ self, /** @type {Compressor} */ compressor) => {
const entries = collectionEntries(self, compressor);
if (entries !== undefined) {
const left = entries
.map((entry) => A.dropSideEffectFree(entry, compressor))
.filter(Boolean);
return left.length === 0
? null
: makeSequence(self, /** @type {Node[]} */ (left));
}
if (
!improvements.enabled ||
self.arguments.some(A.isExpansionNode) ||
!(A.isClassExpressionNode(self.callee)
? constructsPurely(self.callee, compressor)
: constructsNothing(self.callee))
) {
return original(self, compressor);
}
const left = self.arguments
.map((argument) => A.dropSideEffectFree(argument, compressor))
.filter(Boolean);
return left.length === 0
? null
: makeSequence(self, /** @type {Node[]} */ (left));
}
);
/**
* A class whose static parts run nothing leaves only what it extends, even
* where they read the class: dropped, nothing of them stays to read it.
* @param {Node} self a class
* @param {Compressor} compressor the compressor
* @returns {boolean} true where only what it extends runs with effects
*/
A.runsOnlyHeritage = (self, compressor) => {
if (!improvements.enabled || !self.superClass) return false;
const own = self.id && self.id.definition;
return (
!A.walk(self.superClass, (/** @type {Node} */ node) =>
A.isSymbolRefNode(node) && own && node.definition === own
? A.walk_abort
: undefined
) &&
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body).every(
(/** @type {Node} */ member) => !A.hasSideEffects(member, compressor)
)
);
};
/**
* Whether a tagged template is marked `#__PURE__`, so only its template's
* expressions keep effects, as a marked call's arguments do.
* @param {Node} self a tagged template
* @param {Compressor} compressor the compressor
* @returns {boolean} true where its tag's call can be dropped
*/
const isPureTaggedTemplate = (self, compressor) =>
improvements.enabled &&
compressor.option("side_effects") &&
(self._annotations & A._PURE) !== 0;
A.wrapHasSideEffects(
"TaggedTemplateExpression",
(/** @type {(self: Node, compressor: Compressor) => boolean} */ original) =>
(/** @type {Node} */ self, /** @type {Compressor} */ compressor) =>
isPureTaggedTemplate(self, compressor)
? self.quasi.segments.some(
(/** @type {Node} */ segment) =>
!A.isTemplateSegmentNode(segment) &&
A.hasSideEffects(segment, compressor)
)
: original(self, compressor)
);
A.wrapDropSideEffectFree(
"TaggedTemplateExpression",
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) => {
if (!isPureTaggedTemplate(self, compressor)) {
return original(self, compressor, firstInStatement);
}
// The tag reads the values unconverted, so only their own effects stay.
/** @type {Node[]} */
const kept = [];
for (const segment of self.quasi.segments) {
if (A.isTemplateSegmentNode(segment)) continue;
const node = A.dropSideEffectFree(
segment,
compressor,
firstInStatement && kept.length === 0
);
if (node) kept.push(node);
}
return kept.length > 0 ? makeSequence(self, kept) : null;
}
);
/**
* @param {Node} node an expression
* @returns {boolean} true for `void 0` or `undefined`
*/
const isVoidConstant = (node) =>
A.isUndefinedNode(node) ||
(A.isUnaryPrefixNode(node) &&
node.operator === "void" &&
A.isNumberNode(node.argument));
// Tested for truth, `void 0` reads as `false` does: `c ? void 0 : x` as `!c && x`.
A.wrapOptimizer(
"Conditional",
(optimizeConditional) => (self, compressor) => {
const optimized = optimizeConditional(self, compressor);
if (
!improvements.enabled ||
optimized !== self ||
!A.isConditionalNode(self) ||
compressor.self() !== self ||
!compressor.in_boolean_context()
) {
return optimized;
}
if (isVoidConstant(/** @type {Node} */ (self.consequent))) {
return makeNode(A.BinaryNode, self, {
operator: "&&",
left: A.negate(self.test, compressor),
right: self.alternate
});
}
if (isVoidConstant(self.alternate)) {
return makeNode(A.BinaryNode, self, {
operator: "&&",
left: self.test,
right: /** @type {Node} */ (self.consequent)
});
}
return optimized;
}
);
/**
* Whether a derived class's constructor does what its default one does:
* passes every argument to `super`, and declares no parameter `length` reads.
* @param {Node} member a class member
* @returns {boolean} true for `constructor() { super(...arguments) }`
*/
const isForwardingConstructor = (member) => {
if (
!A.isConciseMethodNode(member) ||
member.static ||
member.computed ||
(member.key.name !== "constructor" && member.key.value !== "constructor")
) {
return false;
}
const { params, body } = /** @type {Node} */ (member.value);
const statement =
/** @type {Node[]} */ (/** @type {Node} */ (body).body).length === 1 &&
/** @type {Node[]} */ (/** @type {Node} */ (body).body)[0];
if (!statement || !A.isSimpleStatementNode(statement)) return false;
const call = statement.expression;
if (
!A.isCallNode(call) ||
!A.isSuperNode(call.callee) ||
call.arguments.length !== 1 ||
!A.isExpansionNode(call.arguments[0])
) {
return false;
}
const spread = call.arguments[0].argument;
if (!A.isSymbolRefNode(spread)) return false;
if (params.length === 0) return spread.name === "arguments";
return (
params.length === 1 &&
A.isExpansionNode(params[0]) &&
A.isSymbolFunargNode(params[0].argument) &&
spread.definition === params[0].argument.definition
);
};
/**
* `a ? f(...b) : f(...c)` as `f(...a ? b : c)`, where terser merges two
* calls differing in one argument only where no argument is spread.
* @param {Node} self a conditional
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the call, none where the two differ otherwise
*/
const mergedSpreadCall = (self, compressor) => {
const { consequent, alternate } = self;
if (
!A.isCallNode(consequent) ||
A.kindOf(alternate) !== A.kindOf(/** @type {Node} */ (consequent)) ||
/** @type {Node} */ (consequent).optional ||
alternate.optional ||
/** @type {Node} */ (consequent).arguments.length !==
alternate.arguments.length ||
!A.isEquivalent(
/** @type {Node} */ (consequent).callee,
alternate.callee
) ||
A.hasSideEffects(self.test, compressor) ||
A.hasSideEffects(/** @type {Node} */ (consequent).callee, compressor)
) {
return;
}
const first = /** @type {Node} */ (consequent).arguments;
const second = alternate.arguments;
let index = -1;
for (let i = 0; i < first.length; i++) {
if (A.isExpansionNode(first[i]) && A.isExpansionNode(second[i])) {
if (A.isEquivalent(first[i], second[i])) {
// Iterating it ran after the test, and now runs before it.
if (index === -1) return;
continue;
}
if (index !== -1) return;
index = i;
} else if (
A.isExpansionNode(first[i]) ||
A.isExpansionNode(second[i]) ||
!A.isEquivalent(first[i], second[i]) ||
// It ran after the test, and now runs before it.
(index === -1 && A.hasSideEffects(first[i], compressor))
) {
return;
}
}
if (index === -1) return;
// Optimized here, it would read the context of the conditional it replaces.
const merged = makeNode(A.ConditionalNode, self, {
test: self.test,
consequent: first[index].argument,
alternate: second[index].argument
});
const call = A.cloneNode(/** @type {Node} */ (consequent));
const spread = A.cloneNode(first[index]);
spread.argument = merged;
call.arguments[index] = spread;
return call;
};
/**
* Whether a name's binding is one no code run elsewhere can rebind or see:
* its own function's, every reference in that function, and no `eval`.
* @param {Node} node a name read
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it is
*/
const readsOwnLocal = (node, compressor) => {
const definition = node.definition;
if (
!definition ||
definition.global ||
definition.scope.isPinned() ||
!readsAlike(node, compressor)
) {
return false;
}
const functionScope = definition.scope.getDefunScope();
// A sloppy parameter is also its `arguments` slot, which code elsewhere may hold.
if (
A.isSymbolFunargNode(definition.identifiers[0]) &&
A.usesArguments(functionScope.node)
) {
return false;
}
return definition.references.every(
(/** @type {Node} */ reference) =>
reference.scope.getDefunScope() === functionScope
);
};
/**
* Whether reading a callee before a conditional's test, not after, runs
* alike: a function or `var` nothing writes, or, where the test reads only
* its own function's names and runs nothing, any name or member path.
* @param {Node} callee the callee both calls share
* @param {Node} test the conditional's test
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it does
*/
const readsCalleeAside = (callee, test, compressor) => {
if (A.isSymbolRefNode(callee) && callee.definition) {
const definition = callee.definition;
const declared = definition.identifiers[0];
if (
compressor.option("reduce_vars") &&
readsAlike(callee, compressor) &&
(A.isSymbolDefunNode(declared) ||
A.isSymbolVarNode(declared) ||
A.isSymbolFunargNode(declared)) &&
!definition.assignments &&
!definition.patternWrites &&
definition.fixed
) {
return true;
}
}
let path = callee;
while (A.isPropAccessNode(path) && !path.optional) {
if (A.isSubNode(path) && !A.isConstantNode(path.property)) return false;
path = path.object;
}
if (!A.isSymbolRefNode(path) && !A.isThisNode(path)) return false;
if (A.hasSideEffects(test, compressor)) return false;
let ownLocals = true;
walkNode(
test,
A.createWalker((/** @type {Node} */ node) => {
if (!ownLocals) return true;
if (A.isLambdaNode(node)) return (ownLocals = false);
if (A.isSymbolRefNode(node) && !readsOwnLocal(node, compressor)) {
ownLocals = false;
}
return undefined;
})
);
return ownLocals;
};
/**
* `a ? o.f(b) : o.f(c)` as `o.f(a ? b : c)`, which terser keeps where the
* callee may run code, that here neither sees nor changes what the test reads.
* @param {Node} self a conditional
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the call, none where reading it first may differ
*/
const mergedCall = (self, compressor) => {
const { consequent, alternate } = self;
if (
!A.isCallNode(consequent) ||
// Two `new` of one class: gzip reads their repeated text cheaper.
A.isNewNode(/** @type {Node} */ (consequent)) ||
A.kindOf(alternate) !== A.kindOf(/** @type {Node} */ (consequent)) ||
/** @type {Node} */ (consequent).optional ||
alternate.optional ||
/** @type {Node} */ (consequent).arguments.length !==
alternate.arguments.length ||
!A.isEquivalent(/** @type {Node} */ (consequent).callee, alternate.callee)
) {
return;
}
const first = /** @type {Node} */ (consequent).arguments;
const second = alternate.arguments;
let index = -1;
for (let i = 0; i < first.length; i++) {
if (A.isExpansionNode(first[i]) || A.isExpansionNode(second[i])) return;
if (!A.isEquivalent(first[i], second[i])) {
if (index !== -1) return;
index = i;
} else if (index === -1 && !A.isConstantNode(first[i])) {
// It ran after the test, and now runs before it.
return;
}
}
if (
index === -1 ||
!readsCalleeAside(
/** @type {Node} */ (consequent).callee,
self.test,
compressor
)
) {
return;
}
const call = A.cloneNode(/** @type {Node} */ (consequent));
call.arguments[index] = makeNode(A.ConditionalNode, self, {
test: self.test,
consequent: first[index],
alternate: second[index]
});
return call;
};
A.wrapOptimizer(
"Conditional",
(optimizeConditional) => (self, compressor) => {
const optimized = optimizeConditional(self, compressor);
return (
(improvements.enabled &&
A.isConditionalNode(optimized) &&
compressor.option("conditionals") &&
(mergedSpreadCall(optimized, compressor) ||
mergedCall(optimized, compressor))) ||
optimized
);
}
);
/**
* `if (a) break; b()` in the body of a `do … while` that runs once as
* `if (!a) { b() }`: what follows the `break` runs where it would not leave.
* @param {Node} body the body of such a loop
* @returns {void}
*/
const guardAfterLeadingBreak = (body) => {
if (!A.isBlockStatementNode(body)) return;
const statements = /** @type {Node[]} */ (body.body);
const index = statements.findIndex(
(/** @type {Node} */ statement) =>
A.isIfNode(statement) &&
!statement.alternate &&
A.isBreakNode(statement.consequent) &&
!(/** @type {Node} */ (statement.consequent).label)
);
const rest = statements.slice(index + 1);
// A block would hide from the code before it what it declares.
if (
index === -1 ||
rest.length === 0 ||
rest.some(
(/** @type {Node} */ statement) =>
(A.isDefinitionsNode(statement) && !A.isVarNode(statement)) ||
A.isDefunNode(statement) ||
A.isDefClassNode(statement)
)
) {
return;
}
const jump = statements[index];
const guarded = makeNode(A.BlockStatementNode, jump, { body: rest });
// A `break` the block holds leaves the same loop.
guardAfterLeadingBreak(guarded);
statements.splice(
index,
statements.length - index,
makeNode(A.IfNode, jump, {
test: makeNode(A.UnaryPrefixNode, jump.test, {
operator: "!",
argument: jump.test
}),
consequent: guarded,
alternate: null
})
);
};
/**
* A statement without the unlabeled `break` or `continue` it ends with, where
* that only leaves a loop which would end there anyway; `if` branches too.
* @param {Node} statement the last statement of a `do … while` body
* @returns {Node} it without that jump
*/
const withoutTailJump = (statement) => {
if (
(A.isBreakNode(statement) || A.isContinueNode(statement)) &&
!statement.label
) {
return makeNode(A.EmptyStatementNode, statement);
}
const body = /** @type {Node[]} */ (statement.body);
if (A.isBlockStatementNode(statement) && body.length > 0) {
const last = withoutTailJump(body[body.length - 1]);
if (A.isEmptyStatementNode(last)) body.pop();
else body[body.length - 1] = last;
} else if (A.isIfNode(statement)) {
statement.consequent = withoutTailJump(
/** @type {Node} */ (statement.consequent)
);
if (statement.alternate) {
statement.alternate = withoutTailJump(statement.alternate);
} else if (A.isEmptyStatementNode(statement.consequent)) {
return makeNode(A.SimpleStatementNode, statement, {
expression: statement.test
});
}
}
return statement;
};
/**
* `await (a, b)` as `a, await b`, and so for `yield`, where a statement
* drops the parentheses the sequence needed.
* @param {Node} node an expression a statement ends with
* @returns {Node | undefined} the sequence, or undefined
*/
const liftedSuspension = (node) => {
if (A.isSequenceNode(node)) {
const last = liftedSuspension(
node.expressions[node.expressions.length - 1]
);
return (
last &&
makeNode(A.SequenceNode, node, {
expressions: [...node.expressions.slice(0, -1), last]
})
);
}
if (
!(A.isAwaitNode(node) || A.isYieldNode(node)) ||
!node.argument ||
!A.isSequenceNode(node.argument)
) {
return undefined;
}
const { expressions } = node.argument;
return makeNode(A.SequenceNode, node, {
expressions: [
...expressions.slice(0, -1),
makeNode(A.isAwaitNode(node) ? A.AwaitNode : A.YieldNode, node, {
...node,
argument: expressions[expressions.length - 1]
})
]
});
};
A.wrapOptimizer(
"SimpleStatement",
(optimizeStatement) => (self, compressor) => {
const optimized = optimizeStatement(self, compressor);
if (!improvements.enabled || !A.isSimpleStatementNode(optimized)) {
return optimized;
}
const lifted = liftedSuspension(optimized.expression);
if (lifted) optimized.expression = lifted;
return optimized;
}
);
/**
* Whether a property of an object literal spread into another one lands the
* same written in it: not a getter, a setter, a prototype or a `super` read.
* @param {Node} property a property of the spread literal
* @returns {boolean} true when it does
*/
const movesIntoLiteral = (property) => {
if (A.isExpansionNode(property)) return true;
if (A.isObjectKeyValNode(property)) {
return (
property.computed ||
property.shorthand ||
A.terserKey(property.key) !== "__proto__"
);
}
return (
A.isConciseMethodNode(property) &&
!A.walk(property, (/** @type {Node} */ node) =>
A.isSuperNode(node) ? A.walk_abort : undefined
)
);
};
A.wrapOptimizer("Object", (optimizeObject) => (self, compressor) => {
const optimized = optimizeObject(self, compressor);
if (!improvements.enabled || !A.isObjectNode(optimized)) return optimized;
const { properties } = optimized;
for (let i = 0; i < properties.length; i++) {
const property = properties[i];
if (!A.isExpansionNode(property)) continue;
const { argument } = property;
if (
A.isObjectNode(argument) &&
argument.properties.every(movesIntoLiteral)
) {
properties.splice(i, 1, ...argument.properties);
i--;
} else if (A.isFunctionNode(argument) || A.isArrowNode(argument)) {
// A function's own properties are not enumerable.
properties.splice(i, 1);
i--;
}
}
return optimized;
});
/**
* The constant a call of a known function returns, its body one `return`
* and every argument a constant: the parameters read as those constants.
* @param {Node} call an optimized call
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the constant, where it writes shorter
*/
const calledConstant = (call, compressor) => {
const reference = call.callee;
if (
!A.isSymbolRefNode(reference) ||
!reference.definition ||
A.isNewNode(call) ||
call.optional ||
call._annotations & A._NOINLINE ||
!compressor.option("reduce_vars") ||
!compressor.option("evaluate")
) {
return undefined;
}
const fixed = A.fixedValue(reference);
if (!fixed || !A.isLambdaNode(fixed)) return undefined;
const lambda = fixed;
if (
lambda.async ||
lambda.generator ||
A.usesArguments(lambda) ||
A.containsThis(lambda)
) {
return undefined;
}
const body = /** @type {Node[]} */ (/** @type {Node} */ (lambda.body).body);
if (body.length !== 1 || !A.isReturnNode(body[0]) || !body[0].argument) {
return undefined;
}
const { params } = lambda;
const args = call.arguments;
/** @type {EvaluatedValue[]} */
const values = [];
for (let i = 0; i < Math.max(params.length, args.length); i++) {
if (i < params.length && !A.isSymbolFunargNode(params[i])) {
return undefined;
}
if (i >= args.length) {
values.push(undefined);
continue;
}
if (A.isExpansionNode(args[i]) || A.hasSideEffects(args[i], compressor)) {
return undefined;
}
const value = A.evaluate(args[i], compressor);
if (
A.isSyntaxNode(value) ||
(typeof value === "object" && value !== null)
) {
return undefined;
}
values.push(/** @type {EvaluatedValue} */ (value));
}
const definitions = params.map(
(/** @type {Node} */ parameter) => parameter.definition
);
const saved = definitions.map(
(/** @type {Definition} */ definition) => definition.fixed
);
let result;
try {
for (const [i, definition] of definitions.entries()) {
definition.fixed = makeNodeFromConstant(values[i], call);
}
result = A.evaluate(body[0].argument, compressor);
} finally {
for (const [i, definition] of definitions.entries()) {
definition.fixed = saved[i];
}
}
if (
A.isSyntaxNode(result) ||
(typeof result === "object" && result !== null)
) {
return undefined;
}
const constant = A.optimize(makeNodeFromConstant(result, call), compressor);
return A.nodeSize(constant, compressor) <= A.nodeSize(call, compressor)
? constant
: undefined;
};
A.wrapOptimizer("Call", (optimizeCall) => (self, compressor) => {
const optimized = optimizeCall(self, compressor);
return (
(improvements.enabled &&
A.isCallNode(optimized) &&
calledConstant(optimized, compressor)) ||
optimized
);
});
/**
* What an optional chain short-circuits into, where an optional link reads
* `void x` or a sequence ending in `null` or `undefined`: their effects.
* @param {Node} chain an optional chain
* @returns {Node | undefined} what replaces the chain, or undefined
*/
const shortCircuitOf = (chain) => {
let link = chain.expression;
while (A.isPropAccessNode(link) || A.isCallNode(link)) {
const inner = A.isCallNode(link) ? link.callee : link.object;
if (link.optional) {
if (A.isUnaryPrefixNode(inner) && inner.operator === "void") {
return inner;
}
const tail = A.isSequenceNode(inner)
? inner.expressions[inner.expressions.length - 1]
: undefined;
if (
tail !== undefined &&
(A.isNullNode(tail) ||
A.isUndefinedNode(tail) ||
(A.isUnaryPrefixNode(tail) && tail.operator === "void"))
) {
return modules.common.make_sequence(inner, [
...inner.expressions.slice(0, -1),
A.isUnaryPrefixNode(tail) ? tail : modules.utils.make_void_0(tail)
]);
}
}
link = inner;
}
return undefined;
};
/**
* Makes plain each optional link of a chain whose base is never `null` or
* `undefined`, as `42?.toString()` reads the same as `42..toString()`.
* @param {Node} chain an optional chain
* @param {Compressor} compressor the compressor
* @returns {Node} the chain, or its expression where no link stays optional
*/
const withoutNeedlessOptionals = (chain, compressor) => {
let optional = false;
let link = chain.expression;
while (A.isPropAccessNode(link) || A.isCallNode(link)) {
const inner = A.isCallNode(link) ? link.callee : link.object;
if (link.optional) {
if (isNotNullish(inner, compressor)) link.optional = false;
else optional = true;
}
link = inner;
}
return optional ? chain : chain.expression;
};
A.wrapOptimizer("Chain", (optimizeChain) => (self, compressor) => {
const optimized = optimizeChain(self, compressor);
if (!improvements.enabled || !A.isChainNode(optimized)) return optimized;
return (
shortCircuitOf(optimized) ||
withoutNeedlessOptionals(optimized, compressor)
);
});
// `delete` of a declared binding, or of `undefined`, `NaN` or `Infinity`,
// deletes nothing, where no `with` or `eval` can bind the name otherwise.
A.wrapOptimizer("UnaryPrefix", (optimizeUnary) => (self, compressor) => {
const optimized = optimizeUnary(self, compressor);
if (
!improvements.enabled ||
!A.isUnaryPrefixNode(optimized) ||
optimized.operator !== "delete" ||
globalMayBeRebound(compressor)
) {
return optimized;
}
const { argument } = optimized;
const undeletable = A.isSymbolRefNode(argument)
? isUndeclaredRef(argument)
? UNDELETABLE_GLOBALS.has(argument.name)
: argument.definition !== undefined
: A.isUndefinedNode(argument) ||
A.isNaNNode(argument) ||
A.isInfinityNode(argument);
return undeletable ? makeNode(A.FalseNode, optimized) : optimized;
});
// `typeof (a === b)` as `"boolean"`: the type of what nothing else observes.
A.wrapOptimizer("UnaryPrefix", (optimizeUnary) => (self, compressor) => {
const optimized = optimizeUnary(self, compressor);
if (
!improvements.enabled ||
!A.isUnaryPrefixNode(optimized) ||
optimized.operator !== "typeof" ||
A.isConstantNode(optimized.argument) ||
A.hasSideEffects(optimized.argument, compressor)
) {
return optimized;
}
const { argument } = optimized;
const type = A.isClassNode(argument)
? "function"
: A.isBoolean(argument)
? "boolean"
: A.isNumber(argument, compressor)
? "number"
: A.isString(argument, compressor)
? "string"
: undefined;
return type === undefined
? optimized
: makeNode(A.StringNode, optimized, { value: type });
});
// `!(a != b && c != d)` as `a == b || c == d`, where negating the comparisons
// leaves fewer negations. Measured: the other way, `!=` for `==`, gzips worse.
A.wrapOptimizer("UnaryPrefix", (optimizeUnary) => (self, compressor) => {
const optimized = optimizeUnary(self, compressor);
if (
!improvements.enabled ||
!A.isUnaryPrefixNode(optimized) ||
optimized.operator !== "!" ||
!A.isBinaryNode(optimized.argument) ||
!isComparisonLogic(optimized.argument)
) {
return optimized;
}
const negated = A.negate(optimized.argument, compressor);
return isComparisonLogic(negated) &&
negationsIn(negated) < negationsIn(optimized.argument) + 1 &&
A.nodeSize(negated, compressor) < A.nodeSize(optimized, compressor)
? negated
: optimized;
});
/**
* How many negations a comparison tree writes: each `!`, `!=` and `!==`.
* @param {Node} node a comparison tree
* @returns {number} the count
*/
const negationsIn = (node) =>
A.isUnaryPrefixNode(node) && node.operator === "!"
? 1 + negationsIn(node.argument)
: A.isBinaryNode(node)
? Number(node.operator === "!=" || node.operator === "!==") +
negationsIn(node.left) +
negationsIn(node.right)
: 0;
const COMPARISONS = new Set([
"==",
"!=",
"===",
"!==",
"<",
">",
"<=",
">=",
"in",
"instanceof"
]);
/**
* Whether a node is a boolean by its shape: a comparison, a negation, or
* `&&` and `||` of them.
* @param {Node} node an expression
* @returns {boolean} true when it is
*/
const isComparisonLogic = (node) =>
(A.isUnaryPrefixNode(node) && node.operator === "!") ||
(A.isBinaryNode(node) &&
(COMPARISONS.has(node.operator) ||
((node.operator === "&&" || node.operator === "||") &&
isComparisonLogic(node.left) &&
isComparisonLogic(node.right))));
A.wrapOptimizer("Do", (optimizeDo) => (self, compressor) => {
if (
improvements.enabled &&
compressor.option("loops") &&
!A.hasSideEffects(self.test, compressor)
) {
const condition = A.evaluate(self.test, compressor);
if (!A.isSyntaxNode(condition) && !condition) {
self.body = withoutTailJump(/** @type {Node} */ (self.body));
guardAfterLeadingBreak(self.body);
}
}
return optimizeDo(self, compressor);
});
A.wrapOptimizer("Import", (optimizeImport) => (self, compressor) => {
const optimized = optimizeImport(self, compressor);
if (
!improvements.enabled ||
!A.isImportNode(optimized) ||
!compressor.option("unused") ||
!compressor.toplevel.vars
) {
return optimized;
}
const read = (/** @type {Node} */ symbol) => !A.isUnreferenced(symbol);
// A deferred or source-phase import still loads its module, which may
// fail, yet cannot be written without its name, so it stays whole.
if (optimized.phase) return optimized;
if (optimized.imported_name && !read(optimized.imported_name)) {
optimized.imported_name = NO_NODE;
}
if (optimized.imported_names) {
const kept = optimized.imported_names.filter(
(/** @type {Node} */ mapping) => read(mapping.local)
);
optimized.imported_names =
kept.length > 0
? kept
: /** @type {Node[]} */ (/** @type {Node[] | null} */ (null));
}
return optimized;
});
A.wrapOptimizer("Class", (optimizeClass) => (self, compressor) => {
const optimized = optimizeClass(self, compressor);
if (!improvements.enabled) return optimized;
const members = /** @type {Node} */ (optimized.body).body;
const index = /** @type {Node[]} */ (members).findIndex(
optimized.superClass ? isForwardingConstructor : isEmptyConstructor
);
if (index !== -1) /** @type {Node[]} */ (members).splice(index, 1);
dropUnreadPrivateFields(optimized, compressor);
return optimized;
});
/**
* Drops each private field nothing in its class reads, whose value has no
* effects. A derived class keeps its instance ones, which throw when `super()`
* returns an object already holding them.
* @param {Node} self a class
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const dropUnreadPrivateFields = (self, compressor) => {
const members = /** @type {Node} */ (self.body).body;
if (!(/** @type {Node[]} */ (members).some(A.isClassPrivatePropertyNode))) {
return;
}
/** @type {Set<string>} */
const read = new Set();
let readsAll = false;
A.walk(self, (/** @type {Node} */ node) => {
if (A.isDotHashNode(node)) {
read.add(node.property.name);
} else if (A.isPrivateInNode(node)) {
read.add(node.left.name);
}
// A direct `eval` in the class may name any of them.
else if (
A.isCallNode(node) &&
A.isSymbolRefNode(node.callee) &&
node.callee.name === "eval"
) {
readsAll = true;
return A.walk_abort;
}
});
if (readsAll) return;
/** @type {Node} */ (self.body).body = /** @type {Node[]} */ (
members
).filter(
(/** @type {Node} */ member) =>
!A.isClassPrivatePropertyNode(member) ||
read.has(member.key.name) ||
(!member.static && Boolean(self.superClass)) ||
(member.value !== null &&
member.value !== undefined &&
A.hasSideEffects(/** @type {Node} */ (member.value), compressor))
);
};
// A name nothing reads stops `unsafe_arrows` only until `unused` drops it,
// so drop it first and the arrow comes in the same pass.
A.wrapOptimizer("Function", (optimizeFunction) => (self, compressor) => {
const { id } = self;
if (
!improvements.enabled ||
!id ||
!compressor.option("unsafe_arrows") ||
!compressor.option("unused") ||
keepName(compressor.option("keep_fnames"), id.name) ||
!A.isUnreferenced(id)
) {
return optimizeFunction(self, compressor);
}
self.id = /** @type {Node} */ (/** @type {Node | null} */ (null));
const optimized = optimizeFunction(self, compressor);
if (optimized === self) self.id = id;
return optimized;
});
// A function made where only its truth is read is `true`, made for nothing.
for (const kind of ["Function", "Arrow"]) {
A.wrapOptimizer(kind, (optimizeLambda) => (self, compressor) => {
const optimized = optimizeLambda(self, compressor);
return improvements.enabled &&
A.isLambdaNode(optimized) &&
compressor.in_boolean_context()
? makeNode(A.TrueNode, optimized)
: optimized;
});
}
A.wrapOptimizer("Array", (optimizeArray) => (self, compressor) => {
const optimized = optimizeArray(self, compressor);
if (
longArrayMet !== compressor &&
A.isArrayNode(optimized) &&
optimized.elements.length >= SPLIT_MIN_ELEMENTS &&
optimized.elements.every(
(/** @type {Node | null} */ element) =>
element !== null && A.isStringNode(element)
)
) {
longArrayMet = compressor;
}
return optimized;
});
// Run once terser is done, which reads an array literal (`[…][i]`,
// `[…].join()`) where it would not read the split.
const compressThenSplit = compressorMethods.compress;
/**
* @this {Compressor} the compressor
* @param {Node} toplevel the tree
* @returns {Node} the tree compressed
*/
compressorMethods.compress = function compress(toplevel) {
const compressed = compressThenSplit.call(this, toplevel);
const met = longArrayMet === this;
longArrayMet = undefined;
if (!improvements.enabled || !met) return compressed;
return A.transformNode(
compressed,
A.createTransformer((/** @type {Node} */ node) =>
A.isArrayNode(node) ? splitStringArray(node) : undefined
)
);
};
const foldedTables = require("./syntax-printer-data").foldedBuiltIns();
/** @type {Map<string, Set<string>>} */
const foldedStatics = new Map();
for (const owner of Object.keys(foldedTables.statics)) {
foldedStatics.set(owner, new Set(foldedTables.statics[owner]));
}
const foldedGlobals = new Set(foldedTables.globals);
const foldedStringMethods = new Set(foldedTables.methods.String);
const foldedArrayMethods = new Set(foldedTables.methods.Array);
const foldedNumberMethods = new Set(foldedTables.methods.Number);
const unicodeDependentMethods = new Set(foldedTables.unicodeDependent);
const sizedByArgumentMethods = new Set(foldedTables.sizedByArgument);
// Every name a folded call can end in, for the check every call meets.
const foldedNames = new Set([
...foldedTables.globals,
...Object.keys(foldedTables.statics).reduce(
(names, owner) => [...names, ...foldedTables.statics[owner]],
/** @type {string[]} */ ([])
),
...foldedTables.methods.String,
...foldedTables.methods.Array,
...foldedTables.methods.Number,
"replaceAll"
]);
/** @type {(value: ConstantValue, orig: Node) => Node} */
const makeNodeFromConstant = modules.common.make_node_from_constant;
/** @type {(node: Node) => boolean} */
const isUndeclaredRef = modules.inference.is_undeclared_ref;
/**
* The primitive a literal stands for, as terser writes literals while it
* compresses, or `NOT_CONSTANT`.
* @param {Node} node an expression
* @returns {EvaluatedValue} its value
*/
const constantOf = (node) => {
if (A.isStringNode(node) || A.isNumberNode(node)) {
return /** @type {string | number} */ (node.value);
}
if (A.isTrueNode(node)) return true;
if (A.isFalseNode(node)) return false;
if (A.isNullNode(node)) return null;
if (A.isNaNNode(node)) return Number.NaN;
if (A.isInfinityNode(node)) return Infinity;
if (A.isUnaryPrefixNode(node)) {
const inner = constantOf(node.argument);
if (inner === NOT_CONSTANT) return NOT_CONSTANT;
if (node.operator === "-" && typeof inner === "number") return -inner;
if (node.operator === "!") return !inner;
if (node.operator === "void") return undefined;
}
// The compressor writes `Infinity` as `1/0` and `NaN` as `0/0`.
if (A.isBinaryNode(node) && node.operator === "/") {
const left = constantOf(node.left);
const right = constantOf(node.right);
if (typeof left === "number" && typeof right === "number") {
return left / right;
}
}
return NOT_CONSTANT;
};
/**
* `String.prototype.replaceAll` of a string by a string, written out, as the
* oldest Node webpack builds on lacks it; refuses a `$` pattern.
* @this {string}
* @param {EvaluatedValue} search what to replace
* @param {EvaluatedValue} replacement what replaces it
* @returns {string} the string with every match replaced
*/
function replaceAllStrings(search, replacement) {
if (
typeof search !== "string" ||
typeof replacement !== "string" ||
replacement.includes("$")
) {
throw new TypeError("not folded");
}
if (search !== "") return this.split(search).join(replacement);
// An empty search matches between UTF-16 code units, not code points.
let replaced = replacement;
for (let i = 0; i < this.length; i++) replaced += this[i] + replacement;
return replaced;
}
/**
* `Number.prototype.toString`, refusing a fraction in a radix other than
* ten, whose digits the spec leaves to each engine.
* @this {number}
* @param {EvaluatedValue=} radix the radix
* @returns {string} the number's digits
*/
function numberToString(radix) {
if (
radix !== undefined &&
Number(radix) !== 10 &&
Number.isFinite(this) &&
!Number.isInteger(this)
) {
throw new RangeError("engine-defined digits");
}
return Number.prototype.toString.call(this, /** @type {number=} */ (radix));
}
/**
* The string literal a name holds wherever it is read, as `reduce_vars`
* found it set once to one.
* @param {Node} node an expression
* @returns {Node | undefined} the literal, or undefined
*/
const stringHeldBy = (node) => {
if (
!A.isSymbolRefNode(node) ||
!node.definition ||
node.definition.undeclared ||
!node.definition.fixed
) {
return undefined;
}
const value = A.fixedValue(node);
return value && A.isStringNode(value) ? value : undefined;
};
/**
* The built-in a call names and the value it is called on, when both are
* known: a method of a literal, or a function of an undeclared global.
* @param {Node} call a call
* @param {Compressor} compressor the compressor
* @returns {{ fn: BuiltInFunction, receiver: EvaluatedValue, sizedByArgument: boolean } | undefined} the built-in
*/
const builtInOf = (call, compressor) => {
const callee = call.callee;
if (A.isSymbolRefNode(callee)) {
return readsUnassignedGlobal(callee, compressor) &&
foldedGlobals.has(callee.name)
? {
fn: /** @type {BuiltInTable} */ (
/** @type {typeof globalThis | BuiltInTable} */ (global)
)[callee.name],
receiver: undefined,
sizedByArgument: false
}
: undefined;
}
if (!A.isDotNode(callee) || callee.optional) return undefined;
const { object } = callee;
/** @type {string} */
const name = callee.property.name;
const fixedString = stringHeldBy(object);
if (fixedString !== undefined) {
return builtInOf(
makeNode(A.CallNode, call, {
callee: makeNode(A.DotNode, callee, {
object: fixedString,
property: callee.property
}),
arguments: call.arguments
}),
compressor
);
}
if (A.isSymbolRefNode(object)) {
const methods =
readsUnassignedGlobal(object, compressor) &&
!patchedStatics.has(object.name)
? foldedStatics.get(object.name)
: undefined;
return methods && methods.has(name)
? {
fn: /** @type {{ [key: string]: BuiltInTable }} */ (
/** @type {typeof globalThis | { [key: string]: BuiltInTable }} */ (
global
)
)[object.name][name],
receiver: undefined,
sizedByArgument: false
}
: undefined;
}
if (A.isStringNode(object)) {
// Case and white space follow the engine's Unicode version, but for
// the scripts whose case mapping Unicode settled long ago.
const text = /** @type {string} */ (object.value);
if (
unicodeDependentMethods.has(name) &&
!ASCII.test(text) &&
!(CASE_METHODS.has(name) && SETTLED_CASE.test(text))
) {
return undefined;
}
if (name === "replaceAll") {
return {
fn: replaceAllStrings,
receiver: text,
sizedByArgument: false
};
}
return foldedStringMethods.has(name)
? {
fn: /** @type {BuiltInTable} */ (
/** @type {{ valueOf(): string } | BuiltInTable} */ (
String.prototype
)
)[name],
receiver: /** @type {string} */ (object.value),
sizedByArgument: sizedByArgumentMethods.has(name)
}
: undefined;
}
if (
A.isArrayNode(object) &&
foldedArrayMethods.has(name) &&
!patchedPrototypes.has("Array")
) {
// A hole stays one, which `concat` keeps and `join` reads as empty.
/** @type {EvaluatedArray} */
const elements = [];
elements.length = object.elements.length;
for (let i = 0; i < object.elements.length; i++) {
const element = object.elements[i];
if (element === null) continue;
const value = valueOf(element);
if (value === NOT_CONSTANT) return undefined;
elements[i] = value;
}
return {
fn: /** @type {BuiltInTable} */ (
/** @type {EvaluatedArray | BuiltInTable} */ (Array.prototype)
)[name],
receiver: elements,
sizedByArgument: false
};
}
if (foldedNumberMethods.has(name)) {
const value = constantOf(object);
return typeof value === "number"
? {
fn:
name === "toString"
? numberToString
: /** @type {BuiltInTable} */ (
/** @type {{ valueOf(): number } | BuiltInTable} */ (
Number.prototype
)
)[name],
receiver: value,
sizedByArgument: false
}
: undefined;
}
return undefined;
};
/**
* Whether a `with` or a direct `eval` around the node visited could bind a
* global's name to something else.
* @param {Compressor} compressor the compressor
* @returns {boolean} true when one could
*/
const globalMayBeRebound = (compressor) => {
let parent;
for (let i = 0; (parent = compressor.parent(i)); i++) {
if (A.isWithNode(parent)) return true;
if (A.isScopeNode(parent) && A.usesEval(parent)) return true;
}
return false;
};
/**
* Whether a reference reads a built-in global the program never assigns, a
* pattern or loop head included, where no `with` or direct `eval` can rebind it.
* @param {Node} node a reference
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it reads the built-in
*/
const readsUnassignedGlobal = (node, compressor) =>
isUndeclaredRef(node) &&
compressor.option("reduce_vars") &&
node.definition.assignments === 0 &&
node.definition.patternWrites === 0 &&
!isReboundGlobal(node.name) &&
!globalMayBeRebound(compressor);
/**
* Whether the program may assign the global through the global object, as
* `globalThis.Number = n` or `globalThis[key] = n`.
* @param {string} name a global's name
* @returns {boolean} true where it may
*/
const isReboundGlobal = (name) =>
reboundGlobals.has(name) || reboundGlobals.has(ANY_GLOBAL);
/**
* The value an argument stands for: a literal, or an array literal of such
* values without holes, or `NOT_CONSTANT`.
* @param {Node} node an argument
* @returns {EvaluatedValue} its value
*/
const valueOf = (node) => {
if (!A.isArrayNode(node)) return constantOf(node);
// A patched `Array.prototype` may read or convert the array otherwise.
if (patchedPrototypes.has("Array")) return NOT_CONSTANT;
/** @type {EvaluatedArray} */
const values = [];
for (const element of node.elements) {
const value = element === null ? NOT_CONSTANT : valueOf(element);
if (value === NOT_CONSTANT) return NOT_CONSTANT;
values.push(value);
}
return values;
};
/**
* The literal a folded call's answer is written as, an array of answers
* included, or undefined for an array with holes, which no literal keeps.
* @param {EvaluatedValue} value the answer
* @param {Node} orig where its position comes from
* @returns {Node | undefined} the literal
*/
const nodeOf = (value, orig) => {
if (!Array.isArray(value)) return makeNodeFromConstant(value, orig);
/** @type {Node[]} */
const elements = [];
for (let i = 0; i < value.length; i++) {
const element = i in value ? nodeOf(value[i], orig) : undefined;
if (!element) return undefined;
elements.push(element);
}
return makeNode(A.ArrayNode, orig, { elements });
};
/**
* A call of a built-in on literals, evaluated as it runs when built-ins are
* left as the language defines them, or the call where that is not shorter.
* @param {Node} call a call terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node} what replaces it
*/
const foldBuiltInCall = (call, compressor) => {
const builtIn = builtInOf(call, compressor);
if (!builtIn) return call;
const limit = modules.printToString(call).length;
/** @type {EvaluatedArray} */
const args = [];
for (const arg of call.arguments) {
const value = valueOf(arg);
if (value === NOT_CONSTANT) return call;
// A count or length past the call's own size, as in `"a".repeat(1e9)`,
// answers nothing shorter, so the method is never run.
if (
builtIn.sizedByArgument &&
(typeof value === "number" || typeof value === "string") &&
Number(value) > limit
) {
return call;
}
args.push(value);
}
if (globalMayBeRebound(compressor)) return call;
/** @type {EvaluatedValue} */
let result;
try {
result = builtIn.fn.apply(builtIn.receiver, args);
} catch (_err) {
return call;
}
// A string prints at least as long as it is, so a longer one is not printed.
if (typeof result === "string" && result.length > limit) return call;
const folded = nodeOf(result, call);
return folded && modules.printToString(folded).length <= limit
? folded
: call;
};
/** @type {Map<string, Set<string>>} */
const foldedConstants = new Map();
for (const owner of Object.keys(foldedTables.constants)) {
foldedConstants.set(owner, new Set(foldedTables.constants[owner]));
}
/** @type {Map<string, Set<string>>} */
const powerOfTwoConstants = new Map();
for (const owner of Object.keys(foldedTables.powerOfTwoConstants)) {
powerOfTwoConstants.set(
owner,
new Set(foldedTables.powerOfTwoConstants[owner])
);
}
const isLhs = modules.inference.is_lhs;
/**
* A power of two, or one off it, written so from ECMAScript 2016:
* `Number.MAX_SAFE_INTEGER` as `2**53-1`, `Number.MIN_SAFE_INTEGER` as `1-2**53`.
* @param {number} value `2**k`, `2**k-1` or `1-2**k`, as the generator checks
* @param {Node} orig where its position comes from
* @returns {Node} the expression
*/
const powerOfTwoOf = (value, orig) => {
const exponent = Math.round(Math.log2(Math.abs(value)));
const power = makeNode(A.BinaryNode, orig, {
operator: "**",
left: makeNodeFromConstant(2, orig),
right: makeNodeFromConstant(exponent, orig)
});
if (2 ** exponent === value) return power;
const one = makeNodeFromConstant(1, orig);
return makeNode(A.BinaryNode, orig, {
operator: "-",
left: value > 0 ? power : one,
right: value > 0 ? one : power
});
};
/**
* `/a\/b/i.source` as `"a\\/b"`, the pattern as the engine writes it back.
* @param {Node} node a property read
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the string, if it reads a literal's `source`
*/
const regExpSourceOf = (node, compressor) => {
if (
!A.isDotNode(node) ||
node.optional ||
node.property.name !== "source" ||
!A.isRegExpNode(node.object) ||
isLhs(compressor.self(), compressor.parent()) ||
isPatternTarget(compressor.self(), compressor, A) ||
!compressor.option("evaluate")
) {
return undefined;
}
const { source, flags } = /** @type {RegExp} */ (node.object.value);
try {
return makeNode(A.StringNode, node, {
value: new RegExp(source, flags).source
});
} catch (_err) {
return undefined;
}
};
A.wrapOptimizer("Dot", (optimizeDot) => (self, compressor) => {
const optimized = optimizeDot(self, compressor);
const source =
improvements.enabled && regExpSourceOf(optimized, compressor);
if (source) return source;
if (
!improvements.enabled ||
!A.isDotNode(optimized) ||
optimized.optional ||
!A.isSymbolRefNode(optimized.object) ||
!compressor.option("evaluate")
) {
return optimized;
}
const owner = optimized.object.name;
const name = optimized.property.name;
const names = foldedConstants.get(owner);
const powers =
compressor.option("ecma") >= 2016
? powerOfTwoConstants.get(owner)
: undefined;
const asPower = powers !== undefined && powers.has(name);
if (
!(asPower || (names !== undefined && names.has(name))) ||
!readsUnassignedGlobal(optimized.object, compressor) ||
isLhs(self, compressor.parent()) ||
isPatternTarget(self, compressor, A)
) {
return optimized;
}
/** @type {number} */
const value = /** @type {{ [key: string]: { [key: string]: number } }} */ (
/** @type {typeof globalThis | { [key: string]: { [key: string]: number } }} */ (
global
)
)[owner][name];
const folded = asPower
? powerOfTwoOf(value, optimized)
: makeNodeFromConstant(value, optimized);
return modules.printToString(folded).length <
modules.printToString(optimized).length
? folded
: optimized;
});
/**
* `[a, "-", b].join("")` as `a + "-" + b`, where every element is a string,
* number or boolean, which `join` and `+` turn into the same string.
* @param {Node} call a `join` call terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the concatenation, or undefined
*/
const joinAsConcatenation = (call, compressor) => {
const { callee } = call;
const args = call.arguments;
if (
!A.isDotNode(callee) ||
callee.optional ||
!A.isArrayNode(callee.object) ||
args.length > 1 ||
patchedPrototypes.has("Array")
) {
return undefined;
}
const separator = args.length === 0 ? undefined : constantOf(args[0]);
if (separator !== undefined && typeof separator !== "string") {
return undefined;
}
/** @type {(Node | string)[]} */
const terms = [];
/**
* @param {Node | string} term a string or an expression
*/
const append = (term) => {
const last = terms.length - 1;
if (typeof term === "string" && typeof terms[last] === "string") {
terms[last] += term;
} else {
terms.push(term);
}
};
const { elements } = callee.object;
for (let i = 0; i < elements.length; i++) {
if (i > 0) append(separator === undefined ? "," : separator);
const element = elements[i];
if (element === null) continue;
const value = constantOf(element);
if (value === null || value === undefined) continue;
if (value !== NOT_CONSTANT) {
append(String(value));
} else if (
A.isString(element, compressor) ||
A.isNumber(element, compressor) ||
A.isBoolean(element)
) {
append(element);
} else {
return undefined;
}
}
// Holes, `null` and `undefined` alone join as "", left to `foldBuiltInCall`.
if (terms.length === 0) return undefined;
// `+` concatenates once a string comes first or second.
if (
typeof terms[0] !== "string" &&
!A.isString(terms[0], compressor) &&
(terms.length === 1 || typeof terms[1] !== "string")
) {
terms.unshift("");
}
/** @type {Node | undefined} */
let joined;
for (const term of terms) {
const node =
typeof term === "string" ? makeNodeFromConstant(term, call) : term;
joined = joined
? makeNode(A.BinaryNode, call, {
operator: "+",
left: joined,
right: node
})
: node;
}
return joined &&
modules.printToString(joined).length < modules.printToString(call).length
? joined
: undefined;
};
/**
* `"a".concat(b, "c")` as a template, `\`a${b}c\``, from ECMAScript 2015: both
* convert each value with `ToString`, a template as it reads each one.
* @param {Node} call a `concat` call terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the template, or undefined
*/
const concatAsTemplate = (call, compressor) => {
const { callee } = call;
// A template printed as a tag prints its raw text, which no string's is.
if (
!A.isDotNode(callee) ||
callee.optional ||
compressor.option("ecma") < 2015 ||
A.isPrefixedTemplateStringNode(compressor.parent())
) {
return undefined;
}
const receiver = callee.object;
/** @type {Node[]} */
let segments;
if (A.isStringNode(receiver)) {
segments = [
makeNode(A.TemplateSegmentNode, receiver, {
value: receiver.value,
raw: /** @type {string} */ (receiver.value)
})
];
} else if (A.isTemplateStringNode(receiver)) {
segments = [...receiver.segments];
} else {
return undefined;
}
// `concat` converts its arguments once all are read, a template each as
// it reads it, so once one may be an object only constants may follow it.
let readsObject = false;
for (const argument of call.arguments) {
const last = segments[segments.length - 1];
const value = constantOf(argument);
if (value !== NOT_CONSTANT) {
segments[segments.length - 1] = makeNode(A.TemplateSegmentNode, last, {
value: last.value + String(value),
raw: last.value + String(value)
});
continue;
}
if (readsObject || A.isExpansionNode(argument)) return undefined;
readsObject = !(
A.isString(argument, compressor) ||
A.isNumber(argument, compressor) ||
A.isBoolean(argument)
);
segments.push(
argument,
makeNode(A.TemplateSegmentNode, argument, { value: "", raw: "" })
);
}
const template = makeNode(A.TemplateStringNode, call, { segments });
return modules.printToString(template).length <
modules.printToString(call).length
? template
: undefined;
};
/**
* `Math.pow(a, 2)` as `a ** 2` from ECMAScript 2016, where one side alone is
* a number, so no `BigInt` reaches `**` without `Math.pow` throwing too.
* @param {Node} call a `pow` call terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the exponentiation, or undefined
*/
const powAsOperator = (call, compressor) => {
const { callee } = call;
const args = call.arguments;
if (
!A.isDotNode(callee) ||
callee.optional ||
!A.isSymbolRefNode(callee.object) ||
callee.object.name !== "Math" ||
args.length !== 2 ||
A.isExpansionNode(args[0]) ||
A.isExpansionNode(args[1])
) {
return undefined;
}
const base = constantOf(args[0]);
const exponent = constantOf(args[1]);
const numbers =
Number(typeof base === "number") + Number(typeof exponent === "number");
// A whole power a safe integer holds every engine works out exactly.
if (
numbers === 2 &&
Number.isInteger(base) &&
!Object.is(base, -0) &&
Number.isInteger(exponent) &&
/** @type {number} */ (exponent) >= 0 &&
Number.isSafeInteger(
/** @type {number} */ (base) ** /** @type {number} */ (exponent)
) &&
compressor.option("evaluate") &&
readsUnassignedGlobal(callee.object, compressor)
) {
const power = makeNodeFromConstant(
/** @type {number} */ (base) ** /** @type {number} */ (exponent),
call
);
// Digits gzip poorly, where `Math.pow(2,` repeats: measured on test262.
return modules.printToString(power).length * 2 <=
modules.printToString(call).length
? power
: undefined;
}
if (
// Of two numbers the engine's approximation is left to run time, and
// of none only two BigInts, where `Math.pow` throws, read otherwise.
numbers === 2 ||
(numbers === 0 && !compressor.option("unsafe_math")) ||
compressor.option("ecma") < 2016 ||
!readsUnassignedGlobal(callee.object, compressor)
) {
return undefined;
}
return makeNode(A.BinaryNode, call, {
operator: "**",
left: args[0],
right: args[1]
});
};
/**
* `RegExp("a+", "g")` as `/a+/g`, where both strings make a literal the
* targeted ECMAScript reads.
* @param {Node} call a `RegExp` call terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the literal, or undefined
*/
const regExpAsLiteral = (call, compressor) => {
const { callee } = call;
const args = call.arguments;
if (
!A.isSymbolRefNode(callee) ||
call.optional ||
args.length === 0 ||
args.length > 2 ||
!A.isStringNode(args[0]) ||
(args.length === 2 && !A.isStringNode(args[1])) ||
// An escape stays in a string, whose doubled backslashes gzip beside the
// same escapes in other strings: antd's grew written as a literal. A NUL
// stays too, since a literal spells it `\0` and its `source` changes.
/[\\\0\n\r\u2028\u2029]/.test(/** @type {string} */ (args[0].value)) ||
!readsUnassignedGlobal(callee, compressor)
) {
return undefined;
}
/** @type {string} */
const pattern = /** @type {string} */ (args[0].value);
/** @type {string} */
const regExpFlags = /** @type {string} */ (
args.length === 2 ? args[1].value : ""
);
/** @type {string} */
let source;
// The source this Node escapes, which a literal must spell for `source` to
// read alike; the validator, at the targeted edition, refuses newer syntax.
try {
source = new RegExp(pattern, regExpFlags).source;
require("./syntax-parser").Parser.parse(`/${source}/${regExpFlags}`, {
ecmaVersion: /** @type {import("./syntax-parser").EcmaVersion} */ (
compressor.option("ecma")
)
});
} catch (_err) {
return undefined;
}
if (
source.length + regExpFlags.length + 2 >=
modules.printToString(call).length
) {
return undefined;
}
return makeNode(A.RegExpNode, call, {
value: { source, flags: regExpFlags }
});
};
/**
* Whether a member access is a place written: an assignment's, update's or
* loop head's target, or one inside a destructuring.
* @param {Node} access the member access
* @param {{ parent: (n?: number) => Node }} info its ancestors, `access`'s parent first
* @returns {boolean} true when it is written
*/
const writesObjectMember = (access, info) => {
const parent = info.parent(1);
if (
((A.isAssignNode(parent) || A.isDefaultAssignNode(parent)) &&
parent.left === access) ||
(A.isForInNode(parent) && parent.left === access) ||
(A.isUnaryNode(parent) &&
(parent.operator === "++" ||
parent.operator === "--" ||
parent.operator === "delete"))
) {
return true;
}
let depth = 1;
let outer = parent;
while (
outer &&
(A.isObjectPropertyNode(outer) ||
A.isExpansionNode(outer) ||
A.isDefaultAssignNode(outer))
) {
outer = info.parent(++depth);
}
return Boolean(outer) && A.isDestructuringNode(outer);
};
/** @type {WeakMap<Node, Set<string> | true>} */
const writtenObjectMembers = new WeakMap();
/**
* The members of the global `Object` a program may write, or true where
* it may write any: it hands `Object` itself somewhere or writes a computed key.
* @param {Node} toplevel the program
* @returns {Set<string> | true} those members
*/
const objectMembersWritten = (toplevel) => {
const known = writtenObjectMembers.get(toplevel);
if (known !== undefined) return known;
/** @type {Set<string> | true} */
let written = new Set();
A.walk_parent(
toplevel,
(
/** @type {Node} */ node,
/** @type {{ parent: (n?: number) => Node }} */ info
) => {
if (
!A.isSymbolRefNode(node) ||
node.name !== "Object" ||
!isUndeclaredRef(node)
) {
return undefined;
}
const parent = info.parent();
if (A.isPropAccessNode(parent) && parent.object === node) {
if (!writesObjectMember(parent, info)) return undefined;
const key = A.isDotNode(parent)
? parent.property.name
: A.isStringNode(parent.property)
? parent.property.value
: undefined;
if (typeof key === "string") {
/** @type {Set<string>} */ (written).add(key);
return undefined;
}
} else if (
(A.isCallNode(parent) && parent.callee === node) ||
(A.isBinaryNode(parent) && parent.operator === "instanceof") ||
(A.isUnaryPrefixNode(parent) && parent.operator === "typeof")
) {
return undefined;
}
written = true;
return A.walk_abort;
}
);
writtenObjectMembers.set(toplevel, written);
return written;
};
/**
* `Object.keys({ a: 1 })` as `["a"]`, and `values` and `entries` alike, of
* object, array and string literals whose values have no effect.
* @param {Node} call an `Object` call terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the array literal, or undefined
*/
const ownPropertiesAsArray = (call, compressor) => {
const { callee } = call;
const args = call.arguments;
if (
!A.isDotNode(callee) ||
callee.optional ||
call.optional ||
!A.isSymbolRefNode(callee.object) ||
callee.object.name !== "Object" ||
args.length !== 1 ||
!(
A.isObjectNode(args[0]) ||
A.isArrayNode(args[0]) ||
A.isStringNode(args[0])
) ||
!compressor.option("evaluate") ||
!readsUnassignedGlobal(callee.object, compressor)
) {
return undefined;
}
const name = callee.property.name;
const written = objectMembersWritten(
/** @type {Node} */ (compressor.find_parent(A.isToplevelNode))
);
if (written === true || written.has(name)) return undefined;
/** @type {Record<string, Node>} */
const values = Object.create(null);
const argument = args[0];
if (A.isObjectNode(argument)) {
for (const property of argument.properties) {
if (!A.isObjectKeyValNode(property) || property.computed) {
return undefined;
}
const key = A.terserKey(property.key);
const { value } = property;
if (
(typeof key !== "string" && typeof key !== "number") ||
key === "__proto__" ||
// Engines once disagreed on where an integer key past 2 ** 32 - 2 sorts.
/^\d{10}/.test(String(key)) ||
A.hasSideEffects(/** @type {Node} */ (value), compressor) ||
// A function or class takes its name from the key it is defined at.
(name !== "keys" && (A.isLambdaNode(value) || A.isClassNode(value)))
) {
return undefined;
}
values[key] = /** @type {Node} */ (value);
}
} else if (A.isArrayNode(argument)) {
for (let index = 0; index < argument.elements.length; index++) {
const value = argument.elements[index];
if (value === null) continue;
if (
A.isExpansionNode(value) ||
A.hasSideEffects(value, compressor) ||
(name === "keys" && A.dropSideEffectFree(value, compressor))
) {
return undefined;
}
values[index] = value;
}
} else {
const text = /** @type {string} */ (argument.value);
// Boxed strings enumerate UTF-16 code units, not code points.
for (let index = 0; index < text.length; index++) {
values[index] = makeNode(A.StringNode, argument, {
value: text[index]
});
}
}
// The engine orders them: integer keys ascending, then the rest as defined.
const elements = Object.keys(values).map((key) => {
const keyNode = makeNode(A.StringNode, call, { value: key });
if (name === "keys") return keyNode;
if (name === "values") return values[key];
return makeNode(A.ArrayNode, call, { elements: [keyNode, values[key]] });
});
const array = makeNode(A.ArrayNode, call, { elements });
return modules.printToString(array).length <
modules.printToString(call).length
? array
: undefined;
};
A.wrapOptimizer("Call", (optimizeCall) => (self, compressor) => {
const optimized = optimizeCall(self, compressor);
if (
!improvements.enabled ||
!A.isCallNode(optimized) ||
A.isNewNode(optimized)
) {
return optimized;
}
// Every call is met here, nearly none of a built-in, so its name goes first.
const callee = optimized.callee;
/** @type {string} */
const name = A.isDotNode(callee)
? callee.property.name
: A.isSymbolRefNode(callee)
? callee.name
: "";
// A copy of an array literal is the same literal, made once.
if (
name === "slice" &&
optimized.arguments.length === 0 &&
!optimized.optional &&
!callee.optional &&
A.isArrayNode(callee.object) &&
!patchedPrototypes.has("Array")
) {
return callee.object;
}
if (name === "concat") {
const joined = concatOfArrays(optimized, compressor);
if (joined !== undefined) return joined;
}
if (!foldedNames.has(name) || !compressor.option("evaluate")) {
const rewritten =
name === "pow"
? powAsOperator(optimized, compressor)
: name === "RegExp"
? regExpAsLiteral(optimized, compressor)
: name === "keys" || name === "values" || name === "entries"
? ownPropertiesAsArray(optimized, compressor)
: undefined;
return rewritten || optimized;
}
const folded = foldBuiltInCall(optimized, compressor);
return (
(folded === optimized &&
name === "join" &&
joinAsConcatenation(optimized, compressor)) ||
(folded === optimized &&
name === "concat" &&
concatAsTemplate(optimized, compressor)) ||
folded
);
});
const pureTables = require("./syntax-printer-data").pureBuiltIns();
const pureGlobals = new Set(pureTables.globals);
const callableGlobals = new Set(pureTables.callableGlobals);
/** @type {Map<string, Set<string>>} */
const callableStatics = new Map();
for (const owner of Object.keys(pureTables.statics)) {
callableStatics.set(owner, new Set(pureTables.statics[owner]));
}
/**
* The value a call's argument stands for, an array or object literal of
* such values included, or `NOT_CONSTANT`.
* @param {Node} node an argument
* @param {Compressor=} compressor the compressor, to read a built-in's call as its result
* @returns {EvaluatedValue} its value
*/
const argumentValueOf = (node, compressor) => {
if (A.isBigIntNode(node)) return BigInt(/** @type {string} */ (node.value));
if (A.isRegExpNode(node)) {
const { source, flags } = /** @type {RegExp} */ (node.value);
try {
return new RegExp(source, flags);
} catch (_err) {
return NOT_CONSTANT;
}
}
// A call with none but a throw stands for a primitive it returns.
if (compressor !== undefined && A.isCallNode(node)) {
const value = builtInCallValue(node, compressor);
return value !== null &&
(typeof value === "object" || typeof value === "function")
? NOT_CONSTANT
: /** @type {EvaluatedValue} */ (value);
}
if (A.isArrayNode(node)) {
/** @type {EvaluatedArray} */
const values = [];
for (const element of node.elements) {
// A hole reads its index through `Array.prototype`, which may hold a getter.
if (element === null) return NOT_CONSTANT;
const value = argumentValueOf(element, compressor);
if (value === NOT_CONSTANT) return NOT_CONSTANT;
values.push(value);
}
return values;
}
if (A.isObjectNode(node)) {
/** @type {{ [key: string]: EvaluatedValue }} */
const object = {};
for (const property of node.properties) {
const key = A.isObjectKeyValNode(property)
? A.terserKey(property.key)
: undefined;
if (
property.computed ||
typeof key !== "string" ||
key === "__proto__"
) {
return NOT_CONSTANT;
}
const value = argumentValueOf(
/** @type {Node} */ (property.value),
compressor
);
if (value === NOT_CONSTANT) return NOT_CONSTANT;
object[key] = value;
}
return object;
}
const value = constantOf(node);
// A count past this, as in `new Uint8Array("1e10")`, could allocate without
// bound; an infinite one throws a `RangeError` before it allocates.
return (typeof value === "number" || typeof value === "string") &&
Number.isFinite(Number(value)) &&
Math.abs(Number(value)) > MAX_EVALUATED_COUNT
? NOT_CONSTANT
: value;
};
/**
* Whether a node reads a built-in global that nothing in scope shadows.
* @param {Node} node an expression
* @returns {boolean} true when it does
*/
const isBuiltInGlobal = (node) =>
A.isSymbolRefNode(node) &&
pureGlobals.has(node.name) &&
isUndeclaredRef(node);
/**
* Whether reading a built-in global, a property of it or of its prototype
* has no effect, as reading it here without a throw shows.
* @param {Node} node a reference or a property read
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it has none
*/
const readsWithoutEffect = (node, compressor) => {
let object = node;
/** @type {string | undefined} */
let property;
let onPrototype = false;
if (A.isDotNode(object)) {
property = object.property.name;
object = object.object;
if (
A.isDotNode(object) &&
!object.optional &&
object.property.name === "prototype"
) {
onPrototype = true;
object = object.object;
}
}
// Where it runs, ECMAScript 2020 defines `globalThis`, a read of which is
// a read of the global object, whatever the host.
if (
property === undefined &&
A.isSymbolRefNode(object) &&
object.name === "globalThis" &&
isUndeclaredRef(object) &&
compressor.option("ecma") >= 2020 &&
!isReboundGlobal("globalThis") &&
!globalMayBeRebound(compressor)
) {
return true;
}
if (!isBuiltInGlobal(object) || globalMayBeRebound(compressor)) {
return false;
}
try {
const value =
/** @type {{ [key: string]: { [key: string]: BuiltInTable } }} */ (
/** @type {typeof globalThis | { [key: string]: { [key: string]: BuiltInTable } }} */ (
global
)
)[object.name];
const owner = onPrototype ? value.prototype : value;
if (property !== undefined) Reflect.get(owner, property);
} catch (_err) {
return false;
}
return true;
};
/**
* Whether a `RegExp` built from these arguments parses alike in every Node
* webpack builds on, which ECMAScript 2018's grammar and flags guarantee.
* @param {EvaluatedArray} args its pattern and flags
* @returns {boolean} true when it does
*/
const isPortableRegExp = ([given, givenFlags]) => {
if (given === undefined && givenFlags === undefined) return true;
const pattern = given instanceof RegExp ? given.source : given;
const flags =
given instanceof RegExp && givenFlags === undefined
? given.flags
: givenFlags;
if (
typeof pattern !== "string" ||
/[/\n\r\u2028\u2029]/.test(pattern) ||
(flags !== undefined &&
(typeof flags !== "string" || !/^[gimsuy]*$/.test(flags)))
) {
return false;
}
// The base `Parser` always runs the spec's validator, where `parse` lets
// a pattern the host engine accepts through, newer syntax included.
try {
require("./syntax-parser").Parser.parse(`/${pattern}/${flags || ""}`, {
ecmaVersion: 2018
});
} catch (_err) {
return false;
}
return true;
};
/**
* Whether calling or constructing a built-in on literals has no effect, as
* running it here without a throw shows.
* @param {Node} call a call or `new`
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it has none
*/
const callsWithoutEffect = (call, compressor) =>
builtInCallValue(call, compressor) !== NOT_CONSTANT;
/**
* What calling or constructing a built-in on literals returns, run here,
* or `NOT_CONSTANT` where it may have an effect or throws.
* @param {Node} call a call or `new`
* @param {Compressor} compressor the compressor
* @returns {EvaluatedValue} its result
*/
const builtInCallValue = (call, compressor) => {
if (
call.optional ||
// A patched `Array.prototype` may run code as a built-in iterates a list.
(patchedPrototypes.has("Array") &&
call.arguments.some(
(/** @type {Node} */ argument) =>
A.isArrayNode(argument) || A.isExpansionNode(argument)
))
) {
return NOT_CONSTANT;
}
const callee = call.callee;
/** @type {string | undefined} */
let owner;
/** @type {string} */
let name;
// The built-in only where the program never assigns its name, which only
// `reduce_vars` counts: `Set = f` before `new Set([1])` runs `f`.
if (A.isSymbolRefNode(callee)) {
if (
!callableGlobals.has(callee.name) ||
!readsUnassignedGlobal(callee, compressor)
) {
return NOT_CONSTANT;
}
name = callee.name;
} else if (
A.isDotNode(callee) &&
!callee.optional &&
A.isSymbolRefNode(callee.object)
) {
const members = callableStatics.get(callee.object.name);
if (
!members ||
!members.has(callee.property.name) ||
patchedStatics.has(callee.object.name) ||
!readsUnassignedGlobal(callee.object, compressor)
) {
return NOT_CONSTANT;
}
owner = callee.object.name;
name = callee.property.name;
} else {
return NOT_CONSTANT;
}
/** @type {EvaluatedArray} */
const args = [];
// A `Math` function returns a number from any numbers, never throwing, so
// it alone reads an argument computed by another call, whatever it returns.
const results = owner === "Math" ? compressor : undefined;
for (const arg of call.arguments) {
if (A.isExpansionNode(arg)) {
// A string or an array literal spreads by the built-in iterator.
const spread = argumentValueOf(arg.argument, results);
if (typeof spread !== "string" && !Array.isArray(spread)) {
return NOT_CONSTANT;
}
args.push(...spread);
continue;
}
const value = argumentValueOf(arg, results);
if (value === NOT_CONSTANT) return NOT_CONSTANT;
args.push(value);
}
if (globalMayBeRebound(compressor)) return NOT_CONSTANT;
if (owner === undefined && name === "RegExp" && !isPortableRegExp(args)) {
return NOT_CONSTANT;
}
// An entry reads its key and value at 0 and 1, through `Array.prototype`
// or `String.prototype` where it holds neither.
if (
owner === undefined &&
(name === "Map" || name === "WeakMap") &&
Array.isArray(args[0]) &&
!args[0].every(
(entry) =>
(Array.isArray(entry) || typeof entry === "string") &&
entry.length >= 2
)
) {
return NOT_CONSTANT;
}
try {
/** @type {BuiltInTable} */
const target =
owner === undefined
? /** @type {BuiltInTable} */ (
/** @type {typeof globalThis | BuiltInTable} */ (global)
)
: /** @type {{ [key: string]: BuiltInTable }} */ (
/** @type {typeof globalThis | { [key: string]: BuiltInTable }} */ (
global
)
)[owner];
return A.isNewNode(call)
? Reflect.construct(target[name], args)
: Reflect.apply(target[name], target, args);
} catch (_err) {
return NOT_CONSTANT;
}
};
/**
* The reference a `typeof` compared with a string proves declared before
* the right of `&&` or `||` reads it, as in `typeof x != "undefined" && x`.
* @param {Node} node a logical expression
* @returns {Node | undefined} that reference's `typeof` operand
*/
const guardedReference = (node) => {
const test = node.left;
const compare = A.isBinaryNode(test)
? TYPEOF_COMPARISONS[test.operator]
: undefined;
if (!compare) return undefined;
const typeofFirst = A.isUnaryPrefixNode(test.left);
const typeofNode = typeofFirst ? test.left : test.right;
const other = typeofFirst ? test.right : test.left;
if (
!A.isUnaryPrefixNode(typeofNode) ||
typeofNode.operator !== "typeof" ||
!A.isSymbolRefNode(typeofNode.argument) ||
!A.isStringNode(other)
) {
return undefined;
}
const whenUndefined = typeofFirst
? compare("undefined", /** @type {string} */ (other.value))
: compare(/** @type {string} */ (other.value), "undefined");
// `&&` reads the right only where the test holds, `||` where it fails.
return whenUndefined === (node.operator === "||")
? typeofNode.argument
: undefined;
};
/**
* Whether a logical expression reads an undeclared global only where a
* `typeof` before it proved it declared.
* @param {Node} node a binary expression
* @returns {boolean} true when it does
*/
const readsOnlyWhereDeclared = (node) => {
if (node.operator !== "&&" && node.operator !== "||") return false;
const guarded = guardedReference(node);
return (
guarded !== undefined &&
A.isSymbolRefNode(node.right) &&
node.right.name === guarded.name &&
isUndeclaredRef(guarded) &&
isUndeclaredRef(node.right)
);
};
/**
* Whether dropping a node's value leaves nothing to run, by the rules above.
* @param {Node} node a node terser's own drop keeps whole
* @param {Compressor} compressor the compressor
* @returns {boolean} true when nothing is left
*/
const hasNoEffect = (node, compressor) =>
A.isSymbolRefNode(node) || A.isDotNode(node)
? readsWithoutEffect(node, compressor)
: A.isCallNode(node)
? callsWithoutEffect(node, compressor)
: // Neither throws where it parses: a module's, or a function's.
A.isImportMetaNode(node) || A.isNewTargetNode(node);
const dropSideEffectFree = A.dropSideEffectFree;
/**
* terser's `drop_side_effect_free`, dropping as well what the built-ins
* and `typeof` guards above prove has no effect.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left
*/
A.dropSideEffectFree = (node, compressor, firstInStatement) => {
if (
improvements.enabled &&
A.isBinaryNode(node) &&
readsOnlyWhereDeclared(node)
) {
return null;
}
const kept = dropSideEffectFree(node, compressor, firstInStatement);
if (!improvements.enabled) return kept;
if (kept === node && hasNoEffect(node, compressor)) return null;
return (kept && coalesceGuard(kept, compressor)) || kept;
};
/**
* Whether reading any key of a value runs no code of the program's: a
* literal holding no accessor, spread or `__proto__`, or a primitive.
* @param {Node | null} node an expression, or nothing known
* @returns {boolean} true when it runs none
*/
const hasNoGetters = (node) =>
node !== null &&
(A.isArrayNode(node) ||
A.isStringNode(node) ||
A.isNumberNode(node) ||
(A.isObjectNode(node) &&
node.properties.every(
(/** @type {Node} */ property) =>
A.isObjectKeyValNode(property) &&
(property.computed ||
/** @type {Node | string} */ (property.key) !== "__proto__")
)));
/**
* Whether evaluating a default can neither run code nor throw: a literal,
* `void`, `!` or a numeric `-` of one, or a function, which
* `has_side_effects` does not promise.
* @param {Node} node a default value
* @returns {boolean} true when it can do neither
*/
const isInertValue = (node) =>
A.isConstantNode(node) ||
A.isFunctionNode(node) ||
A.isArrowNode(node) ||
(A.isUnaryPrefixNode(node) &&
A.isConstantNode(node.argument) &&
(node.operator === "void" ||
node.operator === "!" ||
(node.operator === "-" &&
(A.isNumberNode(node.argument) || A.isBigIntNode(node.argument)))));
/**
* Whether a pattern's target can go: a name nothing reads, whose default
* cannot throw; a nested pattern stays, as it asserts a shape, but is pruned.
* @param {Node} target a pattern element or property value
* @param {(symbol: Node) => boolean} drop counts a name as dropped, if it can be
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it went
*/
const dropsTarget = (target, drop, compressor) => {
let name = target;
if (A.isDefaultAssignNode(name)) {
if (!isInertValue(name.right)) return false;
name = name.left;
}
if (A.isDestructuringNode(name)) {
prunePattern(name, drop, compressor, null);
return false;
}
return A.isSymbolDeclarationNode(name) && drop(name);
};
/**
* Prunes a pattern a target kept nests, its default or not.
* @param {Node} target a pattern element or property value that stays
* @param {(symbol: Node) => boolean} drop counts a name as dropped, if it can be
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const pruneNested = (target, drop, compressor) => {
const name = A.isDefaultAssignNode(target) ? target.left : target;
if (A.isDestructuringNode(name)) prunePattern(name, drop, compressor, null);
};
/**
* Drops from a pattern the names nothing reads: an array's become holes
* where shorter, an object's go where no getter or rest would notice.
* @param {Node} pattern an array or object pattern
* @param {(symbol: Node) => boolean} drop counts a name as dropped, if it can be
* @param {Compressor} compressor the compressor
* @param {Node | null} init the value it reads, where known
* @returns {void}
*/
const prunePattern = (pattern, drop, compressor, init) => {
if (pattern.is_array) {
/** @type {(Node | null)[]} */
const elements = pattern.elements;
// A last name written as a hole is no shorter, and every element
// stays a step of the iteration, which an iterator can observe.
let last = true;
for (let i = elements.length; --i >= 0;) {
const element = elements[i];
if (element === null) continue;
if (A.isExpansionNode(element)) {
last = false;
} else if (last) {
last = false;
pruneNested(element, drop, compressor);
} else if (dropsTarget(element, drop, compressor)) {
elements[i] = null;
}
}
return;
}
/** @type {Node[]} */
const properties = pattern.properties;
// A rest holds every key the pattern does not name, and reading a key
// may run a getter `pure_getters` does not rule out.
const keepsKeys =
// Loose, as terser's: an option of `1` reads as `true`.
// eslint-disable-next-line eqeqeq
(compressor.option("pure_getters") != true && !hasNoGetters(init)) ||
properties.some((property) => A.isExpansionNode(property));
pattern.properties = properties.filter((property) => {
if (!A.isObjectKeyValNode(property)) return true;
// A computed key converts its value, which may run `toString`.
if (keepsKeys || property.computed) {
pruneNested(/** @type {Node} */ (property.value), drop, compressor);
return true;
}
return !dropsTarget(
/** @type {Node} */ (property.value),
drop,
compressor
);
});
};
/**
* Whether a value is never `null` or `undefined`, so a pattern reading it
* cannot throw.
* @param {Node} node an expression
* @returns {boolean} true when it never is
*/
const isNeverNullish = (node) =>
A.isObjectNode(node) ||
A.isArrayNode(node) ||
A.isLambdaNode(node) ||
A.isStringNode(node) ||
A.isNumberNode(node);
/**
* Whether a value spreads another anywhere it runs, whose iteration or
* getters dropping it for its effects alone would skip.
* @param {Node} node an expression
* @returns {boolean} true when it does
*/
const spreads = (node) => {
let found = false;
walkNode(
node,
A.createWalker((/** @type {Node} */ inner) => {
if (found) return true;
// A function's body does not run where it is written.
if (A.isLambdaNode(inner)) return true;
if (A.isExpansionNode(inner)) {
found = true;
return true;
}
})
);
return found;
};
/** @type {UnusedHooks} */
const unusedHooks = modules.webpackUnused;
unusedHooks.prunePattern = (pattern, drop, compressor, init) => {
if (improvements.enabled) {
prunePattern(pattern, drop, compressor, init === undefined ? null : init);
}
};
unusedHooks.dropsPattern = (definition, compressor) => {
/** @type {Node} */
const pattern = definition.id;
/** @type {Node | null} */
const init = definition.init;
return (
improvements.enabled &&
init !== null &&
init !== undefined &&
// An array pattern, empty or not, looks up its value's iterator.
!pattern.is_array &&
A.namesOf(pattern).length === 0 &&
isNeverNullish(init) &&
!spreads(init)
);
};
unusedHooks.dropsCatchBinding = (compressor) =>
improvements.enabled && compressor.option("ecma") >= 2019;
unusedHooks.dropsLetWrite = () => improvements.enabled;
/** @type {InlineHooks} */
const inlineHooks = modules.webpackInline;
// terser keeps a function out of a pure call lest the callee lose the name
// `pure_funcs` matches it by; an argument made a function runs no code.
inlineHooks.inlinesIntoPureArgument = (
call,
reference,
value,
compressor
) => {
if (!improvements.enabled || !A.isLambdaNode(value)) return false;
if (call.callee !== reference) return true;
// Called, the function inlines where the call's value is read: then no
// drop of the call is lost, as `inlinesAsRead` has it for the copy.
const outer = compressor.parent(1);
if (A.isVarDefLikeNode(outer) && outer.init === call) {
// A declaration nothing reads drops with its value while it stays pure.
const definition = A.isSymbolNode(outer.id) && outer.id.definition;
return Boolean(definition && definition.references.length > 0);
}
return (
(A.isCallNode(outer) && outer.callee !== call) ||
A.isReturnNode(outer) ||
(A.isBinaryNode(outer) && outer.operator === "+")
);
};
// A call of a function `#__NO_SIDE_EFFECTS__` marks is pure, as for Rollup,
// esbuild and swc, when nothing writes the name it reads.
const isCalleePureUnmarked = A.isCalleePure;
A.isCalleePure = function isCalleePure(self, compressor) {
if (isCalleePureUnmarked.call(this, self, compressor)) return true;
if (
!improvements.enabled ||
!compressor.option("side_effects") ||
// Only `reduce_vars` counts the writes to the name.
!compressor.option("reduce_vars") ||
!A.isCallNode(self) ||
A.isNewNode(self) ||
!A.isSymbolRefNode(self.callee)
) {
return false;
}
const { definition } = self.callee;
if (!definition || definition.assignments > 0) return false;
const value = A.fixedValue(self.callee);
if (value) return A.noSideEffectsFunctions.has(value);
// An export has no fixed value, yet only its own module writes it: the
// function it is declared with, or its name declaring it, is marked.
if (
!definition.export ||
definition.identifiers.length !== 1 ||
definition.patternWrites > 0
) {
return false;
}
return A.noSideEffectsFunctions.has(
definition.init || definition.identifiers[0]
);
};
inlineHooks.flattensInLoop = (lambda) => {
if (!improvements.enabled || !A.isLambdaNode(lambda)) return false;
const body = /** @type {Node[]} */ (A.bodyOf(lambda));
return (
!lambda.async &&
!lambda.generator &&
body.length === 1 &&
(A.isSimpleStatementNode(body[0]) || A.isReturnNode(body[0]))
);
};
// `export default function(){…}` exports the same function, hoisted like
// the declaration it was; one keeping its name would need parentheses.
inlineHooks.inlinesIntoDefaultExport = (lambda, compressor) =>
improvements.enabled &&
!keepName(compressor.option("keep_fnames"), lambda.id.name) &&
lambda.id.definition.recursive_refs === 0;
// `(a ? f : g)(x)` reads `f` only to call it, as `f(x)` does. A function
// calling itself stays declared: its expression, nested deeper, mangles worse.
inlineHooks.inlinesIntoCallee = (reference, definition, compressor) => {
if (!improvements.enabled || definition.recursive_refs > 0) return false;
let node = reference;
let parent = compressor.parent();
for (
let i = 1;
parent && A.isConditionalNode(parent) && parent.test !== node;
i++
) {
node = parent;
parent = compressor.parent(i);
}
return (
node !== reference &&
A.isCallNode(parent) &&
!A.isNewNode(parent) &&
parent.callee === node
);
};
/**
* Whether a node writes a variable anywhere in it: `reduce_vars` counts
* neither a pattern's writes nor any in a function copied in since.
* @param {Node} node where to look
* @param {Definition} definition the variable
* @returns {boolean} true when one does
*/
const writesVariable = (node, definition) =>
A.walk(node, (/** @type {Node} */ child) => {
const kind = child[walkedKinds];
const target =
kind === WRITES_LEFT
? child.left
: kind === WRITES_ARGUMENT
? child.argument
: undefined;
if (
target !== undefined &&
A.walk(target, (/** @type {Node} */ symbol) => {
if (A.isPropAccessNode(symbol)) return true;
if (A.isSymbolNode(symbol) && symbol.definition === definition) {
return A.walk_abort;
}
})
) {
return A.walk_abort;
}
});
inlineHooks.writesVariable = writesVariable;
/**
* Whether a `var`'s initializer runs before the call the compressor is at,
* in a statement before it; inlining refuses a loop around the call itself.
* @param {Definition} definition the `var`
* @param {Compressor} compressor the compressor at the call
* @returns {boolean} true when it is
*/
const initializedBefore = (definition, compressor) => {
let child = compressor.self();
for (let i = 0; ; i++) {
const parent = compressor.parent(i);
const list = A.bodyOf(parent);
if (Array.isArray(list)) {
for (let j = list.indexOf(child); --j >= 0;) {
const statement = list[j];
if (
A.isVarNode(statement) &&
statement.declarations.some(
(/** @type {Node} */ declaration) =>
declaration.init && declaration.id.definition === definition
)
) {
return true;
}
}
}
if (A.isScopeNode(parent)) return false;
child = parent;
}
};
/**
* Whether a function declares a name anywhere in it, its parameters included.
* @param {Node} lambda the function
* @param {string} name the name
* @returns {boolean} true when it does
*/
const declaresName = (lambda, name) =>
A.walk(lambda, (/** @type {Node} */ node) => {
if (A.isSymbolDeclarationNode(node) && node.name === name) {
return A.walk_abort;
}
});
// `function(a){…}(b)` flattens where `a` is taken, its body reading the `b`
// around it, which nothing writes, so it holds the argument's value.
inlineHooks.aliasesParameter = (call, lambda, index, compressor) => {
if (!improvements.enabled) return false;
const parameter = lambda.params[index];
const value = call.arguments[index];
// Measured: a body's `var`s hoisted beside the call gzip worse than the
// call, but for those holding a function.
if (
/** @type {Node[]} */ (/** @type {Node} */ (lambda.body).body).some(
(/** @type {Node} */ statement) =>
A.isVarNode(statement) &&
!statement.declarations.every(
(/** @type {Node} */ declaration) =>
declaration.init && A.isLambdaNode(declaration.init)
)
) ||
!A.isSymbolFunargNode(parameter) ||
!A.isSymbolRefNode(value) ||
// Read under the argument's name, the body must declare none like it.
(value.name !== parameter.name && declaresName(lambda, value.name))
) {
return false;
}
const parameterDefinition = parameter.definition;
const variable = value.definition;
if (
!variable ||
parameterDefinition.identifiers.length !== 1 ||
// Unmangled, a longer name read often costs more than the `var` that
// would copy it.
(!compressor._mangle_options &&
(value.name.length - parameter.name.length) *
parameterDefinition.references.length >
parameter.name.length + value.name.length + 6) ||
variable.undeclared ||
variable.global ||
variable.identifiers.length !== 1 ||
variable.scope.isPinned() ||
// Only a parameter is what `arguments` reads and writes.
(variable.scope.usesArguments &&
A.isSymbolFunargNode(variable.identifiers[0])) ||
/** @type {Scope} */ (compressor.find_scope()).findVariable(
value.name
) !== variable
) {
return false;
}
const reachable = modules.common.is_reachable(lambda, [
parameterDefinition
]);
// A closure reads the `var` after the call, when it may hold another value.
if (
A.isSymbolVarNode(variable.identifiers[0]) &&
// A parameter holds its argument before its function's body runs.
!A.isSymbolFunargNode(variable.identifiers[0]) &&
reachable &&
!initializedBefore(variable, compressor)
) {
return false;
}
const written = writesVariable(variable.scope.node, variable);
return (
!writesVariable(/** @type {Node} */ (lambda.body), parameterDefinition) &&
// Written only by its own function's statements, which run around the
// body and never in it; a closure the body makes would see a later write.
(!written ||
(!reachable &&
!writesVariable(/** @type {Node} */ (lambda.body), variable) &&
!writtenInClosure(variable)))
);
};
/**
* Whether a function nested in a variable's scope writes the variable.
* @param {Definition} variable the variable
* @returns {boolean} true when one does
*/
const writtenInClosure = (variable) => {
const root = variable.scope.node;
return A.walk(root, (/** @type {Node} */ node) =>
node !== root && A.isLambdaNode(node) && writesVariable(node, variable)
? A.walk_abort
: undefined
);
};
// `function(a){…}(f(a))` flattens where `a` is taken by a variable read only
// in the argument: assigned the argument, it is read in the parameter's place.
inlineHooks.reusesVariable = (
call,
lambda,
index,
scope,
inLoop,
compressor
) => {
if (!improvements.enabled || inLoop) return undefined;
const parameter = lambda.params[index];
const value = call.arguments[index];
// As for an alias, a body's `var`s hoisted beside the call gzip worse
// than the call, but for those holding a function.
if (
!value ||
/** @type {Node[]} */ (/** @type {Node} */ (lambda.body).body).some(
(/** @type {Node} */ statement) =>
A.isVarNode(statement) &&
!statement.declarations.every(
(/** @type {Node} */ declaration) =>
declaration.init && A.isLambdaNode(declaration.init)
)
) ||
!A.isSymbolFunargNode(parameter) ||
parameter.definition.identifiers.length !== 1 ||
A.usesEval(lambda) ||
globalMayBeRebound(compressor)
) {
return undefined;
}
const variable = /** @type {Scope} */ (
compressor.find_scope()
).findVariable(parameter.name);
if (
!variable ||
variable.undeclared ||
variable.global ||
variable.scope.node !== scope ||
variable.assignments !== 0 ||
variable.identifiers.length !== 1 ||
!(
A.isSymbolFunargNode(variable.identifiers[0]) ||
A.isSymbolVarNode(variable.identifiers[0])
) ||
variable.scope.isPinned() ||
// Only a parameter is what `arguments` reads and writes.
(variable.scope.usesArguments &&
A.isSymbolFunargNode(variable.identifiers[0]))
) {
return undefined;
}
// Read in the argument and nowhere else, outside any closure, the
// variable holds nothing anything reads once the argument is evaluated.
let readsInArgument = false;
let readsElsewhere = false;
A.walk(scope, (/** @type {Node} */ node) => {
if (readsElsewhere) return true;
if (node === value) {
A.walk(value, (/** @type {Node} */ child) => {
if (A.isLambdaNode(child)) {
if (
A.walk(child, (/** @type {Node} */ symbol) =>
A.isSymbolRefNode(symbol) && symbol.definition === variable
? A.walk_abort
: undefined
)
) {
readsElsewhere = true;
}
return true;
}
if (A.isSymbolRefNode(child) && child.definition === variable) {
readsInArgument = true;
}
});
return true;
}
if (A.isSymbolRefNode(node) && node.definition === variable) {
readsElsewhere = true;
}
});
if (!readsInArgument || readsElsewhere) return undefined;
return variable;
};
/**
* @param {Node} node an expression
* @returns {boolean} whether it is a number, string, boolean, `null` or `void 0`
*/
const isPrimitiveConstant = (node) => {
if (A.isUnaryPrefixNode(node)) {
return (
(node.operator === "!" ||
node.operator === "void" ||
node.operator === "-") &&
A.isNumberNode(node.argument)
);
}
return (
A.isNumberNode(node) ||
A.isStringNode(node) ||
A.isNullNode(node) ||
A.isTrueNode(node) ||
A.isFalseNode(node) ||
A.isUndefinedNode(node)
);
};
// `function(x){…}(3)` flattens with `3` read where `x` was, which nothing
// writes, and needs no variable around the call.
inlineHooks.readsConstant = (call, lambda, index) => {
if (!improvements.enabled) return false;
const parameter = lambda.params[index];
const value = call.arguments[index];
if (
!value ||
!A.isSymbolFunargNode(parameter) ||
!isPrimitiveConstant(value)
) {
return false;
}
const definition = parameter.definition;
return (
definition.identifiers.length === 1 &&
!writesVariable(/** @type {Node} */ (lambda.body), definition)
);
};
/**
* @param {Node} value a property's value
* @returns {boolean} whether calling it may read `this`
*/
const readsThis = (value) => {
// As swc assumes, a function another module exports reads no `this`.
if (
A.isSymbolRefNode(value) &&
value.definition &&
A.kindOf(value.definition.identifiers[0]) === "SymbolImport"
) {
return false;
}
const callee = A.isSymbolRefNode(value) ? A.fixedValue(value) : value;
if (!callee) return true;
if (A.isArrowNode(callee) || A.isConstantNode(callee)) return false;
return (
!(A.isFunctionNode(callee) || A.isDefunNode(callee)) ||
A.containsThis(callee)
);
};
// terser holds an object escaping once a read passes a property's value on,
// which a variable per property passes on the same, yet splits one whose
// tag reads it as `this`; the object itself read whole keeps it whole too.
/** @type {HoistHooks} */
const hoistHooks = modules.webpackHoist;
hoistHooks.splits = (scope, definition, value) => {
if (!improvements.enabled) return definition.escaped !== 1;
if (definition.scope.isPinned()) return false;
/** @type {Map<string, Node>} */
const values = new Map();
for (const property of value.properties) {
if (!A.isObjectKeyValNode(property)) return false;
const key = String(A.terserKey(property.key));
// `__proto__` sets the prototype, so a read of it is no property's value.
if (key !== "__proto__") {
values.set(key, /** @type {Node} */ (property.value));
}
}
const { references } = definition;
let reads = 0;
let split = true;
const walker = A.createWalker((/** @type {Node} */ node) => {
if (!split) return true;
if (!A.isSymbolRefNode(node) || node.definition !== definition) return;
const access = walker.parent();
if (
!(A.isDotNode(access) || A.isSubNode(access)) ||
access.object !== node ||
access.optional
) {
split = false;
return;
}
const key = modules.common.get_simple_key(A.terserProperty(access));
const propertyValue =
typeof key === "object" ? undefined : values.get(String(key));
const parent = walker.parent(1);
if (
propertyValue === undefined ||
(A.isUnaryPrefixNode(parent) && parent.operator === "delete") ||
// A call or a tag reads the object as `this`.
(((A.isCallNode(parent) &&
!A.isNewNode(parent) &&
parent.callee === access) ||
(A.isPrefixedTemplateStringNode(parent) && parent.tag === access)) &&
readsThis(propertyValue))
) {
split = false;
return;
}
reads++;
});
walkNode(scope, walker);
return split && reads === references.length;
};
/** @type {CollapseHooks} */
const collapseHooks = modules.webpackCollapse;
// A store runs a setter `Array.prototype` may hold on that index, which the
// literal's own element never reaches.
collapseHooks.joinsArrayStores = (compressor) =>
improvements.enabled && Boolean(compressor.option("unsafe"));
// Every name read inside a `try` block as the tree came in, so a read asked
// about away from where the compressor stands, as `drop_unused` asks about a
// declarator's value, is still found probing.
/** @type {WeakSet<Node>} */
let probedReads = new WeakSet();
// The built-ins whose `prototype` the program writes, deletes from or
// hands a call, which a missing key's read may then find.
/** @type {Set<string>} */
const patchedPrototypes = new Set();
const BUILT_IN_PROTOTYPES_NAMES = new Set(["Array", "Object", "String"]);
// The built-ins whose own members the program writes, `Math.max = f` say.
/** @type {Set<string>} */
const patchedStatics = new Set();
// The globals the program assigns through the global object, as
// `globalThis.Number = n`, which no count on `Number` itself records.
/** @type {Set<string>} */
const reboundGlobals = new Set();
const GLOBAL_OBJECT_NAMES = new Set([
"globalThis",
"window",
"self",
"global"
]);
// Stands for every global where a write's key through the global object
// is not known.
const ANY_GLOBAL = "*";
// The built-ins that write what an argument holds, where handing the global
// object to any other call, as `isGlobal(globalThis)`, is taken to read it.
const ARGUMENT_WRITERS = new Set([
"assign",
"defineProperties",
"defineProperty",
"deleteProperty",
"set",
"setPrototypeOf"
]);
/**
* Whether a call is `Object` or `Reflect` writing what an argument holds,
* as `Object.defineProperty(globalThis, "Number", …)` does.
* @param {Node} call a call
* @returns {boolean} true where it is
*/
const definesOnArgument = (call) =>
A.isDotNode(call.callee) &&
A.isSymbolRefNode(call.callee.object) &&
(call.callee.object.name === "Object" ||
call.callee.object.name === "Reflect") &&
ARGUMENT_WRITERS.has(call.callee.property.name);
/**
* Whether a reference reads the global object, not a local named `self`.
* @param {Node} node a node
* @returns {boolean} true where it does
*/
const isGlobalObjectRef = (node) =>
A.isSymbolRefNode(node) &&
GLOBAL_OBJECT_NAMES.has(node.name) &&
isUndeclaredRef(node);
/**
* The global a read names, `Array` or `globalThis.Array`, with
* `ANY_GLOBAL` for a key through the global object that is not known.
* @param {Node} node a read
* @returns {string | undefined} the global's name, if it names one
*/
const globalNameOf = (node) => {
if (A.isSymbolRefNode(node)) return node.name;
if (!A.isPropAccessNode(node) || !isGlobalObjectRef(node.object)) {
return undefined;
}
const name = A.isSubNode(node)
? constantOf(node.property)
: A.terserProperty(node);
return typeof name === "string" ? name : ANY_GLOBAL;
};
// `...[1]` and `"" + [1]` run what the program puts on `Array.prototype`.
modules.webpackInline.arrayPrototypePatched = () =>
improvements.enabled && patchedPrototypes.has("Array");
/**
* The built-in, `Array`, `Object` or `String`, whose `prototype` is written
* through where it stands, or handed to a call that may write it.
* @param {Node} node a property read
* @param {Walker} walker the walk holding its parents
* @returns {string | undefined} its name, or `ANY_GLOBAL` where not known
*/
const writtenPrototypeOf = (node, walker) => {
if (
(A.isSubNode(node)
? constantOf(node.property)
: A.terserProperty(node)) !== "prototype"
) {
return undefined;
}
const name = globalNameOf(node.object);
return name !== undefined &&
(name === ANY_GLOBAL || BUILT_IN_PROTOTYPES_NAMES.has(name)) &&
writesThrough(node, walker)
? name
: undefined;
};
/**
* Whether a property of the object is written or deleted where it stands,
* or the object handed to a call that may write one.
* @param {Node} node an object read
* @param {Walker} walker the walk holding its parents
* @returns {boolean} true where it is
*/
const writesThrough = (node, walker) => {
const parent = walker.parent(0);
if (A.isCallNode(parent)) return parent.callee !== node;
// `f(globalThis?.Array.prototype)` hands the call the chain around it.
if (A.isChainNode(parent)) {
const call = walker.parent(1);
return A.isCallNode(call) && call.callee !== parent;
}
return (
A.isPropAccessNode(parent) &&
parent.object === node &&
writesObjectMember(parent, walker)
);
};
const compressBeforeProbes = compressorMethods.compress;
/**
* @this {Compressor} the compressor
* @param {Node} toplevel the tree
* @returns {Node} the tree compressed
*/
compressorMethods.compress = function compress(toplevel) {
probedReads = new WeakSet();
patchedPrototypes.clear();
patchedStatics.clear();
reboundGlobals.clear();
return compressBeforeProbes.call(this, toplevel);
};
// Runs once scope is known, so a local named `self` is told from the global.
collapseHooks.probeProgram = (toplevel) => {
if (improvements.enabled) {
let depth = 0;
const walker = A.createWalker(
(/** @type {Node} */ node, /** @type {() => void} */ descend) => {
const prototypeOf = A.isPropAccessNode(node)
? writtenPrototypeOf(node, walker)
: undefined;
if (prototypeOf !== undefined) {
for (const name of prototypeOf === ANY_GLOBAL
? BUILT_IN_PROTOTYPES_NAMES
: [prototypeOf]) {
patchedPrototypes.add(name);
}
} else {
const name = globalNameOf(node);
if (
name !== undefined &&
(name === ANY_GLOBAL || pureGlobals.has(name)) &&
writesThrough(node, walker)
) {
for (const global of name === ANY_GLOBAL ? pureGlobals : [name]) {
patchedStatics.add(global);
}
}
}
if (isGlobalObjectRef(node) && writesThrough(node, walker)) {
const access = walker.parent(0);
if (A.isPropAccessNode(access)) {
reboundGlobals.add(/** @type {string} */ (globalNameOf(access)));
} else if (
definesOnArgument(
A.isChainNode(access) ? walker.parent(1) : access
)
) {
reboundGlobals.add(ANY_GLOBAL);
}
}
if (depth > 0 && A.isSymbolRefNode(node)) {
probedReads.add(node);
} else if (A.isTryNode(node)) {
depth++;
A.walkNode(node.block, walker);
depth--;
if (node.handler) A.walkNode(node.handler, walker);
if (node.finalizer) A.walkNode(node.finalizer, walker);
return true;
}
return undefined;
}
);
A.walkNode(toplevel, walker);
}
};
/**
* Whether a read sits in a `try` block, where a global's read may be there to
* throw: `try { x } catch {}` probes for `x`.
* @param {Node} node the read
* @param {Compressor} compressor the compressor
* @returns {boolean} true there
*/
const probesInTry = (node, compressor) => {
if (probedReads.has(node)) return true;
let child = compressor.self();
for (let i = 0, parent; (parent = compressor.parent(i)); i++) {
if (A.isTryNode(parent) && parent.block === child) return true;
if (A.isLambdaNode(parent)) return false;
child = parent;
}
return false;
};
const hostGlobals = new Set(pureTables.hostGlobals);
/**
* Whether a name is a global the language or a browser page defines, which
* a program reading it expects there, as esbuild and oxc list them.
* @param {string} name a global's name
* @param {Compressor} compressor the compressor
* @returns {boolean} true for such a global
*/
const isKnownGlobal = (name, compressor) =>
pureGlobals.has(name) ||
hostGlobals.has(name) ||
compressor.pure_access_globals(name);
// As swc and oxc, a known global's read is taken to run nothing and never
// throw, where no `with` object answers it first and no `try` probes for it.
for (const wrapAnswer of [A.wrapHasSideEffects, A.wrapMayThrow]) {
wrapAnswer(
"Identifier",
(
/** @type {(self: Node, compressor: Compressor) => boolean} */ original
) =>
(/** @type {Node} */ self, /** @type {Compressor} */ compressor) =>
improvements.enabled &&
A.isSymbolRefNode(self) &&
isUndeclaredRef(self) &&
isKnownGlobal(self.name, compressor) &&
!(self.scope && self.scope.usesWith) &&
!probesInTry(self, compressor)
? false
: original(self, compressor)
);
}
/**
* Whether a node reads a known global no `with` object answers first.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {boolean} true for such a read
*/
const readsPlainGlobal = (node, compressor) =>
A.isSymbolRefNode(node) &&
isUndeclaredRef(node) &&
isKnownGlobal(node.name, compressor) &&
!(node.scope && node.scope.usesWith);
for (const type of ["Identifier", "MemberExpression"]) {
A.wrapDropSideEffectFree(
type,
(
/** @type {(self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} */ original
) =>
(
/** @type {Node} */ self,
/** @type {Compressor} */ compressor,
/** @type {boolean=} */ firstInStatement
) =>
improvements.enabled &&
(readsPlainGlobal(self, compressor) ||
(A.isDotNode(self) &&
!self.optional &&
readsPlainGlobal(self.object, compressor))) &&
!probesInTry(A.isDotNode(self) ? self.object : self, compressor)
? null
: original(self, compressor, firstInStatement)
);
}
// As swc and oxc, a value moved past a global's read, or its member's, is taken
// not to reassign or delete that global, nor reach a getter on it.
collapseHooks.readsBuiltIn = (node, compressor) => {
if (!improvements.enabled) return false;
const global = A.isDotNode(node) && !node.optional ? node.object : node;
if (
!A.isSymbolRefNode(global) ||
// The writes are counted only while `reduce_vars` reads the tree.
!compressor.option("reduce_vars") ||
isReboundGlobal(global.name) ||
globalMayBeRebound(compressor) ||
probesInTry(global, compressor)
) {
return false;
}
// Only one nothing in the program writes, past a declaration; a member
// only of a built-in, as the program may write its own objects' members.
const { definition } = global;
const undeclared =
isUndeclaredRef(global) && isKnownGlobal(global.name, compressor);
return Boolean(
definition &&
definition.global &&
definition.assignments === 0 &&
definition.patternWrites === 0 &&
(undeclared || (global === node && definition.identifiers.length === 1))
);
};
/**
* Whether `this` where the compressor is reads a derived constructor's,
* past arrows only, which throws until `super()` has run.
* @param {Compressor} compressor the compressor
* @returns {boolean} true there
*/
const thisMayBeUninitialized = (compressor) => {
let node = compressor.self();
for (let i = 0; node; node = compressor.parent(i++)) {
if (!A.isLambdaNode(node) || A.isArrowNode(node)) continue;
// A constructor's parent is its class.
return (
compressor.parent(i).kind === "constructor" &&
Boolean(compressor.parent(i + 1).superClass)
);
}
return false;
};
collapseHooks.storesWithoutReading = (
target,
parent,
defunScope,
compressor
) => {
if (
!improvements.enabled ||
!A.isAssignNode(parent) ||
parent.left !== target ||
parent.operator !== "="
) {
return false;
}
// A computed key may convert to a string, which then runs first.
if (
A.isSubNode(target) &&
!A.isStringNode(target.property) &&
!A.isNumberNode(target.property)
) {
return false;
}
const { object } = target;
if (A.isThisNode(object)) return !thisMayBeUninitialized(compressor);
if (!A.isSymbolRefNode(object)) return false;
// Read first, a `let` in its dead zone would throw before the value runs.
const { definition } = object;
return (
definition.scope.getDefunScope() === defunScope &&
!definition.scope.usesArguments &&
!compressor.exposed(definition) &&
definition.identifiers.every(
(/** @type {Node} */ identifier) =>
A.isSymbolVarNode(identifier) || A.isSymbolDefunNode(identifier)
)
);
};
// A `let` of the function folds as a `var` does, its dead zone read as a
// throw; terser keeps every `let` where it is.
collapseHooks.assignsLet = (target, defunScope, compressor) =>
improvements.enabled &&
target.definition.scope.getDefunScope() === defunScope &&
// Measured: a `let` the program shares gzips worse folded, and wins nothing.
!compressor.exposed(target.definition);
// A global's getter cannot see a name only its function reads, and what
// it throws leaves the function, which then never reads the name.
collapseHooks.foldsUnobserved = () => improvements.enabled;
// A method the moved value calls may change the object a name holds, not
// the binding: an argument or element only collects the object, which an
// operator or template would read before the moved value runs.
collapseHooks.readsUnchangedBinding = (node, parent, candidate) =>
improvements.enabled &&
node.definition !== undefined &&
!node.definition.undeclared &&
((A.isCallNode(parent) && parent.arguments.includes(node)) ||
(A.isArrayNode(parent) && parent.elements.includes(node))) &&
!writesVariable(candidate, node.definition);
// A `var` nothing writes again keeps its value while a moved value runs:
// its declaration runs only in its own frame, never inside that value.
collapseHooks.readsSettled = (node, candidate, compressor) => {
if (!improvements.enabled || !compressor.option("reduce_vars")) {
return false;
}
const { definition } = node;
if (
!definition ||
definition.undeclared ||
definition.global ||
definition.assignments !== 0 ||
// A value called is no longer `fixed`, though nothing writes it again.
definition.patternWrites !== 0 ||
definition.identifiers.length !== 1 ||
!A.isSymbolVarNode(definition.identifiers[0]) ||
// `arguments` passed on writes a parameter wherever it goes.
(A.isSymbolFunargNode(definition.identifiers[0]) &&
definition.scope.usesArguments) ||
definition.scope.isPinned() ||
compressor.exposed(definition)
) {
return false;
}
// A generator's `next` runs its body on, so one declaring the name may
// run the declaration while the moved value does.
const declaringScope = definition.scope.getDefunScope();
if (declaringScope.node.generator) return false;
// Suspended at `await` or `yield`, the moved value lets any code run.
return !A.walk(candidate, (/** @type {Node} */ child) =>
A.isAwaitNode(child) || A.isYieldNode(child) ? A.walk_abort : undefined
);
};
const constructedWhenCalled = new Set(foldedTables.constructedWhenCalled);
/**
* Whether a `new` of a built-in the program never assigns constructs as its
* call does: always but for `RegExp`, which hands back a regular expression
* passed without flags, so it needs a constant or string pattern, or flags
* that are a string.
* @param {Node} node a `new` expression
* @param {Compressor} compressor the compressor
* @returns {boolean} true when the call constructs the same
*/
const constructsWhenCalled = (node, compressor) => {
const { callee } = node;
if (
!A.isSymbolRefNode(callee) ||
!constructedWhenCalled.has(callee.name) ||
!readsUnassignedGlobal(callee, compressor)
) {
return false;
}
const args = node.arguments;
if (callee.name !== "RegExp" || args.length === 0) return true;
const [pattern, flags] = args;
return (
!args.some((/** @type {Node} */ arg) => A.isExpansionNode(arg)) &&
(constantOf(pattern) !== NOT_CONSTANT ||
A.isString(pattern, compressor) ||
(flags !== undefined && A.isString(flags, compressor)))
);
};
/**
* How many leading arguments of a `new` the constructor it runs can read.
* @param {Node} node a `new`
* @returns {number} the count, or -1 where the constructor is unknown or reads `arguments`
*/
const constructorArgumentsRead = (node) => {
const { callee } = node;
const target = A.isSymbolRefNode(callee) ? A.fixedValue(callee) : callee;
if (!target) return -1;
/** @type {Node | undefined} */
let lambda = target;
if (A.isClassNode(target)) {
// An implicit derived constructor passes every argument to `super`.
if (target.superClass) return -1;
lambda = undefined;
for (const property of /** @type {Node[]} */ (
/** @type {Node} */ (target.body).body
)) {
if (
A.isConciseMethodNode(property) &&
!property.static &&
!property.computed &&
property.key.name === "constructor"
) {
lambda = /** @type {Node} */ (property.value);
}
}
if (lambda === undefined) return 0;
} else if (
!A.isLambdaNode(target) ||
A.isArrowNode(target) ||
target.generator ||
target.async
) {
return -1;
}
if (
A.usesArguments(lambda) ||
A.usesEval(lambda) ||
lambda.params.some((/** @type {Node} */ parameter) =>
A.isExpansionNode(parameter)
)
) {
return -1;
}
return lambda.params.length;
};
A.wrapOptimizer("New", (optimizeNew) => (self, compressor) => {
const optimized = optimizeNew(self, compressor);
if (!improvements.enabled || !A.isNewNode(optimized)) return optimized;
// A spread array literal passes its elements, as terser has it for a call.
const args = optimized.arguments;
for (let i = 0; i < args.length && !patchedPrototypes.has("Array"); i++) {
const arg = args[i];
if (
A.isExpansionNode(arg) &&
A.isArrayNode(arg.argument) &&
!arg.argument.elements.includes(null)
) {
args.splice(i, 1, .../** @type {Node[]} */ (arg.argument.elements));
i--;
}
}
const read = constructorArgumentsRead(optimized);
if (
read >= 0 &&
args.length > read &&
!args.some((/** @type {Node} */ arg) => A.isExpansionNode(arg))
) {
// An argument past them only runs, so one without effects goes; a
// read that may throw, as in a dead zone, stays.
let kept = read;
for (let i = read; i < args.length; i++) {
const effects = A.dropSideEffectFree(args[i], compressor);
if (effects) args[kept++] = effects;
}
args.length = kept;
}
if (!constructsWhenCalled(optimized, compressor)) return optimized;
return A.optimize(
makeNode(A.CallNode, optimized, {
callee: optimized.callee,
arguments: optimized.arguments,
optional: false,
_annotations: optimized._annotations
}),
compressor
);
});
/**
* Whether an expression is a value no `Array` call reads as a length.
* @param {Node} node an argument
* @returns {boolean} true when it is a literal of another type than number
*/
const isNeverNumber = (node) =>
A.isStringNode(node) ||
A.isBigIntNode(node) ||
A.isTemplateStringNode(node) ||
A.isArrayNode(node) ||
A.isObjectNode(node) ||
A.isLambdaNode(node) ||
A.isClassNode(node) ||
A.isRegExpNode(node) ||
(A.isUnaryPrefixNode(node) &&
(node.operator === "!" || node.operator === "typeof"));
/**
* The literal an `Array` or `Object` call constructs, or the call.
* @param {Node} call a call of the undeclared global it names
* @returns {Node} what replaces it
*/
const literalOfConstructorCall = (call) => {
const args = call.arguments;
if (call.callee.name === "Object") {
return args.length === 0
? makeNode(A.ObjectNode, call, { properties: [] })
: call;
}
// Two arguments besides the spreads pass `Array` two or more, never a length.
if (args.some((/** @type {Node} */ arg) => A.isExpansionNode(arg))) {
return args.filter((/** @type {Node} */ arg) => !A.isExpansionNode(arg))
.length >= 2
? makeNode(A.ArrayNode, call, { elements: args })
: call;
}
if (args.length !== 1 || isNeverNumber(args[0])) {
return makeNode(A.ArrayNode, call, { elements: args });
}
const [length] = args;
// `[,,]` is shorter than `Array(2)` up to six holes.
if (
A.isNumberNode(length) &&
Number.isInteger(length.value) &&
/** @type {number} */ (length.value) <= 6
) {
return makeNode(A.ArrayNode, call, {
elements: Array.from(
{ length: /** @type {number} */ (length.value) },
() => null
)
});
}
return call;
};
A.wrapOptimizer("Call", (optimizeCall) => (self, compressor) => {
const optimized = optimizeCall(self, compressor);
if (
!improvements.enabled ||
!A.isCallNode(optimized) ||
A.isNewNode(optimized) ||
optimized.optional ||
!A.isSymbolRefNode(optimized.callee)
) {
return optimized;
}
const { name } = optimized.callee;
return (name === "Array" || name === "Object") &&
readsUnassignedGlobal(optimized.callee, compressor)
? literalOfConstructorCall(optimized)
: optimized;
});
/**
* @param {Node} node an expression
* @param {Definition} definition a variable's definition
* @returns {boolean} whether it reads that variable
*/
const readsVariable = (node, definition) =>
A.isSymbolRefNode(node) && node.definition === definition;
/**
* @param {Node} node an expression
* @param {number} value a number
* @returns {boolean} whether it is that number's literal
*/
const isNumberOf = (node, value) =>
A.isNumberNode(node) && node.value === value;
/**
* Whether an expression is `n` read as an index from `start`: `n` itself,
* or `n - start` past 0.
* @param {Node} node an expression
* @param {Definition} definition the definition of `n`
* @param {number} start where the index starts
* @returns {boolean} true when it is
*/
const readsOffset = (node, definition, start) =>
start === 0
? readsVariable(node, definition)
: A.isBinaryNode(node) &&
node.operator === "-" &&
readsVariable(node.left, definition) &&
isNumberOf(node.right, start);
/**
* Babel's rest parameter, `for (var n = arguments.length, a = Array(n > 1 ?
* n - 1 : 0), k = 1; k < n; k++) a[k - 1] = arguments[k]`, as its array;
* a `k` above 0 calls `Array.prototype.slice`, so it needs `unsafe`.
* @param {Node} loop a `for` statement terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the `var` declaring `a = [...arguments].slice(1)`, or undefined
*/
const restParameterOf = (loop, compressor) => {
const { init, test, update } = loop;
if (
!init ||
!test ||
!update ||
!A.isVarNode(init) ||
init.declarations.length < 3 ||
compressor.option("ecma") < 2015
) {
return undefined;
}
const declarations = init.declarations;
const [lengthDeclaration, copyDeclaration, indexDeclaration] =
declarations.slice(-3);
const length = lengthDeclaration.init;
const copy = copyDeclaration.init;
const index = indexDeclaration.init;
if (
!A.isSymbolVarNode(lengthDeclaration.id) ||
!A.isSymbolVarNode(copyDeclaration.id) ||
!A.isSymbolVarNode(indexDeclaration.id) ||
!A.isDotNode(length) ||
length.optional ||
length.property.name !== "length" ||
!A.isArgumentsObject(length.object) ||
!A.isNumberNode(index) ||
!Number.isInteger(index.value) ||
!A.isCallNode(copy) ||
copy.optional ||
copy.arguments.length !== 1 ||
!A.isSymbolRefNode(copy.callee) ||
copy.callee.name !== "Array" ||
!readsUnassignedGlobal(copy.callee, compressor)
) {
return undefined;
}
const lengthDefinition = lengthDeclaration.id.definition;
const indexDefinition = indexDeclaration.id.definition;
const argumentsDefinition = length.object.definition;
const start = /** @type {number} */ (index.value);
if (start > 0 && !compressor.option("unsafe")) return undefined;
const [size] = copy.arguments;
// `Array(n > 1 ? n - 1 : 0)` sizes the copy of what follows 1 parameter.
const sized =
start === 0
? readsVariable(size, lengthDefinition)
: A.isConditionalNode(size) &&
A.isBinaryNode(size.test) &&
size.test.operator === ">" &&
readsVariable(size.test.left, lengthDefinition) &&
isNumberOf(size.test.right, start) &&
readsOffset(
/** @type {Node} */ (size.consequent),
lengthDefinition,
start
) &&
isNumberOf(size.alternate, 0);
const counts =
A.isBinaryNode(test) &&
test.operator === "<" &&
readsVariable(test.left, indexDefinition) &&
readsVariable(test.right, lengthDefinition) &&
(A.isUnaryPostfixNode(update) || A.isUnaryPrefixNode(update)) &&
update.operator === "++" &&
readsVariable(update.argument, indexDefinition);
// terser has unwrapped a block of one statement.
const { body } = loop;
const store =
A.isSimpleStatementNode(body) && /** @type {Node} */ (body).expression;
if (
!sized ||
!counts ||
!A.isAssignNode(store) ||
/** @type {Node} */ (store).operator !== "=" ||
!A.isSubNode(/** @type {Node} */ (store).left) ||
!A.isSymbolRefNode(/** @type {Node} */ (store).left.object) ||
/** @type {Node} */ (store).left.object.definition !==
copyDeclaration.id.definition ||
!readsOffset(
/** @type {Node} */ (store).left.property,
indexDefinition,
start
) ||
!A.isSubNode(/** @type {Node} */ (store).right) ||
/** @type {Node} */ (store).right.optional ||
!readsVariable(
/** @type {Node} */ (store).right.object,
argumentsDefinition
) ||
!readsVariable(
/** @type {Node} */ (store).right.property,
indexDefinition
) ||
copyDeclaration.id.definition.identifiers.some(A.isSymbolFunargNode) ||
lengthDefinition.identifiers.length !== 1 ||
indexDefinition.identifiers.length !== 1 ||
A.isLabeledStatementNode(compressor.parent()) ||
globalMayBeRebound(compressor)
) {
return undefined;
}
// Neither counter nor `arguments` is read outside the loop: code before
// it could have changed the `length` of `arguments`.
let readElsewhere = false;
walkNode(
argumentsDefinition.scope.node,
A.createWalker((/** @type {Node} */ node) => {
if (readElsewhere || node === loop) return true;
if (
A.isSymbolRefNode(node) &&
(node.definition === lengthDefinition ||
node.definition === indexDefinition ||
node.definition === argumentsDefinition)
) {
readElsewhere = true;
}
})
);
if (readElsewhere) return undefined;
/** @type {Node} */
let rest = makeNode(A.ArrayNode, copy, {
elements: [
makeNode(A.SpreadElementNode, copy, {
argument: /** @type {Node} */ (store).right.object
})
]
});
if (start > 0) {
rest = makeNode(A.CallNode, copy, {
callee: makeNode(A.DotNode, copy, {
object: rest,
optional: false,
property: A.SymbolPropertyNode({ name: "slice" }),
computed: false
}),
arguments: [index],
optional: false
});
}
copyDeclaration.init = rest;
return makeNode(A.VarNode, init, {
declarations: [...declarations.slice(0, -3), copyDeclaration]
});
};
A.wrapOptimizer("For", (optimizeFor) => (self, compressor) => {
const optimized = optimizeFor(self, compressor);
return (
(improvements.enabled &&
A.isForNode(optimized) &&
restParameterOf(optimized, compressor)) ||
optimized
);
});
/**
* Whether a `#__PURE__` call of a function written in place, passed nothing,
* stays as droppable once its returned value stands in for it.
* @param {Node} call a `#__PURE__` call
* @param {Compressor} compressor the compressor
* @returns {boolean} true when the value has no side effects, or is a call whose parts have none
*/
const inlinesAsDroppable = (call, compressor) => {
const fn = call.callee;
if (
call.optional ||
call.arguments.length > 0 ||
!(A.isArrowNode(fn) || A.isFunctionNode(fn)) ||
fn.async ||
fn.generator ||
/** @type {Node[]} */ (/** @type {Node} */ (fn.body).body).length !== 1 ||
!A.isReturnNode(
/** @type {Node[]} */ (/** @type {Node} */ (fn.body).body)[0]
)
) {
return false;
}
const value = /** @type {Node[]} */ (/** @type {Node} */ (fn.body).body)[0]
.argument;
if (!value || !A.hasSideEffects(value, compressor)) return true;
return (
A.isCallNode(value) &&
!value.optional &&
(A.isSymbolRefNode(value.callee) ||
!A.hasSideEffects(value.callee, compressor)) &&
value.arguments.every(
(/** @type {Node} */ arg) => !A.hasSideEffects(arg, compressor)
)
);
};
/**
* Whether a `#__PURE__` call of a function written in place has its value
* read, so no pass drops it and its annotation buys nothing.
* @param {Node} call a `#__PURE__` call
* @param {Compressor} compressor the compressor
* @returns {boolean} true when the call returns one value something reads
*/
const inlinesAsRead = (call, compressor) => {
const fn = call.callee;
if (
call.optional ||
!(A.isArrowNode(fn) || A.isFunctionNode(fn)) ||
fn.async ||
fn.generator ||
/** @type {Node[]} */ (/** @type {Node} */ (fn.body).body).length !== 1 ||
!A.isReturnNode(
/** @type {Node[]} */ (/** @type {Node} */ (fn.body).body)[0]
) ||
!(
/** @type {Node[]} */ (/** @type {Node} */ (fn.body).body)[0].argument
) ||
// Any other argument takes a `var` to hold, which reads longer.
!call.arguments.every(
(/** @type {Node} */ arg) =>
A.isConstantNode(arg) ||
(A.isSymbolRefNode(arg) && !A.hasSideEffects(arg, compressor))
) ||
!passesValueOn(call, compressor)
) {
return false;
}
const parent = compressor.parent();
if (!A.isVarDefLikeNode(parent) || call !== parent.init) return true;
// A declaration nothing reads drops with its value while it stays pure.
const definition = A.isSymbolNode(parent.id) && parent.id.definition;
return Boolean(
definition &&
(definition.references.length > 0 || compressor.exposed(definition))
);
};
// terser keeps a `#__PURE__` call of a function whole; when dropping the value
// it returns drops as much, or the value is read, the call inlines as an
// unannotated one would.
A.wrapOptimizer("Call", (optimizeCall) => (self, compressor) => {
if (
!improvements.enabled ||
A.isNewNode(self) ||
!(self._annotations & A._PURE)
) {
return optimizeCall(self, compressor);
}
const droppable = inlinesAsDroppable(self, compressor);
if (!droppable && !inlinesAsRead(self, compressor)) {
return optimizeCall(self, compressor);
}
self._annotations &= ~A._PURE;
const optimized = optimizeCall(self, compressor);
self._annotations |= A._PURE;
// The value replaces the call, so the annotation holds for it.
if (droppable && optimized !== self && A.isCallNode(optimized)) {
optimized._annotations |= A._PURE;
}
return optimized;
});
/**
* A copy of an expression read again, each name in it counted as read.
* @param {Node} node an expression
* @returns {Node} the copy
*/
const readAgain = (node) => {
const copy = A.cloneNode(node, true);
A.walk(copy, (/** @type {Node} */ inner) => {
if (!A.isSymbolRefNode(inner) || !inner.definition) return;
// Counted, the read keeps `collapse_vars` from moving the value into
// the first copy only, and the name is no longer read once.
inner.definition.references.push(inner);
inner.definition.single_use = false;
});
return copy;
};
/**
* What one branch of a `switch` does, where it does one thing: return a
* value, or run one expression, assigning to a name or not, and leave.
* @param {Node[]} consequent the branch's statements
* @param {boolean} last whether no branch follows it
* @returns {{ value: Node, returns: boolean, target?: Node, expression?: Node } | undefined} the value, whether it is returned, the name assigned to and the assignment
*/
const branchOutcome = (consequent, last) => {
const [first] = consequent;
if (consequent.length === 1 && A.isReturnNode(first)) {
return { value: first.argument || makeVoid0(first), returns: true };
}
const tail = consequent[consequent.length - 1];
const leaves = A.isBreakNode(tail) && !tail.label;
const statements = leaves ? consequent.slice(0, -1) : consequent;
if (
(!leaves && !last) ||
statements.length === 0 ||
!statements.every((/** @type {Node} */ statement) =>
A.isSimpleStatementNode(statement)
)
) {
return;
}
if (statements.length > 1) {
const expressions = statements.map(
(/** @type {Node} */ statement) => statement.expression
);
return { value: makeSequence(first, expressions), returns: false };
}
const { expression } = first;
if (
A.isAssignNode(expression) &&
expression.operator === "=" &&
A.isSymbolRefNode(expression.left)
) {
return {
value: expression.right,
returns: false,
target: expression.left,
expression
};
}
return { value: expression, returns: false };
};
/**
* A `switch` on a local whose every branch returns, or assigns to one name,
* as one conditional: a `return` after it stands in for a missing default.
* @param {Node} self a `switch`
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the statement, or undefined where it cannot be one
*/
const switchAsConditional = (self, compressor) => {
const { discriminant, cases } = self;
if (
!A.isSymbolRefNode(discriminant) ||
!discriminant.definition ||
discriminant.definition.undeclared ||
cases.length < 2
) {
return;
}
/** @type {{ tests: Node[], value: Node, expression: Node }[]} */
const groups = [];
/** @type {Node | undefined} */
let fallback;
/** @type {Node | undefined} */
let target;
/** @type {Node | undefined} */
let fallbackExpression;
let returns = false;
/** @type {Node[]} */
let tests = [];
let inDefault = false;
for (let i = 0; i < cases.length; i++) {
const branch = cases[i];
if (A.isDefaultNode(branch)) {
inDefault = true;
} else if (A.isConstantNode(branch.test)) {
tests.push(branch.test);
} else {
return;
}
if (/** @type {Node[]} */ (branch.consequent).length === 0) {
if (i === cases.length - 1) return;
continue;
}
const outcome = branchOutcome(
/** @type {Node[]} */ (branch.consequent),
i === cases.length - 1
);
if (!outcome) return;
if (groups.length === 0 && !fallback) {
returns = outcome.returns;
target = outcome.target;
} else if (outcome.returns !== returns) {
return;
} else if (
target &&
(!outcome.target || outcome.target.definition !== target.definition)
) {
// Branches assigning apart run each assignment as an effect.
target = undefined;
for (const group of groups) group.value = group.expression;
if (fallback) fallback = fallbackExpression;
}
// The cases falling into the default are its own: no test picks them.
const value = target
? outcome.value
: outcome.expression || outcome.value;
if (inDefault) {
fallback = value;
fallbackExpression = outcome.expression || outcome.value;
} else {
groups.push({
tests,
value,
expression: outcome.expression || outcome.value
});
}
tests = [];
inDefault = false;
}
if (!fallback && !returns) {
// Every case missing runs nothing: each assignment then runs on its own.
if (target) {
target = undefined;
for (const group of groups) group.value = group.expression;
}
fallback = makeVoid0(self);
} else if (!fallback) {
// The `return` after the `switch`, or the end of the function, is where
// every case missed lands.
const next = returnAfter(self, compressor);
if (next === undefined) return;
fallback =
next && next.argument ? readAgain(next.argument) : makeVoid0(self);
}
if (groups.length === 0) return;
let result = fallback;
for (let i = groups.length - 1; i >= 0; i--) {
const test = groups[i].tests
.map((/** @type {Node} */ caseTest) =>
makeNode(A.BinaryNode, caseTest, {
operator: "===",
left: caseTest,
right: readAgain(discriminant)
})
)
.reduce((/** @type {Node} */ left, /** @type {Node} */ right) =>
makeNode(A.BinaryNode, right, { operator: "||", left, right })
);
result = makeNode(A.ConditionalNode, self, {
test,
consequent: groups[i].value,
alternate: result
});
}
if (returns) return makeNode(A.ReturnNode, self, { argument: result });
const expression = target
? makeNode(A.AssignNode, self, {
operator: "=",
left: target,
right: result
})
: result;
return makeNode(A.SimpleStatementNode, self, { expression });
};
// terser writes a `switch` of one case and a default as an `if`; one whose
// every branch returns or assigns reads shorter as a conditional.
A.wrapOptimizer("Switch", (optimizeSwitch) => (self, compressor) => {
const optimized = optimizeSwitch(self, compressor);
if (!improvements.enabled || !A.isSwitchNode(optimized)) return optimized;
const written = switchAsConditional(optimized, compressor);
if (!written) return optimized;
const conditional = A.optimize(written, compressor);
// A case's `||` test repeats the name a grouped `case` writes once.
return mangledLength(conditional) <= mangledLength(optimized)
? conditional
: optimized;
});
/**
* Whether calling a function written in place runs nothing: a generator's
* body waits for `next()`, and an async one with no body has none to run.
* @param {Node} call a call
* @param {Compressor} compressor the compressor
* @returns {boolean} true when only an unread iterator or promise is made
*/
const callsNothing = (call, compressor) => {
const fn = call.callee;
return (
!call.optional &&
(A.isArrowNode(fn) || A.isFunctionNode(fn)) &&
(fn.generator ||
(fn.async &&
/** @type {Node[]} */ (/** @type {Node} */ (fn.body).body).length ===
0)) &&
fn.params.every((/** @type {Node} */ param) =>
A.isSymbolFunargNode(param)
) &&
call.arguments.every(
(/** @type {Node} */ arg) => !A.hasSideEffects(arg, compressor)
)
);
};
/**
* Whether nothing reads the value of the node visited: a statement, an
* operand of a sequence before its last, or either under `!` or `void`.
* @param {Node} node the node visited
* @param {Compressor} compressor the compressor
* @returns {boolean} true when the value is dropped
*/
const valueUnread = (node, compressor) => {
let level = 0;
let parent = compressor.parent(level);
while (
A.isUnaryPrefixNode(parent) &&
(parent.operator === "!" || parent.operator === "void")
) {
node = parent;
parent = compressor.parent(++level);
}
return (
(A.isSimpleStatementNode(parent) && parent.expression === node) ||
(A.isSequenceNode(parent) && A.tailNode(parent) !== node)
);
};
A.wrapOptimizer("Call", (optimizeCall) => (self, compressor) => {
const optimized = optimizeCall(self, compressor);
return improvements.enabled &&
A.isCallNode(optimized) &&
!A.isNewNode(optimized) &&
valueUnread(self, compressor) &&
callsNothing(optimized, compressor)
? makeVoid0(optimized)
: optimized;
});
// The well-known symbols of ECMAScript 2015, each a property of `Symbol`.
const WELL_KNOWN_SYMBOLS = new Set([
"hasInstance",
"isConcatSpreadable",
"iterator",
"match",
"replace",
"search",
"species",
"split",
"toPrimitive",
"toStringTag",
"unscopables"
]);
/**
* Whether a node is the global `Symbol`, which no binding shadows.
* @param {Node} node an expression
* @returns {boolean} true for it
*/
const isGlobalSymbol = (node) =>
A.isSymbolRefNode(node) &&
node.name === "Symbol" &&
Boolean(node.definition) &&
node.definition.undeclared;
// Babel's `typeof` helper asks whether `Symbol` exists, which every engine
// running ECMAScript 2015 answers the same.
A.wrapOptimizer("UnaryPrefix", (optimizeUnary) => (self, compressor) => {
if (
improvements.enabled &&
self.operator === "typeof" &&
compressor.option("ecma") >= 2015
) {
const { argument } = self;
if (isGlobalSymbol(argument)) {
return makeNode(A.StringNode, self, { value: "function" });
}
if (
A.isDotNode(argument) &&
isGlobalSymbol(argument.object) &&
WELL_KNOWN_SYMBOLS.has(argument.property.name)
) {
return makeNode(A.StringNode, self, { value: "symbol" });
}
}
return optimizeUnary(self, compressor);
});
// `c ? t1 ? v1 : t2 ? v2 : d : d` as `c && t1 ? v1 : c && t2 ? v2 : d`, where
// that reads shorter: a `c` read once per test must have no effects.
A.wrapOptimizer(
"Conditional",
(optimizeConditional) => (self, compressor) => {
const optimized = optimizeConditional(self, compressor);
if (
!improvements.enabled ||
!A.isConditionalNode(optimized) ||
!A.isConditionalNode(optimized.consequent) ||
A.hasSideEffects(optimized.test, compressor)
) {
return optimized;
}
/** @type {Node[]} */
const links = [];
let link = optimized.consequent;
while (A.isConditionalNode(link)) {
links.push(/** @type {Node} */ (link));
link = /** @type {Node} */ (link).alternate;
}
// The test is read again after each inner one, which must not change it.
if (
!A.isEquivalent(/** @type {Node} */ (link), optimized.alternate) ||
links.some((/** @type {Node} */ inner) =>
A.hasSideEffects(inner.test, compressor)
)
) {
return optimized;
}
let written = optimized.alternate;
for (let i = links.length - 1; i >= 0; i--) {
written = makeNode(A.ConditionalNode, links[i], {
test: makeNode(A.BinaryNode, links[i].test, {
operator: "&&",
left: readAgain(optimized.test),
right: links[i].test
}),
consequent: links[i].consequent,
alternate: written
});
}
return mangledLength(written) < mangledLength(optimized)
? written
: optimized;
}
);
/**
* Whether the value of the node visited goes on as it is, where no test or
* operator around it could fold a constant written in its place.
* @param {Node} node the node visited
* @param {Compressor} compressor the compressor
* @returns {boolean} true for an argument, an element, a property's value, and what a declaration, an assignment, `return` or `+` reads
*/
const passesValueOn = (node, compressor) => {
let level = 0;
let parent = compressor.parent(level);
// terser's `mark_escaped` reads these as passing their value on too.
while (
(A.isBinaryNode(parent) &&
LAZY_OPERATORS.has(parent.operator) &&
node === parent.right) ||
(A.isConditionalNode(parent) && node !== parent.test) ||
(A.isSequenceNode(parent) && node === A.tailNode(parent))
) {
node = parent;
parent = compressor.parent(++level);
}
return (
(A.isCallNode(parent) && node !== parent.callee) ||
A.isArrayNode(parent) ||
(A.isObjectKeyValNode(parent) && node === parent.value) ||
(A.isVarDefLikeNode(parent) && node === parent.init) ||
A.isReturnNode(parent) ||
(A.isBinaryNode(parent) &&
(parent.operator === "+" ||
((parent.operator === "=" || parent.logical) &&
node === parent.right)))
);
};
/**
* Whether a call of a function that only returns a constant reads shorter
* than the constant terser wrote in its place, the function kept for it.
* @param {Node} call the call
* @param {Node} written what terser wrote in its place
* @param {Compressor} compressor the compressor
* @returns {boolean} true when the constant is longer than the call, by half the function where only inlining every call drops it
*/
const keepsConstantCall = (call, written, compressor) => {
const { callee } = call;
if (
!A.isSymbolRefNode(callee) ||
call._annotations & A._INLINE ||
!passesValueOn(call, compressor)
) {
return false;
}
const { definition } = callee;
const fn = A.fixedValue(callee);
const statement =
A.isLambdaNode(fn) &&
/** @type {Node[]} */ (
/** @type {Node} */ (/** @type {Node} */ (fn).body).body
)[0];
if (
!A.isReturnNode(statement) ||
!(/** @type {Node} */ (statement).argument) ||
!A.isConstantExpression(/** @type {Node} */ (statement).argument)
) {
return false;
}
const saved =
A.nodeSize(written, compressor) - A.nodeSize(call, compressor);
// The function stays for the calls kept, so two of them must repay it: a
// call read by a test or an operator still inlines, and may be the rest.
// Passed on as a value, it stays whatever its calls become.
return compressor.option("unused") &&
!compressor.exposed(definition) &&
!definition.escaped
? Math.min(definition.references.length, 2) * saved >
A.nodeSize(/** @type {Node} */ (fn), compressor)
: saved > 0;
};
// terser writes the constant a function returns in place of every call, of
// any length, where a variable's constant replaces only a longer name.
A.wrapOptimizer("Call", (optimizeCall) => (self, compressor) => {
const optimized = optimizeCall(self, compressor);
return improvements.enabled &&
!A.isCallNode(optimized) &&
keepsConstantCall(self, optimized, compressor)
? self
: optimized;
});
/**
* The function a call runs when it is a local one doing nothing: no
* statement in its body, no parameter default or destructuring to run.
* @param {Node} call a call
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its arguments are all the call does
*/
const callsEmptyFunction = (call, compressor) => {
const { callee } = call;
if (
!A.isCallNode(call) ||
call.optional ||
!A.isSymbolRefNode(callee) ||
!callee.definition ||
callee.definition.global ||
call._annotations & A._NOINLINE ||
!compressor.option("reduce_vars") ||
!compressor.option("side_effects") ||
compressor.in_computed_key() ||
call.arguments.some(A.isExpansionNode)
) {
return false;
}
const fn = A.fixedValue(callee);
if (
!fn ||
!A.isLambdaNode(fn) ||
fn.generator ||
fn.async ||
!fn.params.every(A.isSymbolFunargNode)
) {
return false;
}
const block = /** @type {Node | Node[] | undefined} */ (fn.body);
const body = block && !Array.isArray(block) ? block.body : undefined;
return Array.isArray(body) && body.every(modules.common.is_empty);
};
// terser keeps every call in a loop from inlining, so a closure is not made
// each turn; a call to an empty function, which makes none, is dropped too.
A.wrapOptimizer("Call", (optimizeCall) => (self, compressor) => {
const optimized = optimizeCall(self, compressor);
if (
!improvements.enabled ||
optimized !== self ||
!callsEmptyFunction(self, compressor)
) {
return optimized;
}
return A.optimize(
makeSequence(self, [...self.arguments, makeVoid0(self)]),
compressor
);
});
/**
* The length a node prints at once every name it declares is mangled to
* one character; a global keeps its name.
* @param {Node} node a node
* @returns {number} its estimated length
*/
const mangledLength = (node) => {
let length = modules.printToString(node).length;
A.walk(node, (/** @type {Node} */ child) => {
if (
A.isSymbolNode(child) &&
child.definition &&
!child.definition.undeclared &&
!child.definition.global
) {
length -= child.name.length - 1;
}
});
return length;
};
/** @type {WeakMap<Definition, boolean>} */
const everyCallCopied = new WeakMap();
// Their right side may not run, so an argument read there may not either.
const LOGICAL_OPERATORS = new Set(["&&", "||", "??"]);
/**
* @typedef {object} ScopeCall
* @property {Node} call a call of a function the scope declares
* @property {Node[]} ancestors the nodes around it, the scope's node first
*/
/**
* @typedef {object} ScopeFunctionCalls
* @property {number} references how many references the function had when the scope was walked
* @property {ScopeCall[]} calls its calls in the scope's node
*/
/** @type {WeakMap<Scope, Map<Definition, ScopeFunctionCalls>>} */
const callsByScope = new WeakMap();
/**
* The calls in a scope's node of a function it declares, from one walk of
* the scope shared by its functions, walked again once a reference is added.
* @param {Definition} definition the function
* @returns {ScopeCall[]} its calls
*/
const callsOf = (definition) => {
const scope = definition.scope;
const recorded = callsByScope.get(scope);
const entry = recorded && recorded.get(definition);
// A copy made since the walk adds calls the record lacks.
if (entry && entry.references === definition.references.length) {
return entry.calls;
}
/** @type {Map<Definition, ScopeFunctionCalls>} */
const found = new Map([
[definition, { references: definition.references.length, calls: [] }]
]);
A.walk_parent(
scope.node,
(
/** @type {Node} */ node,
/** @type {{ parent: (n?: number) => Node | undefined }} */ parents
) => {
if (!A.isSymbolRefNode(node)) return;
const target = node.definition;
if (!target || target.scope !== scope) return;
let calls = found.get(target);
if (calls === undefined) {
calls = { references: target.references.length, calls: [] };
found.set(target, calls);
}
const call = parents.parent();
if (!call || !A.isCallNode(call) || call.callee !== node) return;
/** @type {Node[]} */
const ancestors = [];
for (let i = 1, parent; (parent = parents.parent(i)); i++) {
ancestors.push(parent);
}
ancestors.reverse();
calls.calls.push({ call, ancestors });
}
);
callsByScope.set(scope, found);
return /** @type {ScopeFunctionCalls} */ (found.get(definition)).calls;
};
/**
* Whether every read of a function is a call a copy can replace, so the
* declaration goes: a copy left beside it only adds to the program.
* @param {Definition} definition the function's name
* @param {(call: Node, parents: Compressor) => boolean} copies whether a call, its ancestors given, takes a copy
* @returns {boolean} true when every read is such a call
*/
const copiesEveryCall = (definition, copies) => {
let result = everyCallCopied.get(definition);
if (result === undefined) {
const calls = callsOf(definition);
result =
calls.length === definition.references.length &&
calls.every(({ call, ancestors }) => {
if (A.isNewNode(call)) return false;
/** @type {{ parent: (n?: number) => Node | undefined }} */
const parents = {
parent: (n = 0) => ancestors[ancestors.length - 1 - n]
};
return copies(call, /** @type {Compressor} */ (parents));
});
everyCallCopied.set(definition, result);
}
return result;
};
/**
* Whether a function's value reads the parameters in order, where nothing
* observable runs before the last read: their arguments then run as they
* did before the call. A constant argument may run anywhere.
* @param {Node} value the expression the body evaluates
* @param {Definition[]} all every parameter
* @param {Definition[]} parameters those whose arguments may have effects
* @returns {boolean} true when the arguments may take their reads' places
*/
const readsParametersInOrder = (value, all, parameters) => {
let next = 0;
/**
* @param {Node} node a part of the value, in evaluation order
* @returns {boolean} false where something observable runs too early
*/
const scan = (node) => {
const done = next === parameters.length;
if (A.isSymbolRefNode(node)) {
const index = parameters.indexOf(node.definition);
if (index === -1) {
if (all.includes(node.definition)) return true;
// A variable an argument cannot write reads the same before it,
// unless it is a `let` or `const` read in its dead zone.
const declared = node.definition.identifiers[0];
return (
done ||
(node.definition.assignments === 0 &&
(A.isSymbolVarNode(declared) ||
A.isSymbolDefunNode(declared) ||
A.isSymbolFunargNode(declared)))
);
}
if (index !== next) return false;
next++;
return true;
}
if (
done &&
!A.walk(node, (/** @type {Node} */ child) =>
A.isSymbolRefNode(child) && parameters.includes(child.definition)
? A.walk_abort
: undefined
)
) {
return true;
}
if (A.isConstantNode(node)) return true;
// `x += a` reads `x` before `a` runs, as `x = a` does not.
if (
A.isAssignNode(node) &&
node.operator === "=" &&
A.isSymbolRefNode(node.left) &&
!all.includes(node.left.definition)
) {
return scan(node.right) && next === parameters.length;
}
if (A.isCallNode(node) && !A.isNewNode(node)) {
if (A.isPropAccessNode(node.callee) || !scan(node.callee)) return false;
for (const argument of node.arguments) {
if (A.isExpansionNode(argument) || !scan(argument)) return false;
}
return next === parameters.length;
}
if (A.isUnaryPrefixNode(node) && node.operator === "!") {
return scan(node.argument);
}
// An operator converting an object may run its `valueOf`.
if (A.isBinaryNode(node) && !LOGICAL_OPERATORS.has(node.operator)) {
return (
scan(node.left) &&
scan(node.right) &&
(next === parameters.length ||
node.operator === "===" ||
node.operator === "!==")
);
}
return false;
};
return scan(value) && next === parameters.length;
};
/**
* The innermost scope around the node the compressor is at, a block's
* own included.
* @param {Compressor} compressor the compressor
* @returns {Scope | undefined} the scope
*/
const innermostScope = (compressor) => {
for (let i = 0, parent; (parent = compressor.parent(i)); i++) {
if (parent.block_scope) return parent.block_scope;
}
return undefined;
};
/**
* Whether every name a value reads from outside it, its parameters aside,
* resolves in the scope to the variable it resolves to where it is written.
* @param {Node} value the value
* @param {Definition[]} parameters the parameters
* @param {Scope} scope the scope it would move to
* @returns {boolean} true when none is shadowed there
*/
const readsSameVariables = (value, parameters, scope) => {
/** @type {Set<Definition>} */
const own = new Set(parameters);
A.walk(value, (/** @type {Node} */ node) => {
if (A.isSymbolDeclarationNode(node) && node.definition) {
own.add(node.definition);
}
});
return !A.walk(value, (/** @type {Node} */ node) => {
if (!A.isSymbolRefNode(node) || own.has(node.definition)) return;
const definition = node.definition;
const found = scope.findVariable(node.name);
if (definition.undeclared ? found : found !== definition) {
return A.walk_abort;
}
});
};
/**
* Whether a node reads a property, `a?.b` included, so a call of it passes
* the object as `this`.
* @param {Node | undefined} node a node
* @returns {boolean} true for such a read
*/
const readsProperty = (node) =>
node !== undefined &&
(A.isPropAccessNode(node) ||
(A.isChainNode(node) && A.isPropAccessNode(node.expression)));
/**
* Whether a call's arguments can take the places of the parameters' reads
* in a copy of the function's value.
* @param {Node} call a call
* @param {Node} value the function's value
* @param {Definition[]} parameters the parameters
* @param {Map<Definition, number>} reads how often the value reads each parameter
* @returns {boolean} true when they run there as they ran before the call
*/
const argumentsMove = (call, value, parameters, reads) =>
call.arguments.length <= parameters.length &&
!call.arguments.some(A.isExpansionNode) &&
// A parameter never read is passed nothing, or a constant the copy drops.
parameters.every(
(parameter, index) =>
reads.has(parameter) ||
index >= call.arguments.length ||
A.isConstantNode(call.arguments[index])
) &&
readsParametersInOrder(
value,
parameters,
parameters.filter(
(parameter, index) =>
reads.has(parameter) &&
index < call.arguments.length &&
!A.isConstantNode(call.arguments[index])
)
) &&
// A method read from an argument and called in its place gets `this`.
!A.walk(value, (/** @type {Node} */ node) =>
A.isCallNode(node) &&
A.isSymbolRefNode(node.callee) &&
readsProperty(call.arguments[parameters.indexOf(node.callee.definition)])
? A.walk_abort
: undefined
);
/**
* Whether a call's value is dropped where it stands.
* @param {Node} call a call
* @param {Node | undefined} parent the node holding it
* @returns {boolean} true in a statement or before a sequence's end
*/
const valueDropped = (call, parent) =>
parent !== undefined &&
(A.isSimpleStatementNode(parent) ||
(A.isSequenceNode(parent) &&
parent.expressions[parent.expressions.length - 1] !== call));
/**
* Whether a call's argument may stand where the call stood.
* @param {Node} call a call
* @param {Node | undefined} parent the node holding it
* @returns {boolean} false where it would be called as a method, or read by `typeof` or `delete`
*/
const argumentStandsAlone = (call, parent) =>
parent !== undefined &&
!(
((A.isCallNode(parent) || A.isNewNode(parent)) &&
parent.callee === call) ||
(A.isPrefixedTemplateStringNode(parent) && parent.tag === call) ||
(A.isUnaryPrefixNode(parent) &&
(parent.operator === "typeof" || parent.operator === "delete"))
);
/**
* A call of a function called from several places, written as its body
* with the arguments in place of the parameters, or undefined.
* @param {Node} call a call
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the copy
*/
const copyOfCalledFunction = (call, compressor) => {
const reference = call.callee;
if (
!A.isSymbolRefNode(reference) ||
A.isNewNode(call) ||
call._annotations & (A._PURE | A._NOINLINE) ||
!compressor.option("inline") ||
!compressor.option("unused") ||
!compressor.option("reduce_vars")
) {
return undefined;
}
const definition = reference.definition;
if (
!definition ||
definition.references.length < 2 ||
(definition.global && !compressor.toplevel.funcs)
) {
return undefined;
}
const lambda = /** @type {Node} */ (A.fixedValue(reference));
if (
!A.isLambdaNode(lambda) ||
// A function expression's own name is bound only inside it.
(lambda.id && A.isFunctionNode(lambda)) ||
lambda.generator ||
lambda.async ||
/** @type {Node[]} */ (/** @type {Node} */ (lambda.body).body).length !==
1 ||
A.usesArguments(lambda) ||
A.containsThis(lambda) ||
modules.common.retain_top_func(lambda, compressor) ||
modules.common.is_recursive_ref(compressor, definition) ||
!A.isConstantExpression(lambda, reference.scope)
) {
return undefined;
}
const statement = /** @type {Node[]} */ (
/** @type {Node} */ (lambda.body).body
)[0];
const returns = A.isReturnNode(statement);
const value = returns
? statement.argument
: A.isSimpleStatementNode(statement)
? statement.expression
: null;
// A property read called in the call's place would call it as a method.
if (!value || readsProperty(value)) return undefined;
/** @type {Definition[]} */
const parameters = [];
for (const parameter of lambda.params) {
if (!A.isSymbolFunargNode(parameter)) return undefined;
parameters.push(parameter.definition);
}
// `function(a){return a}` leaves its argument where the call stood.
const passesArgument =
returns &&
A.isSymbolRefNode(value) &&
parameters.includes(value.definition);
if (A.isSymbolRefNode(value) && !passesArgument) return undefined;
/** @type {Map<Definition, number>} */
const reads = new Map();
A.walk(value, (/** @type {Node} */ node) => {
if (A.isSymbolRefNode(node) && parameters.includes(node.definition)) {
reads.set(node.definition, (reads.get(node.definition) || 0) + 1);
}
});
// Each parameter read once, its argument moving there whole.
if (
[...reads.values()].some((count) => count !== 1) ||
parameters.some((parameter) => parameter.assignments !== 0) ||
// Copied, a name read once is read twice, and no longer inlined there.
A.walk(/** @type {Node} */ (lambda.body), (/** @type {Node} */ node) =>
A.isSymbolRefNode(node) &&
(node.definition === definition ||
(node.definition &&
!node.definition.undeclared &&
!parameters.includes(node.definition) &&
node.definition.references.length === 1))
? A.walk_abort
: undefined
)
) {
return undefined;
}
// A copy spends its body and saves the call: a name, its parentheses
// and commas; two more pay for parentheses wrapping the copy.
const growth =
mangledLength(value) -
reads.size -
3 -
Math.max(0, parameters.length - 1) +
2;
if (
definition.references.length * growth >= mangledLength(lambda) ||
!copiesEveryCall(definition, (other, parents) => {
const otherScope = innermostScope(parents);
return (
argumentsMove(other, value, parameters, reads) &&
(returns || valueDropped(other, parents.parent())) &&
(!passesArgument || argumentStandsAlone(other, parents.parent())) &&
// A `with` or direct `eval` around the call may rebind a name the
// copy reads, or a block there may declare it again.
!globalMayBeRebound(parents) &&
otherScope !== undefined &&
readsSameVariables(value, parameters, otherScope)
);
})
) {
return undefined;
}
// A `with` or direct `eval` around the call may rebind a name the copy
// reads, and a generator or async function reads `yield` or `await` anew.
const scope = innermostScope(compressor);
if (
!scope ||
globalMayBeRebound(compressor) ||
readsFunctionContext(lambda) ||
!readsSameVariables(value, parameters, scope)
) {
return undefined;
}
const copy = A.transformNode(
A.cloneNode(value, true),
A.createTransformer((/** @type {Node} */ node) => {
if (!A.isSymbolRefNode(node)) return undefined;
const index = parameters.indexOf(node.definition);
if (index !== -1) {
return index < call.arguments.length
? call.arguments[index]
: makeVoid0(node);
}
const target = node.definition;
if (target) {
scope.markEnclosed(target);
target.references.push(node);
target.single_use = false;
}
return node;
})
);
return returns ? copy : makeSequence(call, [copy, makeVoid0(call)]);
};
// terser flattens a function into its call only where it is called once.
A.wrapOptimizer("Call", (optimizeCall) => (self, compressor) => {
if (improvements.enabled) {
const copy = copyOfCalledFunction(self, compressor);
if (copy !== undefined) return A.optimize(copy, compressor);
}
return optimizeCall(self, compressor);
});
/**
* Whether a statement is an `if` with no `else` whose branch always leaves.
* @param {Node} node a statement
* @returns {boolean} true for `if (a) return x`, `if (a) break` or `if (a) { b(); continue }`
*/
const isBareExitIf = (node) =>
A.isIfNode(node) &&
!node.alternate &&
Boolean(A.aborts(/** @type {Node} */ (node.consequent)));
/**
* `if (a) return x; if (b) return x;` as `if (a || b) return x;`, any branch
* that always leaves alike: the second test runs only where the first did not leave.
* @param {Node[]} statements the statements, changed in place
* @returns {void}
*/
const mergeRepeatedExits = (statements) => {
for (let i = 0; i < statements.length - 1; i++) {
const first = statements[i];
if (!isBareExitIf(first)) continue;
const second = statements[i + 1];
if (
!isBareExitIf(second) ||
/** @type {Node} */ (first.consequent).type !==
/** @type {Node} */ (second.consequent).type ||
!A.isEquivalent(
/** @type {Node} */ (first.consequent),
/** @type {Node} */ (second.consequent)
)
) {
continue;
}
statements.splice(
i,
2,
makeNode(A.IfNode, first, {
test: makeNode(A.BinaryNode, first.test, {
operator: "||",
left: first.test,
right: second.test
}),
consequent: first.consequent,
alternate: null
})
);
i--;
}
};
for (const name of [
"Block",
"BlockStatement",
"Lambda",
"Arrow",
"Function",
"ClassStaticBlock"
]) {
A.wrapOptimizer(name, (optimizeBody) => (self, compressor) => {
const statements = A.bodyOf(self);
if (improvements.enabled && Array.isArray(statements)) {
mergeRepeatedExits(statements);
}
return optimizeBody(self, compressor);
});
}
/**
* Drops the values of the `var`s in a catch clause naming its parameter where
* nothing reads the parameter, and the `var`s too where another declares the
* name: Annex B has such a `var`'s value written to the parameter.
* @param {Node} handler a catch clause
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const dropCatchRedeclarations = (handler, compressor) => {
const { param } = handler;
if (!param || !A.isSymbolCatchNode(param)) return;
const { definition } = param;
if (
!definition ||
definition.scope.isPinned() ||
globalMayBeRebound(compressor) ||
definition.references.some((/** @type {Node} */ reference) =>
A.isSymbolRefNode(reference)
)
) {
return;
}
const statements = /** @type {Node[]} */ (
/** @type {Node} */ (handler.body).body
);
/** @type {Set<Node>} */
const redeclarations = new Set();
for (const statement of statements) {
if (
A.isVarNode(statement) &&
statement.declarations.every(
(/** @type {Node} */ declaration) =>
A.isSymbolVarNode(declaration.id) &&
declaration.id.name === param.name
)
) {
for (const declaration of statement.declarations) {
redeclarations.add(declaration.id);
}
}
}
if (redeclarations.size === 0) return;
const outer = definition.scope.getDefunScope().getBinding(param.name);
// A function expression's own name is shadowed by the `var`, not declared.
const declaredElsewhere = Boolean(
outer &&
outer.identifiers.some(
(/** @type {Node} */ identifier) =>
!redeclarations.has(identifier) && !A.isSymbolLambdaNode(identifier)
)
);
for (let i = 0; i < statements.length; i++) {
const statement = statements[i];
if (
!A.isVarNode(statement) ||
!redeclarations.has(statement.declarations[0].id)
) {
continue;
}
/** @type {Node[]} */
const effects = [];
for (const declaration of statement.declarations) {
const effect =
declaration.init &&
A.dropSideEffectFree(declaration.init, compressor);
if (effect) effects.push(effect);
declaration.init = /** @type {Node} */ (
/** @type {Node | null} */ (null)
);
}
/** @type {Node[]} */
const replacement = declaredElsewhere ? [] : [statement];
if (effects.length > 0) {
replacement.push(
makeNode(A.SimpleStatementNode, statement, {
expression: makeSequence(statement, effects)
})
);
}
statements.splice(i, 1, ...replacement);
i += replacement.length - 1;
}
};
A.wrapOptimizer("Try", (optimizeTry) => (self, compressor) => {
if (improvements.enabled && self.handler) {
dropCatchRedeclarations(self.handler, compressor);
}
return optimizeTry(self, compressor);
});
/**
* The `typeof` an equality with a string reads, and that string, in either
* order, as terser leaves either.
* @param {Node} node a binary expression
* @returns {{ typeofNode: Node, type: string, negated: boolean } | undefined} its parts
*/
const typeofComparison = (node) => {
const negated = node.operator === "!=" || node.operator === "!==";
if (!negated && node.operator !== "==" && node.operator !== "===") {
return undefined;
}
const isTypeof = (/** @type {Node} */ side) =>
A.isUnaryPrefixNode(side) && side.operator === "typeof";
if (isTypeof(node.left) && A.isStringNode(node.right)) {
return {
typeofNode: node.left,
type: /** @type {string} */ (node.right.value),
negated
};
}
return isTypeof(node.right) && A.isStringNode(node.left)
? {
typeofNode: node.right,
type: /** @type {string} */ (node.left.value),
negated
}
: undefined;
};
/**
* Whether an expression is a number literal, which leaves a `+` beside it
* in arithmetic nothing to coerce.
* @param {Node} node an operand
* @returns {boolean} true when it is
*/
const isNumberLiteral = (node) =>
A.isNumberNode(node) ||
(A.isUnaryPrefixNode(node) &&
node.operator === "-" &&
A.isNumberNode(node.argument));
// Arithmetic that converts each operand to a number itself, unlike `+`.
const ARITHMETIC_OPERATORS = new Set(["-", "*", "/", "%", "**"]);
/**
* A binary expression written shorter: a `typeof` against `"undefined"` as
* an ordering, and a `+` beside a number literal in arithmetic dropped.
* @param {Node} node a binary expression terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node} what replaces it
*/
const shortenBinary = (node, compressor) => {
const comparison = typeofComparison(node);
// Every other `typeof` answer sorts before "u", and "undefined" after it.
if (comparison && comparison.type === "undefined") {
return makeNode(A.BinaryNode, node, {
operator: comparison.negated ? "<" : ">",
left: comparison.typeofNode,
right: makeNode(A.StringNode, node, { value: "u" })
});
}
if (
node.operator === "-" ||
node.operator === "*" ||
node.operator === "/" ||
node.operator === "%" ||
node.operator === "**" ||
node.operator === "|" ||
node.operator === "&" ||
node.operator === "^" ||
node.operator === "<<" ||
node.operator === ">>" ||
node.operator === ">>>"
) {
const withoutPlus = (/** @type {Node} */ side) =>
A.isUnaryPrefixNode(side) && side.operator === "+"
? side.argument
: side;
if (isNumberLiteral(node.left)) {
node.right = withoutPlus(node.right);
} else if (isNumberLiteral(node.right)) {
node.left = withoutPlus(node.left);
} else if (
ARITHMETIC_OPERATORS.has(node.operator) &&
compressor.option("unsafe_math")
) {
// A left already a number keeps a BigInt right throwing, as its `+`
// did; the right evaluates with no effect, or converts as a primitive.
const right = withoutPlus(node.right);
if (
right !== node.right &&
!A.hasSideEffects(right, compressor) &&
((A.isUnaryPrefixNode(node.left) && node.left.operator === "+") ||
(!A.hasSideEffects(node.left, compressor) &&
(A.isString(node.left, compressor) || A.isBoolean(node.left))))
) {
node.right = right;
}
}
}
if (node.operator === "&&" || node.operator === "||") {
const truthiness = objectTruthiness(node);
if (truthiness) node.right = truthiness;
}
return node;
};
/**
* `typeof x == "object" && x !== null` as `… && !!x`, and `typeof x !=
* "object" || x === null` as `… || !x`: past the `typeof`, only `null` is falsy.
* @param {Node} node a logical expression
* @returns {Node | undefined} the right side written shorter, or undefined
*/
const objectTruthiness = (node) => {
const and = node.operator === "&&";
const chained =
A.isBinaryNode(node.left) && node.left.operator === node.operator;
const left = chained ? node.left.right : node.left;
const { right } = node;
const comparison = A.isBinaryNode(left) && typeofComparison(left);
if (
!comparison ||
comparison.type !== "object" ||
comparison.negated === and ||
!A.isBinaryNode(right) ||
!(and
? right.operator === "!==" || right.operator === "!="
: right.operator === "===" || right.operator === "==")
) {
return undefined;
}
const name = comparison.typeofNode.argument;
// terser writes a comparison's constant first.
const tested = right.right;
if (
!A.isNullNode(right.left) ||
!A.isSymbolRefNode(name) ||
!A.isEquivalent(name, tested)
) {
return undefined;
}
const not = makeNode(A.UnaryPrefixNode, node, {
operator: "!",
argument: tested
});
return and
? makeNode(A.UnaryPrefixNode, node, { operator: "!", argument: not })
: not;
};
/**
* The opposite of a `typeof` ordered against "u", as written above: no
* `typeof` answers "u" itself, so the other order is its negation.
* @param {Node} node an expression
* @returns {Node | undefined} the negation, or undefined for another node
*/
const negatedTypeofOrder = (node) => {
if (
!A.isBinaryNode(node) ||
(node.operator !== "<" && node.operator !== ">")
) {
return undefined;
}
const isTypeof = (/** @type {Node} */ side) =>
A.isUnaryPrefixNode(side) && side.operator === "typeof";
const isU = (/** @type {Node} */ side) =>
A.isStringNode(side) && side.value === "u";
return (isTypeof(node.left) && isU(node.right)) ||
(isU(node.left) && isTypeof(node.right))
? makeNode(A.BinaryNode, node, {
operator: node.operator === "<" ? ">" : "<",
left: node.left,
right: node.right
})
: undefined;
};
const negate = A.negate;
/**
* terser's `negate`, flipping a `typeof` ordered against "u".
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node} the negation
*/
A.negate = (node, compressor, firstInStatement) =>
(improvements.enabled && negatedTypeofOrder(node)) ||
negate(node, compressor, firstInStatement);
A.wrapOptimizer(
"UnaryPrefix",
(optimizeUnaryPrefix) => (self, compressor) => {
const optimized = optimizeUnaryPrefix(self, compressor);
return (
(improvements.enabled &&
A.isUnaryPrefixNode(optimized) &&
optimized.operator === "!" &&
negatedTypeofOrder(optimized.argument)) ||
optimized
);
}
);
/**
* Whether a declared binding is no property of the global object, which may
* be an accessor: one below the top level, in a module, or `let`, `const` or `class`.
* @param {Definition | SymbolDefinition} definition the binding
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it is none
*/
const ownsBinding = (definition, compressor) =>
!definition.undeclared &&
(!definition.global ||
Boolean(compressor.option("module")) ||
A.isSymbolBlockDeclarationNode(definition.identifiers[0]));
/**
* Whether reading a name twice gives one value: a binding of its own, not
* reached through `with`.
* @param {Node} node a name read
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it does
*/
const readsAlike = (node, compressor) => {
const definition = node.definition;
if (!definition || !ownsBinding(definition, compressor)) return false;
for (let level = 0, parent; (parent = compressor.parent(level)); level++) {
if (A.isWithNode(parent)) return false;
}
return true;
};
/**
* Whether a copy a write through it left without a fixed value still holds
* the name it copies at every read: in its own function, or in one created
* after it was set, none reached through a hoisted declaration.
* @param {Definition} definition the copy
* @returns {boolean} true when it does
*/
const copiedThroughout = (definition) => {
// `copyOf` vouches for the read that set it; one met before it was set
// would find the name it copies where the copy was still unset.
if (!definition.copyOf || definition.readUnset) return false;
const functionScope = definition.scope.getDefunScope();
let nested = false;
for (const reference of definition.references) {
for (
let scope = /** @type {Scope | null} */ (
reference.scope.getDefunScope()
);
scope !== functionScope;
scope = scope.upper ? scope.upper.getDefunScope() : null
) {
if (!scope || A.isDefunNode(scope.node)) return false;
nested = true;
}
}
// Read once in its own function, the copy is `collapse_vars`' to fold.
return nested || definition.references.length > 1;
};
/**
* Whether a name keeps its binding's value while a getter reading through
* it runs: one `reduce_vars` saw never assigned after its declaration.
* @param {Node} node a name read
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it does
*/
const staysPut = (node, compressor) =>
A.isSymbolRefNode(node) &&
readsAlike(node, compressor) &&
compressor.option("reduce_vars") &&
node.definition.assignments === 0;
/**
* Whether a name `reduce_vars` gave a fixed value is written once, by the
* write that value comes from, as in `var x; x = y`, and read more than once
* or from a function inside.
* @param {Node} node a name read
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it is
*/
const setOnceBefore = (node, compressor) => {
if (
!A.isSymbolRefNode(node) ||
!readsAlike(node, compressor) ||
!compressor.option("reduce_vars")
) {
return false;
}
const definition = node.definition;
if (
definition.assignments !== 1 ||
!(
definition.fixed ||
// A write through the copy dropped `fixed`; set once, from nothing,
// `copyOf` is what every read finds.
(definition.copyOf && !definition.declaredWithValue)
)
) {
return false;
}
// Read once in its own function, the name is `collapse_vars`' to fold.
const functionScope = definition.scope.getDefunScope();
return (
definition.references.length > 2 ||
definition.references.some(
(/** @type {Node} */ reference) =>
reference.scope.getDefunScope() !== functionScope
)
);
};
/**
* Whether a scope is another or one around it.
* @param {Scope} outer the scope that may be around
* @param {Scope} inner the scope it may be around
* @returns {boolean} true when it is
*/
const isScopeOrAround = (outer, inner) => {
for (
let scope = /** @type {Scope | null} */ (inner);
scope;
scope = scope.upper
) {
if (scope === outer) return true;
}
return false;
};
/**
* Whether every read of a `var` copying a binding finds the value it was set
* from: a parameter, `let` or `const` set once each time the `var`'s function
* runs, or a `var` or function beside it no function inside reads, as a loop
* may set it again.
* @param {Definition} definition the `var`
* @param {Definition} target the binding it copies
* @returns {boolean} true when every read does
*/
const copiesForEveryRead = (definition, target) => {
const declaration = target.identifiers[0];
if (
A.isSymbolFunargNode(declaration) ||
A.isSymbolLetNode(declaration) ||
A.isSymbolConstNode(declaration) ||
A.kindOf(declaration) === "SymbolImport"
) {
return isScopeOrAround(target.scope, definition.scope);
}
if (target.scope !== definition.scope) {
return (
isScopeOrAround(target.scope, definition.scope) &&
setOncePerCall(target)
);
}
return (
definition.references.every(
(/** @type {Node} */ reference) =>
reference.scope.getDefunScope() === definition.scope
) ||
declaredRightAfter(definition, target) ||
readByFunctionsCreatedAfter(definition, target)
);
};
/**
* Whether every function reading a `var` copying another beside it is a
* function expression created after the copy's declaration, which no loop
* holds, so none can run and read the copy before it is set.
* @param {Definition} definition the copy
* @param {Definition} target the `var` it copies
* @returns {boolean} true when it is
*/
const readByFunctionsCreatedAfter = (definition, target) => {
const copy = definition.identifiers[0];
const scopeNode = definition.scope.node;
if (!A.isSymbolVarNode(copy) || !A.isSymbolVarNode(target.identifiers[0])) {
return false;
}
/** @type {Set<Node>} */
const readers = new Set();
for (const reference of definition.references) {
let reader;
for (
let scope = /** @type {Scope | null} */ (reference.scope);
scope && scope !== definition.scope;
scope = scope.upper
) {
if (A.isLambdaNode(scope.node)) reader = scope.node;
}
if (reader === undefined) continue;
if (A.isDefunNode(reader)) return false;
readers.add(reader);
}
let declared = false;
let created = 0;
let safe = true;
A.walk_parent(
scopeNode,
(
/** @type {Node} */ node,
/** @type {{ parent: (n?: number) => Node | undefined }} */ parents
) => {
if (node === scopeNode) return undefined;
if (node === copy) {
declared = true;
for (let i = 0, parent; (parent = parents.parent(i)); i++) {
if (parent === scopeNode) break;
if (A.isIterationStatementNode(parent)) safe = false;
}
return safe ? true : A.walk_abort;
}
if (!readers.has(node)) return undefined;
if (!declared) {
safe = false;
return A.walk_abort;
}
created++;
return true;
}
);
return safe && declared && created === readers.size;
};
/**
* Whether a `var` copying another beside it is declared right after it, so
* no function can run and read the copy between the two, and neither sits
* in a loop that could set it again.
* @param {Definition} definition the copy
* @param {Definition} target the `var` it copies
* @returns {boolean} true when it is
*/
const declaredRightAfter = (definition, target) => {
const copy = definition.identifiers[0];
const original = target.identifiers[0];
const scopeNode = target.scope.node;
if (
!A.isSymbolVarNode(copy) ||
!A.isSymbolVarNode(original) ||
(A.isLambdaNode(scopeNode) && (scopeNode.async || scopeNode.generator))
) {
return false;
}
let adjacent = false;
A.walk_parent(
scopeNode,
(
/** @type {Node} */ node,
/** @type {{ parent: (n?: number) => Node | undefined }} */ parents
) => {
if (node !== copy) return undefined;
const declarator = parents.parent(0);
const statement = parents.parent(1);
const holder = parents.parent(2);
if (!declarator || !statement || !A.isVarNode(statement) || !holder) {
return A.walk_abort;
}
const { declarations } = statement;
const index = declarations.indexOf(declarator);
let previous;
if (index > 0) {
previous = declarations[index - 1];
} else {
// The last declarator of the statement before, in the same list.
const body = A.bodyOf(holder);
const list = Array.isArray(body) ? body : [];
const position = list.indexOf(statement);
const before = position > 0 && list[position - 1];
previous =
before &&
A.isVarNode(before) &&
before.declarations[before.declarations.length - 1];
}
adjacent = Boolean(previous && previous.id === original);
for (let i = 2, parent; adjacent && (parent = parents.parent(i)); i++) {
if (parent === scopeNode) break;
if (A.isIterationStatementNode(parent)) adjacent = false;
}
return A.walk_abort;
}
);
return adjacent;
};
/**
* Whether a `var` is set once each time its function runs, past which no
* read inside a function nested in it can find it changed: its one
* declaration sits in no loop, and the function never pauses.
* @param {Definition} target the `var`
* @returns {boolean} true when it is
*/
const setOncePerCall = (target) => {
const scopeNode = target.scope.node;
if (
!A.isLambdaNode(scopeNode) ||
scopeNode.async ||
scopeNode.generator ||
!A.isSymbolVarNode(target.identifiers[0])
) {
return false;
}
const declaration = target.identifiers[0];
let inLoop = true;
A.walk_parent(
scopeNode,
(
/** @type {Node} */ node,
/** @type {{ parent: (n?: number) => Node | undefined }} */ parents
) => {
// Nested functions can capture this var, but cannot declare it.
if (node !== scopeNode && A.isLambdaNode(node)) return true;
if (node !== declaration) return undefined;
inLoop = false;
for (let i = 0, parent; (parent = parents.parent(i)); i++) {
if (parent === scopeNode) break;
if (A.isIterationStatementNode(parent)) inLoop = true;
}
return A.walk_abort;
}
);
return !inLoop;
};
/**
* `y` read in place of `x` where `let x = y`, or `const` or `var`, only
* copies it: neither name is written again, `x` is read only after it is
* set, and `y` is found there.
* @param {Node} ref a read terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the read of `y`, or undefined
*/
const aliasedRead = (ref, compressor) => {
const definition = ref.definition;
const declaration = definition && definition.identifiers[0];
if (
!definition ||
!(definition.fixed || copiedThroughout(definition)) ||
definition.identifiers.length !== 1 ||
!(
A.isSymbolLetNode(declaration) ||
A.isSymbolConstNode(declaration) ||
(A.isSymbolVarNode(declaration) && !A.isSymbolFunargNode(declaration))
) ||
compressor.exposed(definition) ||
!(staysPut(ref, compressor) || setOnceBefore(ref, compressor))
) {
return undefined;
}
const value = /** @type {Node} */ (
definition.fixed ? A.fixedValue(ref) : definition.copyOf
);
if (
!A.isSymbolRefNode(value) ||
value.definition === definition ||
// Unmangled, a longer name read more than once may outweigh the copy.
(value.name.length > ref.name.length &&
definition.references.length > 1 &&
(!compressor._mangle_options || A.isToplevelNode(definition.scope)))
) {
return undefined;
}
const target = value.definition;
const targetDeclaration = target && target.identifiers[0];
if (
!target ||
!ownsBinding(target, compressor) ||
compressor.exposed(target) ||
target.assignments !== 0 ||
target.identifiers.length !== 1 ||
target.scope.isPinned() ||
// A parameter has no fixed value to say a pattern or loop head wrote it.
(A.isSymbolFunargNode(targetDeclaration)
? target.scope.usesArguments || target.patternWrites !== 0
: !target.fixed &&
// As swc assumes, no module writes a binding it exports.
A.kindOf(targetDeclaration) !== "SymbolImport") ||
!ref.scope ||
ref.scope.findVariable(value.name) !== target
) {
return undefined;
}
let scope = ref.scope;
if (A.isSymbolVarNode(declaration)) {
// A read of a `var` holds its function's scope, not the block's around it.
const nearest = compressor.find_scope();
// A `var` read once copying a parameter of a call in place is terser's to
// collapse: read twice, that parameter's argument stays where it is.
if (
(definition.references.length < 2 &&
A.isSymbolFunargNode(targetDeclaration) &&
target.fixed) ||
!nearest ||
nearest.findVariable(value.name) !== target ||
!copiesForEveryRead(definition, target)
) {
return undefined;
}
if (
A.isSymbolLetNode(targetDeclaration) ||
A.isSymbolConstNode(targetDeclaration)
) {
scope = nearest;
}
}
const read = makeNode(A.SymbolRefNode, ref, {
name: value.name,
definition: target,
scope
});
// A function reading `y` now captures it: inlining one into a scope that
// declares the name again must see the clash.
scope.markEnclosed(target);
// Counted, the read keeps `collapse_vars` from moving `y`'s value away;
// read again, `y` is no longer read once for inlining at the copy.
target.references.push(read);
target.single_use = false;
return read;
};
/** @type {WeakMap<Definition, boolean>} */
const falsyVariables = new WeakMap();
/**
* Whether a constant is falsy and its expression has no effect.
* @param {Node} node an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it is
*/
const isFalsyConstant = (node, compressor) => {
if (A.hasSideEffects(node, compressor)) return false;
const value = A.evaluate(node, compressor);
return !A.isSyntaxNode(value) && !value;
};
/**
* Whether a `var` only ever holds falsy values: each write stores a falsy
* constant, and a read before the first finds `undefined`.
* @param {Definition} definition the variable
* @param {Compressor} compressor the compressor
* @returns {boolean} true when every read of it is falsy
*/
const isFalsyVariable = (definition, compressor) => {
const known = falsyVariables.get(definition);
if (known !== undefined) return known;
let falsy =
!definition.undeclared &&
!compressor.exposed(definition) &&
!definition.scope.isPinned() &&
definition.identifiers.every(
(/** @type {Node} */ identifier) =>
A.isSymbolVarNode(identifier) && !A.isSymbolFunargNode(identifier)
);
// Every read and write must be in the tree: one an inlining builds is not yet.
let seen = 0;
if (falsy) {
const walker = A.createWalker((/** @type {Node} */ node) => {
if (!falsy) return true;
if (A.isVarDefNode(node) && node.id.definition === definition) {
const loop = walker.parent(1);
if (
(node.init && !isFalsyConstant(node.init, compressor)) ||
(A.isForInNode(loop) && loop.left === walker.parent(0))
) {
falsy = false;
}
return;
}
if (!A.isSymbolRefNode(node) || node.definition !== definition) {
return;
}
seen++;
const parent = walker.parent(0);
if (A.isAssignNode(parent) && parent.left === node) {
if (
parent.operator !== "=" ||
!isFalsyConstant(parent.right, compressor)
) {
falsy = false;
}
} else if (
(A.isUnaryNode(parent) &&
(parent.operator === "++" || parent.operator === "--")) ||
(A.isForInNode(parent) && parent.left === node) ||
A.isDestructuringNode(parent) ||
A.isDefaultAssignNode(parent) ||
A.isObjectPropertyNode(parent) ||
A.isExpansionNode(parent)
) {
// A target that stores whatever it is given, or may be one.
falsy = false;
}
});
A.walkNode(definition.scope.node, walker);
if (seen !== definition.references.length) falsy = false;
}
// Only a no is final: a write may yet be added to a tree being built.
if (!falsy) falsyVariables.set(definition, falsy);
return falsy;
};
A.wrapOptimizer("SymbolRef", (optimizeRef) => (self, compressor) => {
const optimized = optimizeRef(self, compressor);
if (
!improvements.enabled ||
!A.isSymbolRefNode(optimized) ||
compressor.is_lhs()
) {
return optimized;
}
const parent = compressor.parent();
if (
optimized.definition &&
(compressor.in_boolean_context() ||
(A.isBinaryNode(parent) &&
parent.operator === "||" &&
parent.left === self)) &&
isFalsyVariable(optimized.definition, compressor)
) {
return makeNode(A.FalseNode, optimized);
}
return aliasedRead(optimized, compressor) || optimized;
});
/**
* Whether a target reads and writes one place however often it is written:
* a name, or a property of `this` or of a name a getter cannot repoint.
* @param {Node} node an assignment's target
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it does
*/
const isSimpleTarget = (node, compressor) =>
A.isSymbolRefNode(node)
? readsAlike(node, compressor)
: (A.isDotNode(node) || A.isSubNode(node)) &&
!node.optional &&
(A.isThisNode(node.object) || staysPut(node.object, compressor)) &&
// A name's value may convert to a key differently each time.
(A.isDotNode(node) || A.isConstantNode(node.property));
// The compound operators whose operands may swap: `*` multiplies numbers or
// BigInts alike either way round, and the bitwise ones alike.
const COMMUTATIVE_COMPOUND_OPERATORS = new Set(["*", "&", "|", "^"]);
/**
* Whether an access names, read twice with nothing run between, one place:
* a property of `this` or of a name of its own, wherever it is assigned.
* @param {Node} node an assignment's target
* @param {Compressor} compressor the compressor
* @returns {boolean} true for `this.p` and `o.p`
*/
const readsTwiceAlike = (node, compressor) =>
(A.isDotNode(node) || A.isSubNode(node)) &&
!node.optional &&
(A.isThisNode(node.object) ||
(A.isSymbolRefNode(node.object) &&
readsAlike(node.object, compressor))) &&
// A name's value may convert to a key differently each time.
(A.isDotNode(node) || A.isConstantNode(node.property));
/**
* `a || (a = b)` as `a ||= b` and `a || (x(), a = b)` as `a ||= (x(), b)`,
* `&&` and `??` alike, from ECMAScript 2021 on: both read the target
* once and write it last.
* @param {Node} node a logical expression
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the logical assignment, or undefined
*/
const logicalAssignment = (node, compressor) => {
const { operator, left, right } = node;
const assign = A.isSequenceNode(right)
? right.expressions[right.expressions.length - 1]
: right;
if (
(operator !== "||" && operator !== "&&" && operator !== "??") ||
!A.isAssignNode(assign) ||
assign.operator !== "=" ||
// A target's object is found before the sequence runs, not after.
!isSimpleTarget(assign.left, compressor) ||
!A.isEquivalent(left, assign.left) ||
compressor.option("ecma") < 2021
) {
return undefined;
}
return makeNode(A.AssignNode, node, {
operator: `${operator}=`,
logical: true,
left: assign.left,
right:
assign === right
? assign.right
: makeNode(A.SequenceNode, right, {
expressions: [...right.expressions.slice(0, -1), assign.right]
})
});
};
/**
* Whether writing a name again with the value it holds does nothing: a
* `var`, `let` or parameter, never a `const` or a function expression's name.
* @param {Node} node a name read
* @param {Compressor} compressor the compressor
* @returns {boolean} true when the write is unobservable
*/
const isRewritable = (node, compressor) => {
if (!readsAlike(node, compressor)) return false;
const declaration = node.definition.identifiers[0];
return (
A.isSymbolVarNode(declaration) ||
A.isSymbolLetNode(declaration) ||
A.isSymbolFunargNode(declaration)
);
};
// The compound operators whose result is a number for a number on the left.
const NUMERIC_ASSIGN_OPERATORS = new Set([
"-=",
"*=",
"/=",
"%=",
"**=",
"<<=",
">>=",
">>>=",
"&=",
"|=",
"^="
]);
/** @type {WeakMap<Definition, boolean>} */
const numericDefinitions = new WeakMap();
/**
* Whether a name only ever holds a number, `undefined` or `null`: every write
* to it stores one, so `+` on it adds where it could concatenate.
* @param {Node} node a name read
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it does
*/
const holdsOnlyNumbers = (node, compressor) => {
const definition = node.definition;
if (
!definition ||
definition.undeclared ||
// As for its value, no other script writes a top-level `let`.
(compressor.exposed(definition) &&
!A.isSymbolLetNode(definition.identifiers[0])) ||
definition.scope.isPinned() ||
definition.patternWrites !== 0
) {
return false;
}
const known = numericDefinitions.get(definition);
if (known !== undefined) return known;
let numeric = true;
const walker = A.createWalker((/** @type {Node} */ child) => {
if (!numeric) return true;
if (!A.isSymbolNode(child) || child.definition !== definition) return;
const parent = walker.parent(0);
if (A.isSymbolDeclarationNode(child)) {
const loop = walker.parent(2);
numeric =
A.isVarDefNode(parent) &&
parent.id === child &&
(!parent.init || A.isNumber(parent.init, compressor)) &&
// A `for-in` or `for-of` head assigns its declared names each key.
!(A.isForInNode(loop) && loop.left === walker.parent(1));
return;
}
if (A.isAssignNode(parent) && parent.left === child) {
numeric =
!parent.logical &&
(NUMERIC_ASSIGN_OPERATORS.has(parent.operator) ||
A.isNumber(parent.right, compressor));
}
});
A.walkNode(definition.scope.node, walker);
numericDefinitions.set(definition, numeric);
return numeric;
};
A.wrapOptimizer("Assign", (optimizeAssign) => (self, compressor) => {
const optimized = optimizeAssign(self, compressor);
// x -= 1 -> --x and x += 1 -> ++x only where `x` holds only numbers: on a
// BigInt `-= 1` throws where `--` does not, and `+` may concatenate. oxc
// rewrites `-=` on any reference, a bug swc does not share.
if (
improvements.enabled &&
A.isAssignNode(optimized) &&
!optimized.logical &&
A.isNumberNode(optimized.right) &&
optimized.right.value === 1 &&
(optimized.operator === "-=" || optimized.operator === "+=") &&
A.isSymbolRefNode(optimized.left) &&
holdsOnlyNumbers(optimized.left, compressor)
) {
return makeNode(A.UnaryPrefixNode, optimized, {
operator: optimized.operator === "-=" ? "--" : "++",
argument: optimized.left
});
}
// a = a || b -> a ||= b, where writing `a` back to itself shows nothing.
if (
improvements.enabled &&
A.isAssignNode(optimized) &&
optimized.operator === "=" &&
A.isSymbolRefNode(optimized.left) &&
A.isBinaryNode(optimized.right) &&
isRewritable(optimized.left, compressor) &&
A.isEquivalent(optimized.left, optimized.right.left)
) {
const { operator } = optimized.right;
if (
(operator === "||" || operator === "&&" || operator === "??") &&
compressor.option("ecma") >= 2021
) {
return makeNode(A.AssignNode, optimized, {
operator: `${operator}=`,
logical: true,
left: optimized.left,
right: optimized.right.right
});
}
}
// o.p = o.p + 1 -> o.p += 1 and o.p = 2 * o.p -> o.p *= 2: both find `o`
// before the right side runs, and a constant converts to a number alike.
if (
improvements.enabled &&
A.isAssignNode(optimized) &&
optimized.operator === "=" &&
A.isPropAccessNode(optimized.left) &&
A.isBinaryNode(optimized.right) &&
COMPOUND_OPERATORS.has(optimized.right.operator) &&
readsTwiceAlike(optimized.left, compressor)
) {
const { left, right } = optimized.right;
const operand = A.isEquivalent(optimized.left, left)
? right
: COMMUTATIVE_COMPOUND_OPERATORS.has(optimized.right.operator) &&
A.isConstant(left) &&
A.isEquivalent(optimized.left, right)
? left
: undefined;
if (operand !== undefined) {
return makeNode(A.AssignNode, optimized, {
operator: `${optimized.right.operator}=`,
logical: false,
left: optimized.left,
right: operand
});
}
}
return optimized;
});
// `if (a); else b` as `if (!a) b`, which terser writes only where `b` is one
// expression statement.
A.wrapOptimizer("If", (optimizeIf) => (self, compressor) => {
const optimized = optimizeIf(self, compressor);
if (
!improvements.enabled ||
!A.isIfNode(optimized) ||
!optimized.alternate ||
!(
A.isEmptyStatementNode(optimized.consequent) ||
(A.isBlockStatementNode(optimized.consequent) &&
/** @type {Node[]} */ (
/** @type {Node} */ (optimized.consequent).body
).length === 0)
)
) {
return optimized;
}
const { test } = optimized;
// A chain negated spreads its `!` through, losing the repeats gzip found.
if (
A.isBinaryNode(test) &&
(test.operator === "&&" || test.operator === "||")
) {
return optimized;
}
return A.optimize(
makeNode(A.IfNode, optimized, {
test: A.negate(test, compressor),
consequent: optimized.alternate,
alternate: null
}),
compressor
);
});
/**
* `(a?.b)?.()` as `a?.b?.()`: an optional link on a parenthesized chain
* stops on the same `null` or `undefined` the joined chain stops on.
* @param {Node} node an access or call terser has optimized
* @returns {Node} the link, its inner chain joined
*/
const joinChain = (node) => {
if (!node.optional) return node;
if (A.isCallNode(node)) {
if (A.isChainNode(node.callee)) node.callee = node.callee.expression;
} else if (A.isChainNode(node.object)) {
node.object = node.object.expression;
}
return node;
};
for (const kind of ["Dot", "Sub"]) {
A.wrapOptimizer(kind, (optimizeAccess) => (self, compressor) => {
const optimized = optimizeAccess(self, compressor);
return improvements.enabled && A.isPropAccessNode(optimized)
? joinChain(optimized)
: optimized;
});
}
/**
* `Object(f)(x)` as `f(x)`, and `Object(a.b)(x)` as `(0, a.b)(x)`: `Object`
* hands back a function it is passed, and calling what it wraps throws alike.
* @param {Node} call a call terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the call of what `Object` was passed, or undefined
*/
const unwrapObjectCallee = (call, compressor) => {
const inner = call.callee;
if (
A.isNewNode(call) ||
// Object(null) is callable neither way, but null?.() would not throw.
call.optional ||
!A.isCallNode(inner) ||
A.isNewNode(inner) ||
inner.optional ||
!A.isSymbolRefNode(inner.callee) ||
inner.callee.name !== "Object" ||
inner.arguments.length !== 1 ||
A.isExpansionNode(inner.arguments[0]) ||
!isUndeclaredRef(inner.callee) ||
isReboundGlobal("Object") ||
globalMayBeRebound(compressor)
) {
return undefined;
}
const [target] = inner.arguments;
// Called through a property, the target would see its object as `this`.
call.callee =
A.isPropAccessNode(target) || A.isChainNode(target)
? makeNode(A.SequenceNode, inner, {
expressions: [makeNode(A.NumberNode, inner, { value: 0 }), target]
})
: target;
return call;
};
A.wrapOptimizer("Call", (optimizeCall) => (self, compressor) => {
const optimized = optimizeCall(self, compressor);
if (!improvements.enabled || !A.isCallNode(optimized)) return optimized;
return unwrapObjectCallee(optimized, compressor) || joinChain(optimized);
});
/**
* The link a chain reads its subject through: the access or call whose
* object or callee is the name the chain starts from.
* @param {Node} node an access or call
* @returns {Node | undefined} that link, or undefined past another node
*/
const firstLink = (node) => {
let link = node;
for (;;) {
const inner = A.isCallNode(link)
? link.callee
: A.isPropAccessNode(link)
? link.object
: undefined;
if (inner === undefined) return undefined;
if (A.isSymbolRefNode(inner)) return link;
link = inner;
}
};
/**
* `a == null ? void 0 : a.b.c()` as `a?.b.c()` from ECMAScript 2020 on,
* which tests `a` once and stops the whole chain on null or undefined.
* @param {Node} node a conditional
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the optional chain, or undefined
*/
const optionalChain = (node, compressor) => {
let { test, consequent, alternate } =
/** @type {Node & { consequent: Node }} */ (node);
if (!A.isBinaryNode(test)) return undefined;
if (test.operator === "!=") {
[consequent, alternate] = [alternate, consequent];
} else if (test.operator !== "==") {
return undefined;
}
const isNullish = (/** @type {Node} */ side) =>
A.isNullNode(side) ||
A.isUndefinedNode(side) ||
(A.isUnaryPrefixNode(side) &&
side.operator === "void" &&
A.isConstantNode(side.argument));
// terser writes a comparison's constant first.
const subject = test.right;
// `null == (a = b) ? void 0 : a.c` reads back what it stored: `(a = b)?.c`.
const name =
A.isAssignNode(subject) && subject.operator === "="
? subject.left
: subject;
if (
!isNullish(test.left) ||
!A.isSymbolRefNode(name) ||
!readsAlike(name, compressor) ||
!isNullish(/** @type {Node} */ (consequent)) ||
A.isNullNode(consequent) ||
compressor.option("ecma") < 2020
) {
return undefined;
}
const chain = A.isChainNode(alternate) ? alternate.expression : alternate;
const link = firstLink(chain);
if (
link === undefined ||
// A direct eval reads local bindings; an optional eval reads globals.
(A.isCallNode(link) && !link.optional && name.name === "eval") ||
!A.isEquivalent(A.isCallNode(link) ? link.callee : link.object, name)
) {
return undefined;
}
if (A.isCallNode(link)) {
link.callee = subject;
} else {
link.object = subject;
}
link.optional = true;
return makeNode(A.ChainNode, node, { expression: chain });
};
/**
* `null != a && a.b()` and `null == a || a.b()` as `a?.b()` where nothing
* reads the value, which the chain gives as `undefined` in place of a boolean,
* and `null != a && a.b` where only its truth is read.
* @param {Node} node a logical expression
* @param {Node} self the node visited, whose context the compressor knows
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the optional chain, or undefined
*/
const unreadOptionalChain = (node, self, compressor) => {
const { operator, left, right } = node;
const parent = compressor.parent();
if (
!A.isBinaryNode(left) ||
!(
(operator === "&&" && left.operator === "!=") ||
(operator === "||" && left.operator === "==")
) ||
!(
(A.isSimpleStatementNode(parent) && parent.expression === node) ||
(A.isSequenceNode(parent) && A.tailNode(parent) !== node) ||
// `false` and `undefined` are alike falsy, while `||` would read `true`.
(operator === "&&" &&
compressor.self() === self &&
compressor.in_boolean_context())
)
) {
return undefined;
}
const skipped = makeVoid0(node);
return optionalChain(
makeNode(A.ConditionalNode, node, {
test: left,
consequent: operator === "&&" ? right : skipped,
alternate: operator === "&&" ? skipped : right
}),
compressor
);
};
/**
* `null != a ? a : b` and `null == (a = c) ? b : a` as `a ?? b` and
* `(a = c) ?? b` from ECMAScript 2020 on, which terser writes only for `==`.
* @param {Node} node a conditional
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the nullish coalescing, or undefined
*/
const nullishCoalescing = (node, compressor) => {
let { test, consequent, alternate } =
/** @type {Node & { consequent: Node }} */ (node);
if (!A.isBinaryNode(test)) return undefined;
if (test.operator === "!=") {
[consequent, alternate] = [alternate, consequent];
} else if (test.operator !== "==") {
return undefined;
}
const nullish = test.left;
// terser writes a comparison's constant first.
const subject = test.right;
const name =
A.isAssignNode(subject) && subject.operator === "="
? subject.left
: subject;
if (
!(
A.isNullNode(nullish) ||
A.isUndefinedNode(nullish) ||
(A.isUnaryPrefixNode(nullish) &&
nullish.operator === "void" &&
A.isConstantNode(nullish.argument))
) ||
!A.isSymbolRefNode(name) ||
!A.isEquivalent(alternate, name) ||
!readsAlike(name, compressor) ||
compressor.option("ecma") < 2020
) {
return undefined;
}
return makeNode(A.BinaryNode, node, {
operator: "??",
left: subject,
right: /** @type {Node} */ (consequent)
});
};
/**
* `null == a && b` and `null != a || b`, their value unused, as `a ?? b` from
* ECMAScript 2020 on, and `null == a && (a = b)` on to `a ??= b` from 2021.
* @param {Node} node an expression whose value nothing reads
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the nullish coalescing, or undefined
*/
const coalesceGuard = (node, compressor) => {
if (!A.isBinaryNode(node)) return undefined;
const { operator, left: test, right } = node;
if (
!A.isBinaryNode(test) ||
test.operator !== (operator === "&&" ? "==" : "!=") ||
(operator !== "&&" && operator !== "||") ||
!(
A.isNullNode(test.left) ||
A.isUndefinedNode(test.left) ||
(A.isUnaryPrefixNode(test.left) &&
test.left.operator === "void" &&
A.isConstantNode(test.left.argument))
) ||
compressor.option("ecma") < 2020
) {
return undefined;
}
const coalesced = makeNode(A.BinaryNode, node, {
operator: "??",
left: test.right,
right
});
return logicalAssignment(coalesced, compressor) || coalesced;
};
/**
* Whether `this` is bound wherever the compressor stands, so reading it
* cannot throw: not in a class that extends another, where `super()` binds it.
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it is
*/
const thisIsBound = (compressor) => {
for (let level = 0, parent; (parent = compressor.parent(level)); level++) {
if (A.isClassNode(parent) && parent.superClass) return false;
if (A.isLambdaNode(parent) && !A.isArrowNode(parent)) {
const owner = compressor.parent(level + 2);
return !(owner && A.isClassNode(owner) && owner.superClass);
}
}
return true;
};
/**
* Whether finding an assignment's target before a condition runs reaches
* the place the condition would leave it at, and throws nothing earlier.
* @param {Node} node an assignment's target
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it does
*/
const resolvesAlike = (node, compressor) => {
if (A.isSymbolRefNode(node)) {
for (
let level = 0, parent;
(parent = compressor.parent(level));
level++
) {
if (A.isWithNode(parent)) return false;
}
// In strict code a global the condition creates is missed when found first.
return (
Boolean(node.definition) &&
(!node.definition.undeclared || !compressor.has_directive("use strict"))
);
}
return (
(A.isDotNode(node) || A.isSubNode(node)) &&
!node.optional &&
(A.isThisNode(node.object)
? thisIsBound(compressor)
: staysPut(node.object, compressor)) &&
(A.isDotNode(node) || A.isConstantNode(node.property))
);
};
/**
* `c ? a = x : a = y` as `a = c ? x : y`, where terser keeps it because
* `c` or the target may have side effects.
* @param {Node} node a conditional
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the assignment, or undefined
*/
const hoistAssignment = (node, compressor) => {
const { test, consequent, alternate } = node;
if (
!A.isAssignNode(consequent) ||
!A.isAssignNode(alternate) ||
/** @type {Node} */ (consequent).operator !== "=" ||
alternate.operator !== "=" ||
!A.isEquivalent(/** @type {Node} */ (consequent).left, alternate.left) ||
!resolvesAlike(/** @type {Node} */ (consequent).left, compressor)
) {
return undefined;
}
return makeNode(A.AssignNode, node, {
operator: "=",
logical: false,
left: /** @type {Node} */ (consequent).left,
right: makeNode(A.ConditionalNode, node, {
test,
consequent: /** @type {Node} */ (consequent).right,
alternate: alternate.right
})
});
};
/**
* Whether a node is the number literal `value`, never `-0` for `0`.
* @param {Node} node a node
* @param {number} value the number
* @returns {boolean} true when it is
*/
const isNumberValue = (node, value) =>
A.isNumberNode(node) && Object.is(node.value, value);
/**
* `c ? 1 : 0` as `+!!c` and `c ? 0 : 1` as `+!c`, where it prints shorter:
* `+` turns the boolean `c` converts to into the same `1` or `0`.
* @param {Node} node a conditional
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the number conversion, or undefined
*/
const numericConditional = (node, compressor) => {
const { test, consequent, alternate } = node;
let condition;
if (
isNumberValue(/** @type {Node} */ (consequent), 1) &&
isNumberValue(alternate, 0)
) {
condition = test;
} else if (
isNumberValue(/** @type {Node} */ (consequent), 0) &&
isNumberValue(alternate, 1)
) {
condition = A.negate(test, compressor);
} else {
return undefined;
}
const converted = makeNode(A.UnaryPrefixNode, node, {
operator: "+",
argument: A.isBoolean(condition)
? condition
: makeNode(A.UnaryPrefixNode, node, {
operator: "!",
argument: A.negate(condition, compressor)
})
});
const parent = compressor.parent();
// Printed in parentheses where it sits, which `+` needs none of.
const parenthesized =
(A.isBinaryNode(parent) &&
!A.isAssignNode(parent) &&
!A.isDefaultAssignNode(parent)) ||
A.isUnaryNode(parent) ||
(A.isPropAccessNode(parent) && parent.object === node) ||
(A.isCallNode(parent) && parent.callee === node) ||
(A.isConditionalNode(parent) && parent.test === node);
// `a + +b` keeps a space apart from `a ++b`.
const spaced =
(A.isBinaryNode(parent) &&
parent.operator === "+" &&
parent.right === node) ||
(A.isUnaryPrefixNode(parent) && parent.operator === "+");
return modules.printToString(converted).length + (spaced ? 1 : 0) <
modules.printToString(node).length + (parenthesized ? 2 : 0)
? converted
: undefined;
};
const { PRECEDENCE } = /** @type {{ PRECEDENCE: Record<string, number> }} */ (
modules.parse
);
/**
* Whether an expression prints in parentheses as the test of a conditional.
* @param {Node} node the expression
* @returns {boolean} true when it does
*/
const parenthesizedAsTest = (node) =>
A.isConditionalNode(node) ||
A.isAssignNode(node) ||
A.isSequenceNode(node) ||
A.isArrowNode(node) ||
A.isYieldNode(node);
/**
* Whether an expression prints in parentheses as the operand of `!`.
* @param {Node} node the expression
* @returns {boolean} true when it does
*/
const parenthesizedAsOperand = (node) =>
parenthesizedAsTest(node) ||
A.isBinaryNode(node) ||
A.isPrivateInNode(node);
/**
* Whether an expression prints in parentheses as an operand of a binary
* operator other than an assignment.
* @param {Node} node the operand
* @param {string} operator the operator
* @param {boolean} right whether it is the right operand
* @returns {boolean} true when it does
*/
const parenthesizedInBinary = (node, operator, right) => {
if (parenthesizedAsTest(node)) return true;
if (!A.isBinaryNode(node)) return false;
// `??` mixed with `||` or `&&` is a syntax error without parens.
if (
(node.operator === "??" && (operator === "||" || operator === "&&")) ||
(operator === "??" && (node.operator === "||" || node.operator === "&&"))
) {
return true;
}
const precedence = PRECEDENCE[node.operator];
return (
PRECEDENCE[operator] > precedence ||
(PRECEDENCE[operator] === precedence && right)
);
};
/**
* `!!(a) && b` as `a ? b : !1` and `!(a) && b` as `a ? !1 : b`, `||` alike,
* where it prints shorter: both convert `a`, which `!` parenthesizes, once.
* @param {Node} node a logical expression
* @param {Node} self the node optimized into it
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the conditional, or undefined
*/
const booleanConditional = (node, self, compressor) => {
const { operator, left, right } = node;
if (
(operator !== "&&" && operator !== "||") ||
!A.isUnaryPrefixNode(left) ||
left.operator !== "!" ||
// The context read is the visited node's, which a node built since is not.
compressor.self() !== self
) {
return undefined;
}
const double =
A.isUnaryPrefixNode(left.argument) && left.argument.operator === "!";
/** @type {Node} */
let test = double ? left.argument.argument : left.argument;
if (!parenthesizedAsOperand(test) || compressor.in_boolean_context()) {
return undefined;
}
const parent = compressor.parent();
const logicalCost =
(double ? 4 : 3) + (parenthesizedInBinary(right, operator, true) ? 2 : 0);
let conditionalCost = A.isSequenceNode(right) ? 4 : 2;
// Any binary operator or a conditional's test parenthesizes the
// conditional, not always the logical.
if (
(A.isBinaryNode(parent) &&
!A.isAssignNode(parent) &&
!A.isDefaultAssignNode(parent) &&
!parenthesizedInBinary(node, parent.operator, parent.right === self)) ||
(A.isConditionalNode(parent) && parent.test === self)
) {
conditionalCost += 2;
}
// `!a` decides `&&` and `!!a` decides `||` where `a` is truthy.
let decidedByTruthy = double === (operator === "||");
let text = "";
// `!(!a || !b)` tests `a && b` with the branches swapped.
if (
A.isBinaryNode(test) &&
(test.operator === "&&" || test.operator === "||")
) {
text = modules.printToString(test);
const negated = A.negate(test, compressor);
const negatedText = modules.printToString(negated);
if (negatedText.length < text.length) {
conditionalCost += negatedText.length - text.length;
test = negated;
text = negatedText;
decidedByTruthy = !decidedByTruthy;
}
}
if (parenthesizedAsTest(test)) {
conditionalCost += 2;
} else if (
A.isExitNode(parent) ||
A.isYieldNode(parent) ||
A.isCaseNode(parent)
) {
// A keyword before the test keeps a space apart from a name it opens with.
if (text === "") text = modules.printToString(test);
if (/^[\w$\\\u0080-\uFFFF]/.test(text)) conditionalCost++;
}
if (conditionalCost >= logicalCost) return undefined;
const literal = makeNode(A.UnaryPrefixNode, node, {
operator: "!",
argument: makeNode(A.NumberNode, node, {
value: operator === "||" ? 0 : 1
})
});
return makeNode(A.ConditionalNode, node, {
test,
consequent: decidedByTruthy ? literal : right,
alternate: decidedByTruthy ? right : literal
});
};
/**
* `(a = b) === null || a === void 0` as `(a = b) == null`, and `!==` with
* `&&` alike, where `a` holds what the first comparison stored.
* @param {Node} node a logical expression
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the loose comparison, or undefined
*/
const looseNullish = (node, compressor) => {
const { operator } = node;
if (operator !== "||" && operator !== "&&") return undefined;
const strict = operator === "||" ? "===" : "!==";
const chained =
A.isBinaryNode(node.left) && node.left.operator === operator;
const first = chained ? node.left.right : node.left;
const second = node.right;
if (
!A.isBinaryNode(first) ||
!A.isBinaryNode(second) ||
first.operator !== strict ||
second.operator !== strict
) {
return undefined;
}
const isVoid = (/** @type {Node} */ side) =>
A.isUndefinedNode(side) ||
(A.isUnaryPrefixNode(side) &&
side.operator === "void" &&
A.isConstantNode(side.argument));
// terser writes a comparison's constant first.
const store = first.right;
if (
!(A.isNullNode(first.left)
? isVoid(second.left)
: isVoid(first.left) && A.isNullNode(second.left)) ||
!A.isAssignNode(store) ||
store.operator !== "=" ||
!A.isSymbolRefNode(store.left) ||
!readsAlike(store.left, compressor) ||
!A.isEquivalent(store.left, second.right)
) {
return undefined;
}
const loose = makeNode(A.BinaryNode, node, {
operator: strict.slice(0, -1),
left: makeNode(A.NullNode, node),
right: store
});
return chained
? makeNode(A.BinaryNode, node, {
operator,
left: node.left.left,
right: loose
})
: loose;
};
/**
* Which links of a tree are optional, in walk order: `isEquivalent` reads
* `a?.b.c` and `a.b?.c` as one, as terser's `equivalent_to` does.
* @param {Node} node a tree
* @returns {string} one `?` or `.` per call or property access
*/
const optionalLinks = (node) => {
let links = "";
walkNode(
node,
A.createWalker((/** @type {Node} */ child) => {
if (A.isCallNode(child) || A.isPropAccessNode(child)) {
links += child.optional ? "?" : ".";
}
})
);
return links;
};
/**
* How many expressions two sequences end in alike, as swc's
* `compress_similar_cons_alt` counts them.
* @param {Node} consequent a sequence
* @param {Node} alternate a sequence
* @returns {number} the length of the shared tail, short of either whole
*/
const sharedTailLength = (consequent, alternate) => {
const first = consequent.expressions;
const second = alternate.expressions;
const limit = Math.min(first.length, second.length);
let length = 0;
while (length < limit) {
const left = first[first.length - 1 - length];
const right = second[second.length - 1 - length];
if (
!A.isEquivalent(left, right) ||
optionalLinks(left) !== optionalLinks(right)
) {
break;
}
length++;
}
return length === first.length && length === second.length ? 0 : length;
};
// What terser does for `x ? x : y` and `a ? b : (c, b)`, mirrored.
A.wrapOptimizer(
"Conditional",
(optimizeConditional) => (self, compressor) => {
const optimized = optimizeConditional(self, compressor);
if (!improvements.enabled || !A.isConditionalNode(optimized)) {
return optimized;
}
const { test, consequent, alternate } = optimized;
// x ? y : x -> x && y
if (
A.isSymbolRefNode(test) &&
A.isSymbolRefNode(alternate) &&
test.definition === alternate.definition &&
// A global may be a getter, answering each read differently.
readsAlike(test, compressor)
) {
return makeNode(A.BinaryNode, optimized, {
operator: "&&",
left: test,
right: /** @type {Node} */ (consequent)
});
}
// a ? (c, b) : b -> (a && c), b
if (
A.isSequenceNode(consequent) &&
A.isEquivalent(
alternate,
/** @type {Node} */ (consequent).expressions[
/** @type {Node} */ (consequent).expressions.length - 1
]
) &&
optionalLinks(alternate) ===
optionalLinks(
/** @type {Node} */ (consequent).expressions[
/** @type {Node} */ (consequent).expressions.length - 1
]
)
) {
return A.optimize(
makeSequence(optimized, [
makeNode(A.BinaryNode, optimized, {
operator: "&&",
left: test,
right: makeSequence(
optimized,
/** @type {Node} */ (consequent).expressions.slice(0, -1)
)
}),
alternate
]),
compressor
);
}
// a ? (c, b) : (d, b) -> (a ? c : d), b
const shared =
A.isSequenceNode(consequent) && A.isSequenceNode(alternate)
? sharedTailLength(/** @type {Node} */ (consequent), alternate)
: 0;
if (shared > 0) {
const consequentHead = /** @type {Node} */ (
consequent
).expressions.slice(0, -shared);
const alternateHead = alternate.expressions.slice(0, -shared);
const head =
consequentHead.length === 0
? makeNode(A.BinaryNode, optimized, {
operator: "||",
left: test,
right: makeSequence(optimized, alternateHead)
})
: alternateHead.length === 0
? makeNode(A.BinaryNode, optimized, {
operator: "&&",
left: test,
right: makeSequence(optimized, consequentHead)
})
: makeNode(A.ConditionalNode, optimized, {
test,
consequent: makeSequence(optimized, consequentHead),
alternate: makeSequence(optimized, alternateHead)
});
return A.optimize(
makeSequence(optimized, [
head,
...alternate.expressions.slice(-shared)
]),
compressor
);
}
// a ? A : (s, b ? X : A) -> a || (s, !b) ? A : X
const inner = A.isSequenceNode(alternate)
? alternate.expressions[alternate.expressions.length - 1]
: alternate;
if (
A.isConditionalNode(inner) &&
A.isEquivalent(/** @type {Node} */ (consequent), inner.alternate) &&
optionalLinks(/** @type {Node} */ (consequent)) ===
optionalLinks(inner.alternate)
) {
const innerTest = A.negate(inner.test, compressor);
const merged = makeNode(A.ConditionalNode, optimized, {
test: makeNode(A.BinaryNode, optimized, {
operator: "||",
left: test,
right:
inner === alternate
? innerTest
: makeSequence(optimized, [
...alternate.expressions.slice(0, -1),
innerTest
])
}),
consequent,
alternate: /** @type {Node} */ (inner.consequent)
});
if (mangledLength(merged) < mangledLength(optimized)) {
return A.optimize(merged, compressor);
}
}
return optimized;
}
);
A.wrapOptimizer(
"Conditional",
(optimizeConditional) => (self, compressor) => {
const optimized = optimizeConditional(self, compressor);
return (
(improvements.enabled &&
A.isConditionalNode(optimized) &&
(optionalChain(optimized, compressor) ||
nullishCoalescing(optimized, compressor) ||
hoistAssignment(optimized, compressor) ||
numericConditional(optimized, compressor))) ||
optimized
);
}
);
/**
* @param {Node} node an expression
* @returns {boolean} whether it makes a new object each time it runs
*/
const isFreshObject = (node) =>
A.isObjectNode(node) ||
A.isArrayNode(node) ||
A.isFunctionNode(node) ||
A.isArrowNode(node) ||
A.isClassExpressionNode(node) ||
A.isRegExpNode(node);
/**
* `{} === 1` as `false`: a new object equals no primitive nor another new
* object, and `==` only `null` or `undefined` besides those of its kind.
* @param {Node} node a comparison terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the boolean it is, or undefined
*/
const freshComparison = (node, compressor) => {
const { operator, left, right } = node;
const strict = operator === "===" || operator === "!==";
if (!strict && operator !== "==" && operator !== "!=") return undefined;
/**
* @param {Node} other the other side of a new object
* @returns {boolean} whether the two never compare equal
*/
const neverEquals = (other) =>
isFreshObject(other) ||
(strict
? isPrimitiveConstant(other) || A.isBigIntNode(other)
: A.isNullNode(other) ||
A.isUndefinedNode(other) ||
(A.isUnaryPrefixNode(other) &&
other.operator === "void" &&
isPrimitiveConstant(other.argument)));
if (
!(
(isFreshObject(left) && neverEquals(right)) ||
(isFreshObject(right) && neverEquals(left))
) ||
A.hasSideEffects(left, compressor) ||
A.hasSideEffects(right, compressor)
) {
return undefined;
}
return A.optimize(
makeNodeFromConstant(operator === "!==" || operator === "!=", node),
compressor
);
};
/**
* `"a" + x` read for its truthiness as `(x, true)` when conversions are assumed
* pure, unless `x` is an object the program makes.
* @param {Node} node a binary expression
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the replacement, or undefined
*/
const truthyConcatenation = (node, compressor) => {
if (
node.operator !== "+" ||
!compressor.in_boolean_context() ||
!compressor.option("pure_conversions")
) {
return undefined;
}
for (const [side, other] of [
[node.left, node.right],
[node.right, node.left]
]) {
const value = A.evaluate(side, compressor);
const known = A.isSymbolRefNode(other) ? A.fixedValue(other) : other;
if (
value &&
typeof value === "string" &&
!A.hasSideEffects(side, compressor) &&
// An object the program writes may convert itself by code of its own,
// and a script's top-level variable may hold one unseen.
!(known && (A.isObjectNode(known) || A.isClassNode(known))) &&
!(
A.isSymbolRefNode(other) &&
other.definition &&
other.definition.global &&
!isUndeclaredRef(other)
)
) {
return A.optimize(
makeSequence(node, [other, makeNode(A.TrueNode, node)]),
compressor
);
}
}
return undefined;
};
/**
* `a && (b && c)` as `(a && b) && c`, which prints without parentheses: `&&`
* and `||` each associate, and run their operands in the same order.
* @param {Node} node a binary expression
* @returns {Node | undefined} the rotated expression, or undefined
*/
const leftAssociated = (node) => {
const { operator, right } = node;
if (
(operator !== "&&" && operator !== "||") ||
!A.isBinaryNode(right) ||
A.isAssignNode(right) ||
right.operator !== operator
) {
return undefined;
}
return makeNode(A.BinaryNode, node, {
operator,
left: makeNode(A.BinaryNode, node, {
operator,
left: node.left,
right: right.left
}),
right: right.right
});
};
/**
* A chain of one `&&` or `||` associated to the left all along.
* @param {Node} node a binary expression
* @returns {Node} the chain, or the node where it holds none to move
*/
const associateLeft = (node) => {
let rotated = node;
for (let next; (next = leftAssociated(rotated));) rotated = next;
if (rotated !== node) rotated.left = associateLeft(rotated.left);
return rotated;
};
const EQUALITY_OPERATORS = new Set(["==", "!=", "===", "!=="]);
/**
* Whether an equality reads `null`, `void 0` or a number before an operand
* that is none: measured, `x===null` gzips better than `null===x`.
* @param {Node} node a binary
* @returns {boolean} true where the literal can go right
*/
const literalOnLeft = (node) => {
/**
* @param {Node} operand an operand
* @returns {boolean} whether it is `null`, `void 0` or a number
*/
const isLiteral = (operand) =>
A.isNullNode(operand) ||
A.isNumberNode(operand) ||
(A.isUnaryPrefixNode(operand) &&
operand.operator === "void" &&
A.isNumberNode(operand.argument));
// Measured: `typeof` and strings gzip better where terser puts them.
return (
improvements.literalsLast.enabled &&
EQUALITY_OPERATORS.has(node.operator) &&
isLiteral(node.left) &&
!isLiteral(node.right) &&
!A.isStringNode(node.right) &&
!(A.isUnaryPrefixNode(node.right) && node.right.operator === "typeof")
);
};
// Run on the finished tree: `if`s and conditionals become `&&` and `||`
// chains after their operands were optimized.
/**
* Arithmetic on two literals folded where that writes less: the last pass
* may inline a constant into its reads after terser's `evaluate` ran.
* @param {Node} node a binary expression of the finished tree
* @returns {Node | undefined} the literal it computes, if shorter
*/
const foldConstantArithmetic = (node) => {
const operate = ARITHMETIC_OPERATIONS[node.operator];
if (operate === undefined) return undefined;
const { left, right } = node;
// A name such as `NaN` may be a binding a pattern declares.
if (
!(A.isStringNode(left) || A.isNumberNode(left)) ||
!(A.isStringNode(right) || A.isNumberNode(right))
) {
return undefined;
}
const value = operate(left.value, right.value);
// `NaN` and `Infinity` print as names, which a binding may shadow.
if (typeof value === "number" && !Number.isFinite(value)) return undefined;
return literalIfShorter(value, node);
};
const compressBeforeAssociating = compressorMethods.compress;
/**
* @this {Compressor} the compressor
* @param {Node} toplevel the tree
* @returns {Node} the tree compressed
*/
compressorMethods.compress = function compress(toplevel) {
const compressed = compressBeforeAssociating.call(this, toplevel);
if (!improvements.enabled) return compressed;
return A.transformNode(
compressed,
A.createTransformer(undefined, (/** @type {Node} */ node) => {
if (!A.isBinaryNode(node)) return undefined;
const folded = this.option("evaluate")
? foldConstantArithmetic(node)
: undefined;
if (folded !== undefined) return folded;
const associated = associateLeft(node);
if (associated !== node) return associated;
if (literalOnLeft(node)) {
const { left } = node;
node.left = node.right;
node.right = left;
}
return undefined;
})
);
};
A.wrapOptimizer("Binary", (optimizeBinary) => (self, compressor) => {
const optimized = optimizeBinary(self, compressor);
return (
(improvements.enabled &&
A.isBinaryNode(optimized) &&
truthyConcatenation(optimized, compressor)) ||
(improvements.enabled &&
A.isBinaryNode(optimized) &&
(looseNullish(optimized, compressor) ||
freshComparison(optimized, compressor))) ||
optimized
);
});
/**
* `!a || b` as `a && b` and `!a && b` as `a || b`, where nobody reads the value.
* @param {Node} node an expression whose value is unused
* @returns {Node} it with the negation dropped, or itself
*/
const withoutNegation = (node) =>
A.isBinaryNode(node) &&
(node.operator === "||" || node.operator === "&&") &&
A.isUnaryPrefixNode(node.left) &&
node.left.operator === "!"
? makeNode(A.BinaryNode, node, {
operator: node.operator === "||" ? "&&" : "||",
left: node.left.argument,
right: node.right
})
: node;
A.wrapOptimizer(
"SimpleStatement",
(optimizeStatement) => (self, compressor) => {
const optimized = optimizeStatement(self, compressor);
if (!improvements.enabled || !A.isSimpleStatementNode(optimized)) {
return optimized;
}
const { expression } = optimized;
if (A.isSequenceNode(expression)) {
const expressions = expression.expressions.map(withoutNegation);
return expressions.every(
(node, i) => node === expression.expressions[i]
)
? optimized
: makeNode(A.SimpleStatementNode, optimized, {
expression: makeNode(A.SequenceNode, expression, { expressions })
});
}
const replaced = withoutNegation(expression);
return replaced === expression
? optimized
: makeNode(A.SimpleStatementNode, optimized, { expression: replaced });
}
);
A.wrapOptimizer("Sequence", (optimizeSequence) => (self, compressor) => {
const optimized = optimizeSequence(self, compressor);
if (!improvements.enabled || !A.isSequenceNode(optimized)) return optimized;
const { expressions } = optimized;
const last = expressions[expressions.length - 1];
const previous = expressions[expressions.length - 2];
// a ||= b, a -> a ||= b: the assignment's value is what `a` then holds.
if (
A.isSymbolRefNode(last) &&
A.isAssignNode(previous) &&
previous.logical &&
A.isSymbolRefNode(previous.left) &&
isRewritable(last, compressor) &&
A.isEquivalent(previous.left, last)
) {
expressions.pop();
if (expressions.length === 1) return expressions[0];
}
// a = b, a(c) -> (a = b)(c): the call reads `a` right after the write and
// passes no `this` either way.
if (
A.isCallNode(last) &&
!A.isNewNode(last) &&
!last.optional &&
A.isSymbolRefNode(last.callee) &&
A.isAssignNode(previous) &&
previous.operator === "=" &&
!previous.logical &&
A.isSymbolRefNode(previous.left) &&
readsAlike(last.callee, compressor) &&
A.isEquivalent(previous.left, last.callee)
) {
last.callee = previous;
expressions.splice(-2, 1);
if (expressions.length === 1) return expressions[0];
}
return optimized;
});
/**
* The value a literal stands for where it is coerced: what `constantOf`
* answers, a bigint, or an array literal of those as the string it joins to.
* @param {Node} node an expression
* @returns {EvaluatedValue} its value, or `NOT_CONSTANT`
*/
const coercedValueOf = (node) => {
if (A.isBigIntNode(node)) return BigInt(/** @type {string} */ (node.value));
if (!A.isArrayNode(node)) return constantOf(node);
// A patched `Array.prototype` may read or convert the array otherwise.
if (patchedPrototypes.has("Array")) return NOT_CONSTANT;
/** @type {EvaluatedArray} */
const values = [];
values.length = node.elements.length;
for (let i = 0; i < node.elements.length; i++) {
const element = node.elements[i];
if (element === null) continue;
const value = A.isArrayNode(element)
? NOT_CONSTANT
: coercedValueOf(element);
if (value === NOT_CONSTANT) return NOT_CONSTANT;
values[i] = value;
}
return values.join();
};
/**
* The shorter of an expression and a literal of the value it folds to.
* @param {EvaluatedValue} value the value
* @param {Node} node the expression
* @returns {Node | undefined} the literal, or undefined where it is longer
*/
const literalIfShorter = (value, node) => {
const literal = makeNodeFromConstant(value, node);
return modules.printToString(literal).length <=
modules.printToString(node).length
? literal
: undefined;
};
/**
* Whether an expression always yields a number, never a BigInt: `+x` and
* `x >>> y` throw on one, and arithmetic mixing one with a number throws.
* @param {Node} node an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it does
*/
const isNumberAlone = (node, compressor) =>
(A.isUnaryPrefixNode(node) && node.operator === "+") ||
(A.isBinaryNode(node) && node.operator === ">>>") ||
A.isNumber(node, compressor);
/**
* Whether an expression yields a 32-bit integer, so it is falsy exactly
* where it is zero, never `NaN`.
* @param {Node} node an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it does
*/
const isInt32 = (node, compressor) => {
if (A.isUnaryPrefixNode(node)) {
return node.operator === "~" && isNumberAlone(node.argument, compressor);
}
if (!A.isBinaryNode(node)) return false;
if (node.operator === ">>>") return true;
return (
BITWISE_OPERATORS.has(node.operator) &&
(isNumberAlone(node.left, compressor) ||
isNumberAlone(node.right, compressor))
);
};
/**
* What an expression read only as a test is written as: an integer
* compared with zero as the integer, or its negation.
* @param {Node} node an expression read as a test
* @param {Compressor} compressor the compressor
* @returns {Node} what tests the same
*/
const testOf = (node, compressor) => {
if (A.isConditionalNode(node)) {
const consequent = testOf(
/** @type {Node} */ (node.consequent),
compressor
);
const alternate = testOf(node.alternate, compressor);
return consequent === node.consequent && alternate === node.alternate
? node
: makeNode(A.ConditionalNode, node, {
test: node.test,
consequent,
alternate
});
}
if (!A.isBinaryNode(node) || !(node.operator in NEGATED_EQUALITY)) {
return node;
}
const isZero = (/** @type {Node} */ side) =>
A.isNumberNode(side) && side.value === 0;
const integer = isZero(node.left)
? node.right
: isZero(node.right)
? node.left
: undefined;
if (!integer || !isInt32(integer, compressor)) return node;
return node.operator[0] === "!"
? integer
: makeNode(A.UnaryPrefixNode, node, { operator: "!", argument: integer });
};
/**
* Whether an expression is never `null` or `undefined`, which leaves a
* `??` after it nothing to choose.
* @param {Node} node an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it never is
*/
const isNotNullish = (node, compressor) =>
// Every operator but these yields a boolean, a number, a bigint or a string.
(A.isUnaryPrefixNode(node) && node.operator !== "void") ||
A.isUnaryPostfixNode(node) ||
(A.isBinaryNode(node) &&
!A.isAssignNode(node) &&
node.operator !== "&&" &&
node.operator !== "||" &&
node.operator !== "??") ||
A.isBoolean(node) ||
A.isNumber(node, compressor) ||
A.isString(node, compressor) ||
A.isBigIntNode(node) ||
A.isArrayNode(node) ||
A.isObjectNode(node) ||
A.isLambdaNode(node) ||
A.isClassNode(node) ||
A.isRegExpNode(node);
/**
* An equality of `null` or `undefined` with a value never either, such as a
* binding fixed to an object literal, as the boolean it answers.
* @param {Node} node a binary expression
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the boolean, or undefined
*/
const nullishComparison = (node, compressor) => {
const { operator } = node;
if (!NEGATED_EQUALITY[operator]) return undefined;
/**
* @param {Node} side a side of the comparison
* @returns {boolean} whether it is `null` or `undefined` with no effect
*/
const nullish = (side) =>
A.isNullNode(side) ||
A.isUndefinedNode(side) ||
(A.isUnaryPrefixNode(side) &&
side.operator === "void" &&
!A.hasSideEffects(side.argument, compressor));
/**
* @param {Node} side a side of the comparison
* @returns {boolean} whether it is never `null` nor `undefined`, read with no effect
*/
const never = (side) => {
if (A.isSymbolRefNode(side)) {
const fixed = A.fixedValue(side);
return (
A.isSyntaxNode(fixed) &&
(A.isObjectNode(fixed) ||
A.isArrayNode(fixed) ||
A.isLambdaNode(fixed) ||
A.isClassNode(fixed))
);
}
return (
isNotNullish(side, compressor) && !A.hasSideEffects(side, compressor)
);
};
if (
!(nullish(node.left) && never(node.right)) &&
!(nullish(node.right) && never(node.left))
) {
return undefined;
}
return makeNodeFromConstant(operator[0] === "!", node);
};
/**
* A binary expression written shorter: an array literal or a bigint
* concatenated folded, `??` after a value never nullish dropped, an integer
* tested against zero read as the integer, and `===` between numbers loose.
* @param {Node} node a binary expression terser has optimized
* @param {Node} self the expression as it was visited
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} what replaces it
*/
const coerceBinary = (node, self, compressor) => {
const { operator, left, right } = node;
if (operator === "+") {
const coerced =
A.isArrayNode(left) ||
A.isArrayNode(right) ||
A.isBigIntNode(left) ||
A.isBigIntNode(right);
if (!coerced) return undefined;
const leftValue = coercedValueOf(left);
const rightValue = coercedValueOf(right);
if (leftValue !== NOT_CONSTANT && rightValue !== NOT_CONSTANT) {
/** @type {EvaluatedValue} */
let value;
try {
value =
/** @type {string} */ (leftValue) +
/** @type {string} */ (rightValue);
} catch (_err) {
return undefined;
}
return literalIfShorter(value, node);
}
// An empty array joins to "", which a string after it reads as nothing.
if (leftValue === "" && A.isString(right, compressor)) return right;
if (rightValue === "" && A.isString(left, compressor)) return left;
return undefined;
}
if (operator === "??") {
return isNotNullish(left, compressor) ? left : undefined;
}
if (!(operator in NEGATED_EQUALITY)) return undefined;
// The context read is the visited node's, which a node built since is not.
if (compressor.self() === self && compressor.in_boolean_context()) {
const test = testOf(node, compressor);
if (test !== node) return test;
}
return (operator === "===" || operator === "!==") &&
isNumberAlone(left, compressor) &&
isNumberAlone(right, compressor)
? makeNode(A.BinaryNode, node, {
operator: operator.slice(0, -1),
left,
right
})
: undefined;
};
/**
* A prefix operation written shorter: `+` of an array literal folded, `!`
* of an equality as the opposite equality, and `!!` of a boolean dropped.
* @param {Node} node a prefix operation terser has optimized
* @returns {Node | undefined} what replaces it
*/
const coerceUnary = (node) => {
const { operator, argument } = node;
if (operator === "+" && A.isArrayNode(argument)) {
const value = coercedValueOf(argument);
return typeof value === "string"
? literalIfShorter(Number(value), node)
: undefined;
}
if (operator !== "!") return undefined;
if (A.isBinaryNode(argument) && argument.operator in NEGATED_EQUALITY) {
return makeNode(A.BinaryNode, argument, {
operator: NEGATED_EQUALITY[argument.operator],
left: argument.left,
right: argument.right
});
}
if (
A.isBinaryNode(argument) &&
(argument.operator === "&&" || argument.operator === "||") &&
isNegation(argument.left) &&
isNegation(argument.right)
) {
// De Morgan: !(!a || !b) tests what a && b does, one `!` shorter.
return makeNode(A.UnaryPrefixNode, node, {
operator: "!",
argument: makeNode(A.UnaryPrefixNode, node, {
operator: "!",
argument: makeNode(A.BinaryNode, argument, {
operator: argument.operator === "&&" ? "||" : "&&",
left: argument.left.argument,
right: argument.right.argument
})
})
});
}
return isNegation(argument) && A.isBoolean(argument.argument)
? argument.argument
: undefined;
};
/**
* @param {Node} node an expression
* @returns {boolean} whether it is a `!` of another
*/
const isNegation = (node) =>
A.isUnaryPrefixNode(node) && node.operator === "!";
/**
* A call coercing a value written shorter: `Boolean(x)` as `!!x`,
* `Number`, `String` and `BigInt` of a literal and `toString` of a boolean
* or string folded, the globals taken as the language defines them.
* @param {Node} call a call terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} what replaces it
*/
const coerceCall = (call, compressor) => {
const { callee } = call;
const args = call.arguments;
if (A.isDotNode(callee)) {
if (callee.optional || callee.property.name !== "toString") {
return undefined;
}
const value = constantOf(callee.object);
return args.length === 0 &&
(typeof value === "boolean" || typeof value === "string")
? literalIfShorter(String(value), call)
: undefined;
}
if (
!A.isSymbolRefNode(callee) ||
!COERCIONS.has(callee.name) ||
args.length > 1 ||
(args.length === 1 && A.isExpansionNode(args[0])) ||
!isUndeclaredRef(callee) ||
isReboundGlobal(callee.name) ||
globalMayBeRebound(compressor)
) {
return undefined;
}
if (callee.name === "Boolean") {
if (args.length === 0) return makeNodeFromConstant(false, call);
const test = testOf(args[0], compressor);
if (A.isBoolean(test)) return test;
return makeNode(A.UnaryPrefixNode, call, {
operator: "!",
argument: makeNode(A.UnaryPrefixNode, call, {
operator: "!",
argument: test
})
});
}
const value = args.length === 0 ? undefined : coercedValueOf(args[0]);
if (
value === NOT_CONSTANT ||
(args.length === 0 && callee.name === "BigInt")
) {
return undefined;
}
/** @type {EvaluatedValue} */
let result;
try {
result = /** @type {BuiltInTable} */ (
/** @type {typeof globalThis | BuiltInTable} */ (global)
)[callee.name](...(args.length === 0 ? [] : [value]));
} catch (_err) {
return undefined;
}
return literalIfShorter(result, call);
};
A.wrapOptimizer("Call", (optimizeCall) => (self, compressor) => {
const optimized = optimizeCall(self, compressor);
if (
!improvements.enabled ||
!A.isCallNode(optimized) ||
A.isNewNode(optimized) ||
optimized.optional
) {
return optimized;
}
return coerceCall(optimized, compressor) || optimized;
});
/**
* Whether an expression is `undefined` and reads nothing on the way.
* @param {Node} node an expression
* @returns {boolean} true for `void 0` and an undeclared-free `undefined`
*/
const isPlainUndefined = (node) =>
A.isUndefinedNode(node) ||
(A.isUnaryPrefixNode(node) &&
node.operator === "void" &&
A.isNumberNode(node.argument));
/**
* A list's last statement where falling off it ends the function, returning
* `undefined`: `return void x` as `x`, `return c ? y : void 0` as `if (c) return y`.
* @param {Node[]} statements the statements, changed in place
* @param {boolean} concise whether they are an arrow's whole body
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const shortenTail = (statements, concise, compressor) => {
const last = statements[statements.length - 1];
if (!last) return;
if (A.isBlockStatementNode(last)) {
shortenTail(/** @type {Node[]} */ (last.body), false, compressor);
return;
}
if (A.isIfNode(last)) {
for (const branch of /** @type {("consequent" | "alternate")[]} */ ([
"consequent",
"alternate"
])) {
const statement = /** @type {Node} */ (last[branch]);
if (!statement) continue;
const list = [statement];
shortenTail(list, false, compressor);
last[branch] = list[0];
}
return;
}
if (!A.isReturnNode(last) || !last.argument) return;
const value = last.argument;
const previous = statements[statements.length - 2];
// `if (c) return; return x` falls off where `c` holds.
if (
A.isIfNode(previous) &&
!previous.alternate &&
A.isReturnNode(previous.consequent) &&
!(/** @type {Node} */ (previous.consequent).argument)
) {
statements.splice(
-2,
2,
makeNode(A.IfNode, previous, {
test: A.negate(previous.test, compressor),
consequent: last,
alternate: null
})
);
return;
}
// terser's `return` already drops a `void` of what has no effects.
if (A.isUnaryPrefixNode(value) && value.operator === "void") {
statements[statements.length - 1] = makeNode(
A.SimpleStatementNode,
last,
{ expression: value.argument }
);
return;
}
if (A.isSequenceNode(value)) {
const tail = value.expressions[value.expressions.length - 1];
if (!A.isUnaryPrefixNode(tail) || tail.operator !== "void") return;
statements[statements.length - 1] = makeNode(
A.SimpleStatementNode,
last,
{
expression: makeSequence(value, [
...value.expressions.slice(0, -1),
tail.argument
])
}
);
return;
}
if (concise || !A.isConditionalNode(value)) return;
const whenTrue = isPlainUndefined(value.alternate);
if (
!whenTrue &&
!isPlainUndefined(/** @type {Node} */ (value.consequent))
) {
return;
}
statements[statements.length - 1] = makeNode(A.IfNode, last, {
test: whenTrue ? value.test : A.negate(value.test, compressor),
consequent: makeNode(A.ReturnNode, last, {
argument: /** @type {Node} */ (
whenTrue ? value.consequent : value.alternate
)
}),
alternate: null
});
};
/**
* @param {Node} node a node
* @param {string} name a name
* @returns {boolean} whether it is a reference to the name
*/
const isRefTo = (node, name) => A.isSymbolRefNode(node) && node.name === name;
/**
* How many symbols of a function carry each name asked about, its own
* `arguments` only outside the functions in it that have their own.
* @param {Node} lambda a function that is not an arrow
* @param {Map<string, number>} counts the names, counted into in place
* @returns {void}
*/
const countNames = (lambda, counts) => {
/**
* @param {boolean} ownArguments whether `arguments` there is the function's
* @returns {Walker} a walk counting the names
*/
const counter = (ownArguments) =>
A.createWalker((/** @type {Node} */ node) => {
if (A.isLambdaNode(node) && node !== lambda && !A.isArrowNode(node)) {
if (ownArguments) {
walkNode(node, counter(false));
return true;
}
return;
}
if (
A.isSymbolNode(node) &&
counts.has(node.name) &&
(ownArguments || node.name !== "arguments")
) {
counts.set(
node.name,
/** @type {number} */ (counts.get(node.name)) + 1
);
}
});
walkNode(lambda, counter(true));
};
/**
* The `for` a compiler writes for a rest parameter, copying `arguments`
* from `k` on into a `var`, as that `var` holding `[...arguments].slice(k)`;
* a `k` above 0 calls `Array.prototype.slice`, so it needs `unsafe`.
* @param {Node} lambda a function that is not an arrow
* @param {Node} loop one of its statements
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the `var` in its place, if it is such a copy
*/
const restCopyAsSpread = (lambda, loop, compressor) => {
if (!A.isForNode(loop) || !loop.init || !A.isVarNode(loop.init)) {
return undefined;
}
const definitions = loop.init.declarations;
const counter = definitions[definitions.length - 1];
const copy = definitions[definitions.length - 2];
const { test, update, body } = loop;
if (
!copy ||
!A.isSymbolVarNode(copy.id) ||
copy.id.definition.identifiers.some(A.isSymbolFunargNode) ||
!A.isNumberNode(counter.init) ||
!Number.isInteger(counter.init.value) ||
!A.isArrayNode(copy.init) ||
copy.init.elements.length > 0 ||
!A.isBinaryNode(test) ||
test.operator !== "<" ||
!A.isDotNode(test.right) ||
A.terserProperty(test.right) !== "length" ||
!isRefTo(test.right.object, "arguments") ||
!(A.isUnaryPostfixNode(update) || A.isUnaryPrefixNode(update)) ||
update.operator !== "++" ||
!A.isSimpleStatementNode(body) ||
!A.isAssignNode(/** @type {Node} */ (body).expression) ||
/** @type {Node} */ (body).expression.operator !== "="
) {
return undefined;
}
const index = counter.id.name;
const offset = /** @type {number} */ (counter.init.value);
const { left, right } = /** @type {Node} */ (body).expression;
if (
(offset > 0 && !compressor.option("unsafe")) ||
!isRefTo(test.left, index) ||
!isRefTo(update.argument, index) ||
!A.isSubNode(left) ||
!isRefTo(left.object, copy.id.name) ||
!(offset === 0
? isRefTo(left.property, index)
: A.isBinaryNode(left.property) &&
left.property.operator === "-" &&
isRefTo(left.property.left, index) &&
A.isNumberNode(left.property.right) &&
left.property.right.value === offset) ||
!A.isSubNode(right) ||
!isRefTo(right.object, "arguments") ||
!isRefTo(right.property, index) ||
A.isPinned(lambda)
) {
return undefined;
}
// Nothing else may read the counter's last value or `arguments` itself,
// whose iterator or `length` code elsewhere could have replaced.
const counts = new Map([
[index, 0],
["arguments", 0]
]);
countNames(lambda, counts);
if (counts.get(index) !== 5 || counts.get("arguments") !== 2) {
return undefined;
}
/** @type {Node} */
let value = makeNode(A.ArrayNode, copy.init, {
elements: [
makeNode(A.SpreadElementNode, right.object, {
argument: right.object
})
]
});
if (offset > 0) {
value = makeNode(A.CallNode, copy.init, {
callee: makeNode(A.DotNode, copy.init, {
object: value,
optional: false,
property: A.SymbolPropertyNode({ name: "slice" }),
computed: false
}),
arguments: [counter.init],
optional: false
});
}
return makeNode(A.VarNode, loop.init, {
declarations: [
...definitions.slice(0, -2),
makeNode(A.VarDefNode, copy, { id: copy.id, init: value })
]
});
};
/**
* Writes each rest parameter's copying `for` in a function's body as a spread.
* @param {Node} lambda a function
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const spreadRestCopies = (lambda, compressor) => {
if (A.isArrowNode(lambda) || compressor.option("ecma") < 2015) return;
const statements = /** @type {Node} */ (lambda.body).body;
for (let i = 0; i < /** @type {Node[]} */ (statements).length; i++) {
const spread = restCopyAsSpread(
lambda,
/** @type {Node[]} */ (statements)[i],
compressor
);
if (!spread) continue;
const next = /** @type {Node[]} */ (statements)[i + 1];
// terser's join_vars, which ran while the loop stood between them.
const joined = A.isVarNode(next)
? next
: A.isForNode(next) && A.isVarNode(next.init)
? next.init
: undefined;
if (joined) {
joined.declarations.unshift(...spread.declarations);
/** @type {Node[]} */ (statements).splice(i, 1);
} else {
/** @type {Node[]} */ (statements)[i] = spread;
}
}
};
/**
* A function's last statements, shortened where they fall off its end.
* @param {Node} lambda a function
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const shortenTailReturn = (lambda, compressor) => {
// An async generator awaits a returned value, a tick falling off skips.
if (lambda.async && lambda.generator) return;
const statements = /** @type {Node} */ (lambda.body).body;
// An arrow of one `return` prints as its value, shorter than an `if`.
shortenTail(
/** @type {Node[]} */ (statements),
A.isArrowNode(lambda) && /** @type {Node[]} */ (statements).length === 1,
compressor
);
};
/**
* What a `return` of `undefined` runs first, `a, b` for `return a, void b`,
* as the statement it can run as before a bare `return`.
* @param {Node} statement a statement
* @returns {Node | undefined} that statement, or undefined for another return
*/
const voidReturnEffects = (statement) => {
if (!A.isReturnNode(statement) || !statement.argument) return undefined;
const value = statement.argument;
const expressions = A.isSequenceNode(value) ? value.expressions : [value];
const tail = expressions[expressions.length - 1];
if (!A.isUnaryPrefixNode(tail) || tail.operator !== "void") {
return undefined;
}
const effects = expressions.slice(0, -1);
if (!A.isConstantNode(tail.argument)) effects.push(tail.argument);
return effects.length === 0
? undefined
: makeNode(A.SimpleStatementNode, statement, {
expression: makeSequence(value, effects)
});
};
/**
* What a statement the walk's node holds runs before a bare `return`, if it
* splits: not in an async generator, which awaits a returned value.
* @param {Node} statement a statement the node holds
* @param {Walker} walker the walk visiting the node
* @returns {Node | undefined} that statement, or undefined when it stays
*/
const splitEffectsOf = (statement, walker) => {
const effects = voidReturnEffects(statement);
if (effects === undefined) return undefined;
const lambda = walker.find_parent(A.isLambdaNode);
return lambda && lambda.async && lambda.generator ? undefined : effects;
};
/**
* Splits the `return` a statement-with-body node holds under a key.
* @param {Node} node an `if`, loop or `with`
* @param {"consequent" | "alternate" | "body"} key the key holding its statement
* @param {Walker} walker the walk visiting it
* @returns {void}
*/
const splitVoidReturnAt = (node, key, walker) => {
const statement = /** @type {Node} */ (node[key]);
const effects = statement ? splitEffectsOf(statement, walker) : undefined;
if (effects === undefined) return;
node[key] = makeNode(A.BlockStatementNode, statement, {
body: [effects, makeNode(A.ReturnNode, statement, { argument: null })]
});
};
/**
* `return a, void b` as `a, b; return`, where a statement list holds it, or
* as `{ a, b; return }` in a body that holds one statement: `void` and the
* `return` value's space cost more than the `;` or the braces.
* @param {Node} node a node holding statements
* @param {Walker} walker the walk visiting it
* @returns {void}
*/
const splitVoidReturns = (node, walker) => {
if (A.isStatementWithBodyNode(node)) {
splitVoidReturnAt(node, A.isIfNode(node) ? "consequent" : "body", walker);
splitVoidReturnAt(node, "alternate", walker);
return;
}
if (!A.isBlockNode(node)) return;
const body = A.bodyOf(node);
if (!Array.isArray(body)) return;
for (let i = 0; i < body.length; i++) {
const effects = splitEffectsOf(body[i], walker);
if (effects === undefined) continue;
// A function's own body ending in what only declares falls off there.
if (A.isLambdaNode(node) && body.slice(i + 1).every(onlyDeclares)) {
body[i] = effects;
continue;
}
body.splice(
i,
1,
effects,
makeNode(A.ReturnNode, body[i], { argument: null })
);
i++;
}
};
/**
* @param {Node} statement a statement
* @returns {boolean} whether running it does nothing: a function declaration or `var` without values
*/
const onlyDeclares = (statement) =>
A.isDefunNode(statement) ||
A.isEmptyStatementNode(statement) ||
(A.isVarNode(statement) &&
statement.declarations.every(
(/** @type {Node} */ declaration) =>
!declaration.init && A.isSymbolDeclarationNode(declaration.id)
));
// The unary operators that evaluate their operand before anything else.
const OPERAND_FIRST_UNARY = new Set(["!", "-", "+", "~", "void", "typeof"]);
/**
* The node an expression evaluates first, down its leftmost operands, and
* where it sits; it stops at an assignment to a variable.
* @param {Node} head an expression
* @returns {{ node: Node, parent: Node | undefined, key: string }} that node, what holds it and under which key
*/
const firstEvaluated = (head) => {
/** @type {Node | undefined} */
let parent;
let key = "";
let node = head;
for (;;) {
/** @type {string} */
let next;
if (A.isSequenceNode(node)) {
next = "expressions";
} else if (A.isAssignNode(node)) {
if (!A.isPropAccessNode(node.left)) break;
next = "left";
} else if (A.isBinaryNode(node)) {
next = "left";
} else if (A.isConditionalNode(node)) {
next = "test";
} else if (A.isCallExpressionNode(node)) {
next = "callee";
} else if (A.isPropAccessNode(node)) {
next = "object";
} else if (A.isChainNode(node)) {
next = "expression";
} else if (A.isPrefixedTemplateStringNode(node)) {
next = "tag";
} else if (
A.isUnaryPrefixNode(node) &&
OPERAND_FIRST_UNARY.has(node.operator)
) {
next = "argument";
} else {
break;
}
parent = node;
key = next;
node =
next === "expressions"
? node.expressions[0]
: /** @type {Node} */ (
/** @type {Record<string, NodeFieldValue>} */ (node)[next]
);
}
return { node, parent, key };
};
/**
* The expression a statement evaluates before anything else, if any.
* @param {Node} statement a statement
* @returns {Node | null | undefined} that expression
*/
const headOf = (statement) => {
if (A.isSimpleStatementNode(statement)) return statement.expression;
if (A.isExitNode(statement)) return statement.argument;
if (A.isIfNode(statement)) return statement.test;
if (A.isSwitchNode(statement)) return statement.discriminant;
if (A.isForNode(statement) && !A.isDefinitionsLikeNode(statement.init)) {
return statement.init;
}
return undefined;
};
/**
* @param {Node | null | undefined} expression an expression, if any
* @param {Definition} definition a variable
* @returns {boolean} whether it, or an expression of its sequence, opens assigning that variable
*/
const assignsFirst = (expression, definition) => {
if (!expression) return false;
if (A.isSequenceNode(expression)) {
return expression.expressions.some((/** @type {Node} */ element) =>
assignsFirst(element, definition)
);
}
const { node } = firstEvaluated(expression);
return (
A.isAssignNode(node) &&
A.isSymbolRefNode(node.left) &&
node.left.definition === definition
);
};
/**
* @param {Node} expression an expression
* @returns {boolean} whether it needs parentheses opening a statement
*/
const opensWithParentheses = (expression) => {
const { node } = firstEvaluated(expression);
return (
A.isFunctionNode(node) || A.isClassNode(node) || A.isObjectNode(node)
);
};
/**
* Replaces the expression `headOf` reads.
* @param {Node} statement a statement `headOf` read
* @param {Node} head what it evaluates first instead
* @returns {void}
*/
const setHead = (statement, head) => {
if (A.isSimpleStatementNode(statement)) statement.expression = head;
else if (A.isExitNode(statement)) statement.argument = head;
else if (A.isIfNode(statement)) statement.test = head;
else if (A.isSwitchNode(statement)) statement.discriminant = head;
else statement.init = head;
};
/**
* @param {Node} declaration an import
* @returns {boolean} whether it imports a namespace
*/
const importsNamespace = (declaration) => {
const names = declaration.imported_names;
return (
names !== null &&
names !== undefined &&
names.length === 1 &&
names[0].imported.name === "*" &&
!names[0].imported.quote
);
};
/**
* Merges each import into the first one of the same module and attributes,
* whose place keeps the order modules run in; one importing nothing goes.
* @param {Node[]} statements a module's statements
* @returns {void}
*/
const mergeImports = (statements) => {
/** @type {Map<string, Node>} */
const first = new Map();
for (let i = 0; i < statements.length; i++) {
const declaration = statements[i];
if (!A.isImportNode(declaration) || declaration.phase) continue;
const key = `${modules.printToString(declaration.source)} ${
declaration.attributes
? modules.printToString(declaration.attributes)
: ""
}`;
const target = first.get(key);
if (target === undefined) {
first.set(key, declaration);
continue;
}
const defaultName = declaration.imported_name;
let names = declaration.imported_names;
if (!defaultName && !names) {
statements.splice(i--, 1);
continue;
}
// A second default name is the export named `default`.
if (defaultName && target.imported_name) {
if (importsNamespace(declaration)) continue;
names = [
makeNode(A.NameMappingNode, defaultName, {
imported: makeNode(A.SymbolImportForeignNode, defaultName, {
name: "default"
}),
exported: null,
local: defaultName
}),
...(names || [])
];
}
const targetNames = target.imported_names;
if (
names &&
targetNames &&
(importsNamespace(target) || importsNamespace(declaration))
) {
continue;
}
if (defaultName && !target.imported_name) {
target.imported_name = defaultName;
}
if (names) {
target.imported_names = targetNames
? [...targetNames, ...names]
: names;
}
statements.splice(i--, 1);
}
};
/**
* Writes the names an import binds that only an export reads as exported
* from that module: `import{a as b}from"m";export{b as c}` as
* `export{a as c}from"m"`, in the import's place, which keeps module order.
* @param {Node} toplevel a module
* @param {boolean} namespaceExports whether the output may use `export * as`
* @returns {void}
*/
const exportImportsFrom = (toplevel, namespaceExports) => {
if (!improvements.enabled || A.usesEval(toplevel)) return;
const body = /** @type {Node[]} */ (toplevel.body);
/** @type {Map<Definition, number>} */
const reads = new Map();
/** @type {Map<Definition, Node[]>} */
const exportedAs = new Map();
walkNode(
toplevel,
A.createWalker((/** @type {Node} */ node) => {
if (A.isExportNode(node)) {
if (node.source) return true;
// An exported value or declaration reads its names as any code does.
if (!node.exported_names) return;
for (const mapping of node.exported_names) {
const { definition } = mapping.local;
if (!definition) continue;
const list = exportedAs.get(definition);
if (list) list.push(mapping);
else exportedAs.set(definition, [mapping]);
}
return true;
}
if (A.isSymbolRefNode(node) && node.definition) {
reads.set(node.definition, (reads.get(node.definition) || 0) + 1);
}
})
);
/** @type {Set<Node>} */
const moved = new Set();
/**
* @param {Node} local a name an import binds
* @returns {Node[] | undefined} the mappings exporting it, if only they read it
*/
const onlyExported = (local) => {
const { definition } = local;
const mappings = definition && exportedAs.get(definition);
return mappings && !reads.has(definition) ? mappings : undefined;
};
for (let i = 0; i < body.length; i++) {
const declaration = body[i];
if (!A.isImportNode(declaration) || declaration.phase) continue;
const importedName = declaration.imported_name;
const importedNames = declaration.imported_names;
/** @type {Node[]} */
const movedHere = [];
/** @type {Node[]} */
const names = [];
/** @type {Node[]} */
const namespaces = [];
/**
* @param {string} imported the name the module exports
* @param {Node[]} mappings the exports of the name bound
* @param {string=} quote the quote the name is written in, if a string
* @returns {void}
*/
const reexport = (imported, mappings, quote) => {
for (const mapping of mappings) {
movedHere.push(mapping);
names.push(
makeNode(A.NameMappingNode, mapping, {
imported: null,
exported: mapping.exported,
local: makeNode(A.SymbolExportNode, mapping.local, {
name: imported,
quote
})
})
);
}
};
const defaultName = declaration.imported_name;
const defaultExports = defaultName && onlyExported(defaultName);
if (defaultExports) {
reexport("default", defaultExports);
declaration.imported_name = /** @type {Node} */ (
/** @type {Node | null} */ (null)
);
}
if (declaration.imported_names) {
declaration.imported_names = declaration.imported_names.filter(
(/** @type {Node} */ mapping) => {
const mappings = onlyExported(mapping.local);
if (!mappings) return true;
if (mapping.imported.name !== "*" || mapping.imported.quote) {
reexport(mapping.imported.name, mappings, mapping.imported.quote);
return false;
}
// A namespace is exported once per `export * as`, from ES2020.
if (!namespaceExports || mappings.length !== 1) return true;
movedHere.push(mappings[0]);
namespaces.push(
makeNode(A.ExportNode, declaration, {
exported_definition: null,
exported_value: null,
exported_names: [
makeNode(A.NameMappingNode, mappings[0], {
imported: null,
exported: mappings[0].exported,
local: makeNode(A.SymbolExportNode, mappings[0].local, {
name: "*"
})
})
],
source: declaration.source,
attributes: declaration.attributes
})
);
return false;
}
);
if (declaration.imported_names.length === 0) {
declaration.imported_names = /** @type {Node[]} */ (
/** @type {Node[] | null} */ (null)
);
}
}
// Only all of an import, written as one export: what stays imported, or
// names beside a namespace, would write the source twice.
if (
(names.length === 0 && namespaces.length === 0) ||
declaration.imported_name ||
declaration.imported_names ||
(names.length > 0 && namespaces.length > 0)
) {
declaration.imported_name = importedName;
declaration.imported_names = importedNames;
continue;
}
for (const mapping of movedHere) moved.add(mapping);
const added = [...namespaces];
if (names.length > 0) {
added.unshift(
makeNode(A.ExportNode, declaration, {
exported_definition: null,
exported_value: null,
exported_names: names,
source: declaration.source,
attributes: declaration.attributes
})
);
}
body.splice(i, 1, ...added);
i += added.length - 1;
}
if (moved.size === 0) return;
for (let i = body.length; i-- > 0;) {
const statement = body[i];
if (
!A.isExportNode(statement) ||
statement.source ||
!statement.exported_names
) {
continue;
}
const kept = statement.exported_names.filter(
(/** @type {Node} */ mapping) => !moved.has(mapping)
);
if (kept.length === statement.exported_names.length) continue;
if (kept.length === 0) body.splice(i, 1);
else statement.exported_names = kept;
}
};
/**
* `var a;for(a in b)` as `for(var a in b)`, which declares the same `var`.
* @param {Node[]} statements a block's statements
* @returns {void}
*/
const declareInLoopHeads = (statements) => {
for (let i = 0; i < statements.length - 1; i++) {
const declaration = statements[i];
const loop = statements[i + 1];
if (
!A.isVarNode(declaration) ||
declaration.declarations.length !== 1 ||
declaration.declarations[0].init ||
!A.isSymbolNode(declaration.declarations[0].id) ||
!A.isForInNode(loop) ||
!A.isSymbolRefNode(loop.left) ||
loop.left.definition === undefined ||
loop.left.definition !== declaration.declarations[0].id.definition
) {
continue;
}
loop.left = declaration;
statements.splice(i, 1);
}
};
/**
* Moves the assignments a statement opens with into the `var` or `let` just
* before it that declares their variables without a value: `var a;a=x` as
* `var a=x`. A `let` nothing else in the block names, given a value with no
* effect, goes instead, as the compressor drops such a declaration.
* @param {Node[]} statements a block's statements
* @param {Compressor} compressor the compressor
* @param {boolean} inSwitchBranch whether they are a `switch` case's, whose `let` later cases read
* @returns {void}
*/
const assignIntoDeclarations = (statements, compressor, inSwitchBranch) => {
/** @type {Node[]} */
const filledLets = [];
for (let i = 0; i < statements.length - 1; i++) {
const declaration = statements[i];
const isVar = A.isVarNode(declaration);
if (!isVar && !A.isLetNode(declaration)) continue;
for (;;) {
const statement = statements[i + 1];
const head = statement && headOf(statement);
if (!head) break;
const { node: assign, parent, key } = firstEvaluated(head);
// A nested sequence would keep a bare `a` where the assignment was;
// `(a=x).p` measured worse on gzip, as code repeating it stays.
if (
!A.isAssignNode(assign) ||
assign.operator !== "=" ||
!A.isSymbolRefNode(assign.left) ||
assign.left.definition === undefined ||
assign.left.name === "eval" ||
assign.left.name === "arguments" ||
key === "object" ||
(parent !== head && A.isSequenceNode(parent))
) {
break;
}
const { definition } = assign.left;
const { declarations } = declaration;
// Only past the last initializer, which would otherwise run after it.
let index = declarations.length;
while (
--index >= 0 &&
!declarations[index].init &&
declarations[index].id.definition !== definition
);
const declarator = declarations[index];
if (!declarator || declarator.init) break;
const following = statements[i + 2];
const rest =
key === "expressions" ? /** @type {Node} */ (parent).expressions : [];
// Another `a=` opening what follows repeats this one, which gzip
// reads better kept; a function opening a statement needs parentheses.
if (
rest.some(
(/** @type {Node} */ expression, /** @type {number} */ position) =>
position > 0 && assignsFirst(expression, definition)
) ||
(following !== undefined &&
assignsFirst(headOf(following), definition)) ||
(rest.length > 1 &&
A.isSimpleStatementNode(statement) &&
opensWithParentheses(rest[1]))
) {
break;
}
// A loop's `let` is a binding each time round, seen nowhere after.
if (!isVar && parent === undefined && A.isForNode(statement)) break;
// Moved into the declaration, a read of the `let` is in its dead zone:
// one in the value, or one in a function or `eval` the value may run.
if (
!isVar &&
(A.walk(assign.right, (/** @type {Node} */ node) =>
A.isLambdaNode(node)
? true
: A.isSymbolRefNode(node) && node.definition === definition
? A.walk_abort
: undefined
) ||
(A.hasSideEffects(assign.right, compressor) &&
readByClosure(definition)))
) {
break;
}
declarator.init = assign.right;
if (!isVar) filledLets.push(declarator);
if (parent === undefined) {
if (A.isSimpleStatementNode(statement)) {
statements.splice(i + 1, 1);
continue;
}
if (A.isForNode(statement)) {
statement.init = declaration;
statements.splice(i, 1);
} else {
setHead(statement, assign.left);
}
break;
}
if (key !== "expressions") {
/** @type {Record<string, NodeFieldValue>} */ (parent)[key] =
assign.left;
break;
}
parent.expressions.shift();
if (parent.expressions.length === 1) {
setHead(statement, parent.expressions[0]);
}
}
// A `var` the moved assignments leave next to it joins, then the
// assignments opening what follows that move in too.
const next = statements[i + 1];
if (next !== undefined && A.kindOf(next) === A.kindOf(declaration)) {
declaration.declarations.push(...next.declarations);
statements.splice(i + 1, 1);
i--;
}
}
if (inSwitchBranch) return;
for (const declarator of filledLets) {
dropUnreadLet(statements, declarator, compressor);
}
};
/**
* Whether code other than the function declaring a binding may read it: a
* function inside, or a direct `eval`.
* @param {Definition} definition the binding
* @returns {boolean} true when one may
*/
const readByClosure = (definition) => {
const { scope } = definition;
if (scope.isPinned()) return true;
const declaringFunction = scope.getDefunScope();
return definition.references.some(
(/** @type {Node} */ reference) =>
!reference.scope ||
reference.scope.getDefunScope() !== declaringFunction
);
};
/**
* Drops a `let` declarator of a block's statements where nothing else there
* names it, and its value has no effect.
* @param {Node[]} statements the block's statements
* @param {Node} declarator the declarator
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const dropUnreadLet = (statements, declarator, compressor) => {
const { id } = declarator;
if (
!A.isSymbolLetNode(id) ||
!id.definition ||
id.definition.scope.isPinned() ||
A.hasSideEffects(declarator.init, compressor)
) {
return;
}
let named = 0;
/**
* @param {Node} node a node of the block
* @returns {void}
*/
const count = (node) => {
if (A.isSymbolNode(node) && node.name === id.name) named++;
};
for (const statement of statements) A.walk(statement, count);
if (named !== 1) return;
const index = statements.findIndex(
(statement) =>
A.isLetNode(statement) && statement.declarations.includes(declarator)
);
if (index === -1) return;
const { declarations } = statements[index];
declarations.splice(declarations.indexOf(declarator), 1);
if (declarations.length === 0) statements.splice(index, 1);
};
A.wrapOptimizer("UnaryPrefix", (optimizeUnary) => (self, compressor) => {
const optimized = optimizeUnary(self, compressor);
if (!improvements.enabled || !A.isUnaryPrefixNode(optimized)) {
return optimized;
}
return coerceUnary(optimized) || optimized;
});
/**
* The name a literal key, or a constant read's key, spells.
* @param {Node | string} key a key
* @returns {string | undefined} the name, undefined where it is not constant
*/
const nameOfKey = (key) => {
if (typeof key === "string") return key;
const value = constantOf(key);
return typeof value === "string" || typeof value === "number"
? String(value)
: undefined;
};
const BUILT_IN_PROTOTYPES = {
Array: Array.prototype,
Object: Object.prototype,
String: String.prototype
};
/** @type {Map<string, Set<string>>} */
const knownMembers = new Map();
for (const owner of Object.keys(pureTables.knownMembers)) {
knownMembers.set(owner, new Set(pureTables.knownMembers[owner]));
}
/**
* Whether a built-in's prototype may hold a key: on the build's Node, in any
* edition, as a target newer than the build's Node may, or the program's own.
* @param {"Array" | "Object" | "String"} owner the built-in
* @param {string} name the key
* @returns {boolean} true when it may
*/
const holdsKey = (owner, name) =>
patchedPrototypes.has(owner) ||
// An array's and a string's prototype inherit from `Object.prototype`.
patchedPrototypes.has("Object") ||
name in BUILT_IN_PROTOTYPES[owner] ||
/** @type {Set<string>} */ (knownMembers.get(owner)).has(name);
/**
* `void 0` for an array, string or object literal's read of a key neither it
* nor its prototype holds, its elements' effects kept: as swc reads it, taking
* the prototypes as the language defines them.
* @param {Node} node a property read the optimizer left
* @param {Node} original the read the optimizer was given, which its parent holds
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} what replaces it, undefined to keep it
*/
const readOfMissingKey = (node, original, compressor) => {
if (node.optional) return undefined;
let parent = compressor.parent();
// The parent holds the node the optimizer was given, and the tail of a
// sequence an assignment targets is its target too.
let target = original;
for (
let level = 1;
A.isSequenceNode(parent) && A.tailNode(parent) === target;
level++
) {
target = parent;
parent = compressor.parent(level);
}
// A target inside a pattern, as `[{}.x] = []` writes it, is written too.
for (
let level = 0, holder = parent;
holder &&
(A.isDestructuringNode(holder) ||
A.isDefaultAssignNode(holder) ||
A.isExpansionNode(holder) ||
A.isObjectKeyValNode(holder));
holder = compressor.parent(++level)
) {
if (A.isDestructuringNode(holder)) return undefined;
}
if (
isLhs(target, parent) ||
(A.isUnaryPrefixNode(parent) && parent.operator === "delete") ||
(A.isCallNode(parent) && parent.callee === node) ||
(A.isPrefixedTemplateStringNode(parent) && parent.tag === node)
) {
return undefined;
}
const name = A.isSubNode(node)
? nameOfKey(node.property)
: nameOfKey(/** @type {string | Node} */ (A.terserProperty(node)));
if (name === undefined) return undefined;
let { object } = node;
// A literal a sequence ends in is read once the rest has run.
/** @type {Node[]} */
const before = [];
if (A.isSequenceNode(object)) {
before.push(...object.expressions.slice(0, -1));
object = A.tailNode(object);
}
/** @type {Node[]} */
let parts;
if (A.isArrayNode(object)) {
const { elements } = object;
if (
elements.some(
(/** @type {Node | null} */ element) =>
element !== null && A.isExpansionNode(element)
) ||
(String(Number(name)) === name &&
Number.isInteger(Number(name)) &&
Number(name) >= 0 &&
Number(name) < elements.length) ||
name === "length" ||
holdsKey("Array", name)
) {
return undefined;
}
parts = elements.filter(
(/** @type {Node | null} */ element) => element !== null
);
} else if (A.isStringNode(object)) {
const text = /** @type {string} */ (object.value);
if (
(String(Number(name)) === name &&
Number.isInteger(Number(name)) &&
Number(name) >= 0 &&
Number(name) < text.length) ||
name === "length" ||
holdsKey("String", name)
) {
return undefined;
}
parts = [];
} else if (A.isObjectNode(object)) {
parts = [];
for (const property of object.properties) {
const key = A.isObjectKeyValNode(property)
? !property.computed && nameOfKey(A.terserKey(property.key))
: undefined;
// A getter, a spread or `__proto__` may hold any key.
if (!key || key === "__proto__" || key === name) return undefined;
parts.push(/** @type {Node} */ (property.value));
}
if (holdsKey("Object", name)) return undefined;
} else {
return undefined;
}
const effects = [
...before,
...parts
.map((part) => A.dropSideEffectFree(part, compressor))
.filter(Boolean)
];
return effects.length === 0
? makeVoid0(node)
: makeSequence(node, [
.../** @type {Node[]} */ (effects),
makeVoid0(node)
]);
};
for (const name of ["Sub", "Dot"]) {
A.wrapOptimizer(name, (optimizeRead) => (self, compressor) => {
const optimized = optimizeRead(self, compressor);
if (
!improvements.enabled ||
!(A.isSubNode(optimized) || A.isDotNode(optimized))
) {
return optimized;
}
const replacement = readOfMissingKey(
optimized,
compressor.self(),
compressor
);
if (!replacement) return optimized;
// `[][0]` and `{}.a` are no longer than the `void 0` they read, and a
// sequence, or `void 0` read a property of, takes parentheses.
const result = A.optimize(replacement, compressor);
const parent = compressor.parent();
const parentheses =
A.isSequenceNode(result) ||
(A.isPropAccessNode(parent) && parent.object === self)
? 2
: 0;
return A.nodeSize(result, compressor) + parentheses <=
A.nodeSize(optimized, compressor)
? result
: optimized;
});
}
A.wrapOptimizer("Sub", (optimizeSub) => (self, compressor) => {
const optimized = optimizeSub(self, compressor);
if (
!improvements.enabled ||
!A.isSubNode(optimized) ||
optimized.optional ||
!A.isStringNode(optimized.object)
) {
return optimized;
}
const parent = compressor.parent();
// `delete "ab"[0]` answers false, or throws in strict code.
if (
isLhs(self, parent) ||
(A.isUnaryPrefixNode(parent) && parent.operator === "delete")
) {
return optimized;
}
const text = /** @type {string} */ (optimized.object.value);
const index = constantOf(optimized.property);
if (
typeof index !== "number" ||
!Number.isInteger(index) ||
index < 0 ||
index >= text.length
) {
return optimized;
}
const code = text.charCodeAt(index);
// Half of a surrogate pair prints as an escape longer than the read.
return code >= 0xd800 && code <= 0xdfff
? optimized
: makeNode(A.StringNode, optimized, { value: text[index] });
});
/**
* A computed `"__proto__"` or `"constructor"` key of a method or class
* member written as a name: `__proto__: v` alone sets a prototype, and
* `constructor` means the same on a static method only.
* @param {Node} node an object or class member terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node} the member
*/
const liftSpecialKey = (node, compressor) => {
if (
!node.computed ||
!A.isStringNode(node.key) ||
!compressor.option("computed_props")
) {
return node;
}
const key = node.key.value;
if (
A.isObjectKeyValNode(node) ||
(key === "constructor"
? !A.isClassNode(compressor.parent()) ||
!node.static ||
A.isClassPropertyNode(node)
: key !== "__proto__")
) {
return node;
}
node.computed = false;
node.quote = node.key.quote;
node.key = A.isClassPropertyNode(node)
? makeNode(A.SymbolClassPropertyNode, node.key, {
name: /** @type {string} */ (key)
})
: makeNode(A.SymbolMethodNode, node.key, {
name: /** @type {string} */ (key)
});
return node;
};
for (const kind of ["ObjectProperty", "ConciseMethod"]) {
A.wrapOptimizer(kind, (optimizeMember) => (self, compressor) => {
const optimized = optimizeMember(self, compressor);
return improvements.enabled
? liftSpecialKey(optimized, compressor)
: optimized;
});
}
const { isBasicIdentifier: isPlainKey } = createUnicode();
// Keys a plain name means otherwise than the computed one: a prototype set,
// a class's constructor, and a static `prototype`, an early error.
const SPECIAL_KEYS = new Set(["__proto__", "constructor", "prototype"]);
/**
* The constant a name in brackets stands for, where writing it in its place
* is no longer: a name mangles to a letter, unless a script's top level keeps it.
* @param {Node} node a computed key or property
* @param {Compressor} compressor the compressor
* @param {boolean} dotted whether the constant follows a dot, so must be a name
* @returns {Node | undefined} a string or a number, or undefined
*/
const constantKeyOf = (node, compressor, dotted) => {
if (!A.isSymbolRefNode(node) || !node.definition) return undefined;
let value = A.evaluate(node, compressor);
if (value === node) {
// `reduce_vars` marks no key it walks past, though its binding is fixed.
const fixed = A.fixedValue(node);
value =
fixed && (A.isStringNode(fixed) || A.isNumberNode(fixed))
? fixed.value
: node;
}
/** @type {string} */
let text;
if (typeof value === "string") {
if (SPECIAL_KEYS.has(value)) return undefined;
text = isPlainKey(value) ? value : dotted ? "" : JSON.stringify(value);
} else if (
!dotted &&
typeof value === "number" &&
Number.isSafeInteger(value) &&
value >= 0
) {
text = String(value);
} else {
return undefined;
}
if (text === "") return undefined;
const { definition } = node;
const nameLength =
definition.global &&
!compressor.option("module") &&
!compressor.option("toplevel")
? node.name.length
: 1;
// Only a strictly shorter key: an equal one reads no less, and costs a
// space after `static`, `get` or `set`.
if (text.length + (dotted ? 1 : 0) >= nameLength + 2) return undefined;
return typeof value === "string"
? makeNode(A.StringNode, node, { value })
: makeNode(A.NumberNode, node, { value });
};
for (const kind of ["ObjectProperty", "ConciseMethod", "ObjectKeyVal"]) {
A.wrapOptimizer(kind, (optimizeMember) => (self, compressor) => {
if (
improvements.enabled &&
self.computed &&
compressor.option("computed_props")
) {
const key = constantKeyOf(self.key, compressor, false);
if (key !== undefined) self.key = key;
}
return optimizeMember(self, compressor);
});
}
A.wrapOptimizer("Sub", (optimizeSub) => (self, compressor) => {
if (improvements.enabled && compressor.option("properties")) {
const property = constantKeyOf(self.property, compressor, true);
if (property !== undefined) self.property = property;
}
const optimized = optimizeSub(self, compressor);
return (
(improvements.enabled &&
A.isSubNode(optimized) &&
elementRead(optimized, compressor)) ||
optimized
);
});
/**
* `[a, o][1]` as `o` where the other elements read nothing: the value the
* array literal holds there, neither called nor a place written through it.
* @param {Node} self an indexed read
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the element, if it stands for the read
*/
const elementRead = (self, compressor) => {
const { object, property } = self;
const parent = compressor.parent();
const index = A.isNumberNode(property)
? /** @type {number} */ (property.value)
: -1;
if (
self.optional ||
!A.isArrayNode(object) ||
!Number.isInteger(index) ||
index < 0 ||
index >= object.elements.length ||
isLhs(compressor.self(), parent) ||
isPatternTarget(compressor.self(), compressor, A) ||
(A.isUnaryPrefixNode(parent) && parent.operator === "delete") ||
// Called, it reads the array as its `this`.
(A.isCallNode(parent) && parent.callee === self) ||
A.isPrefixedTemplateStringNode(parent)
) {
return undefined;
}
const element = object.elements[index];
if (
!element ||
object.elements.some(
(/** @type {Node | null} */ other) =>
other !== null &&
(A.isExpansionNode(other) ||
(other !== element && A.hasSideEffects(other, compressor)))
)
) {
return undefined;
}
return element;
};
/**
* Whether a name reads the same before and after other code runs: a
* parameter or `var` never written, which no `arguments` or `eval` reaches.
* @param {Node} node an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} true for a parameter only ever read
*/
const readsUnchanged = (node, compressor) =>
staysPut(node, compressor) &&
A.isSymbolVarNode(node.definition.identifiers[0]) &&
!node.definition.scope.usesArguments &&
!node.definition.scope.usesEval;
/**
* `a * (b % c)` as `b % c * a`, `/` and `*` inside too: the product converts
* its sides in the same order, and the name `a`, read later, reads the same.
* @param {Node} node a binary expression terser has optimized
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the product without parentheses, or undefined
*/
const commutedProduct = (node, compressor) => {
const { left, right } = node;
if (
node.operator !== "*" ||
!A.isBinaryNode(right) ||
(right.operator !== "*" &&
right.operator !== "/" &&
right.operator !== "%") ||
// terser itself moves a number literal last.
!readsUnchanged(left, compressor)
) {
return undefined;
}
return makeNode(A.BinaryNode, node, {
operator: "*",
left: right,
right: left
});
};
/**
* `1 == (a, 2)` as `(a, 1 == 2)`: a constant on the left reads alike before
* the sequence on the right runs or after, so the comparison can fold.
* @param {Node} node an optimized binary
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the sequence, or undefined
*/
const liftedRightSequence = (node, compressor) => {
if (
!A.isSequenceNode(node.right) ||
LAZY_OPERATORS.has(node.operator) ||
A.isAssignNode(node) ||
!A.isConstantNode(node.left)
) {
return undefined;
}
const { expressions } = node.right;
const last = A.optimize(
makeNode(A.BinaryNode, node, {
operator: node.operator,
left: node.left,
right: expressions[expressions.length - 1]
}),
compressor
);
// Lifted without folding, the sequence only moves, and takes parentheses.
if (!A.isConstant(last)) return undefined;
return makeNode(A.SequenceNode, node, {
expressions: [...expressions.slice(0, -1), last]
});
};
/**
* `1 == !a` as `!a` and `0 == !a` as `!!a`: a boolean compared loosely
* with a number or boolean constant is itself or its negation.
* @param {Node} node an optimized comparison
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the boolean, or undefined
*/
const booleanEquality = (node, compressor) => {
if (node.operator !== "==" && node.operator !== "!=") return undefined;
for (const [constant, other] of [
[node.left, node.right],
[node.right, node.left]
]) {
const value = A.evaluate(constant, compressor);
if (
A.isSyntaxNode(value) ||
(value !== 0 && value !== 1 && typeof value !== "boolean") ||
!A.isBoolean(other) ||
// Negated, a binary comparison takes parentheses, gaining nothing.
A.isBinaryNode(other)
) {
continue;
}
return Boolean(value) === (node.operator === "==")
? other
: makeNode(A.UnaryPrefixNode, node, { operator: "!", argument: other });
}
return undefined;
};
A.wrapOptimizer("Binary", (optimizeBinary) => (self, compressor) => {
const optimized = optimizeBinary(self, compressor);
if (!improvements.enabled || !A.isBinaryNode(optimized)) return optimized;
return (
liftedRightSequence(optimized, compressor) ||
nullishComparison(optimized, compressor) ||
booleanEquality(optimized, compressor) ||
commutedProduct(optimized, compressor) ||
logicalAssignment(optimized, compressor) ||
unreadOptionalChain(optimized, self, compressor) ||
coerceBinary(optimized, self, compressor) ||
shortenBinary(optimized, compressor)
);
});
// terser writes `c ? !0 : x` as `!!c || x` even where that prints longer.
for (const kind of ["Binary", "Conditional"]) {
A.wrapOptimizer(kind, (optimize) => (self, compressor) => {
const optimized = optimize(self, compressor);
return (
(improvements.enabled &&
A.isBinaryNode(optimized) &&
booleanConditional(optimized, self, compressor)) ||
optimized
);
});
}
/**
* The names a function's parameters bind, with `arguments` where it has its own.
* @param {Node} lambda the function
* @returns {Set<string>} the names
*/
const parameterNames = (lambda) => {
/** @type {Set<string>} */
const names = new Set();
if (!A.isArrowNode(lambda)) names.add("arguments");
for (const parameter of lambda.params) {
walkNode(
parameter,
A.createWalker((/** @type {Node} */ node) => {
if (A.isSymbolDeclarationNode(node)) names.add(node.name);
})
);
}
return names;
};
/**
* Whether a function declares one of the names, outside the functions it holds.
* @param {Node} fn the function
* @param {Set<string>} names the names
* @returns {boolean} true when it does
*/
const declaresAny = (fn, names) => {
let declares = false;
walkNode(
fn,
A.createWalker((/** @type {Node} */ node) => {
if (declares) return true;
if (node !== fn && A.isLambdaNode(node)) {
if (node.id && names.has(node.id.name)) declares = true;
return true;
}
if (A.isSymbolDeclarationNode(node) && names.has(node.name)) {
declares = true;
}
})
);
return declares;
};
/**
* The function a function's whole body calls in place and returns, passed
* nothing, when its body can stand in for the caller's own.
* @param {Node} lambda a function
* @returns {Node | undefined} the function it calls
*/
const returnedInPlace = (lambda) => {
/** @type {Node | undefined} */
let call;
if (!A.isBlockStatementNode(lambda.body)) {
// An arrow's expression body is what it returns.
call = /** @type {Node | undefined} */ (lambda.body);
} else if (
/** @type {Node[]} */ (/** @type {Node} */ (lambda.body).body).length ===
1 &&
A.isReturnNode(
/** @type {Node[]} */ (/** @type {Node} */ (lambda.body).body)[0]
)
) {
call = /** @type {Node[]} */ (/** @type {Node} */ (lambda.body).body)[0]
.argument;
}
if (!call) return undefined;
if (
!A.isCallExpressionNode(call) ||
call.optional ||
call.arguments.length > 0 ||
(call._annotations & A._NOINLINE) !== 0
) {
return undefined;
}
const fn = call.callee;
if (
!(A.isFunctionNode(fn) || A.isArrowNode(fn)) ||
!A.isBlockStatementNode(fn.body) ||
fn.params.length > 0 ||
fn.id ||
fn.async ||
fn.generator ||
A.isPinned(fn) ||
A.isPinned(lambda) ||
/** @type {Node[]} */ (/** @type {Node} */ (fn.body).body).some(
A.isDirectiveNode
) ||
// A function's own context differs from the caller's, and a generator
// or async caller reads `yield` and `await` as its own.
((A.isFunctionNode(fn) || lambda.async || lambda.generator) &&
readsFunctionContext(fn)) ||
declaresAny(fn, parameterNames(lambda))
) {
return undefined;
}
return fn;
};
/**
* Collects, node by node, the `const` declarations writable as `let`, two
* bytes shorter: where nothing writes a name, as oxc does, at the top level
* only where no other code sees it, nor where `eval` or `with` may write.
* @param {boolean} join whether neighboring `let` declarations join, as `join_vars` says
* @param {boolean} toplevelPrivate whether the top level is a module's, or one `toplevel` names
* @returns {{ note: (node: Node, walker: Walker) => void, finish: () => PendingLets | null }} what notes each node of the compressed tree, then answers the declarations and where they are, null where there are none
*/
const collectLets = (join, toplevelPrivate) => {
/** @type {Node[]} */
const declarations = [];
/** @type {Node[]} */
const holders = [];
/** @type {Node[]} */
const blocks = [];
// A name the compressor wrote without a definition counts by its name.
/** @type {Set<Definition | string>} */
const written = new Set();
// `export { x }` names a binding away from its declaration.
/** @type {Set<Definition>} */
const exported = new Set();
/**
* @param {Node | null} target what an assignment, update or loop writes
* @returns {void}
*/
const markWritten = (target) => {
if (target === null || target === undefined) return;
if (A.isSymbolRefNode(target)) {
written.add(target.definition || target.name);
} else if (A.isDestructuringNode(target)) {
for (const element of target.is_array
? target.elements
: target.properties) {
markWritten(
/** @type {Node | null} */ (
element !== null && A.isObjectKeyValNode(element)
? element.value
: element
)
);
}
} else if (A.isDefaultAssignNode(target)) {
markWritten(target.left);
} else if (A.isExpansionNode(target)) {
markWritten(target.argument);
}
};
/**
* @param {Node[] | Node | undefined} statements a block's statements
* @returns {boolean} whether two declarations neighbor in them
*/
const hasNeighboringDeclarations = (statements) => {
if (!Array.isArray(statements) || statements.length < 2) return false;
let previous = false;
for (let i = 0; i < statements.length; i++) {
const statement = statements[i];
const declares = A.isLetNode(statement) || A.isConstNode(statement);
if (declares && previous) return true;
previous = declares;
}
return false;
};
// By ESTree type, which one read answers: a kind test walks a prototype
// chain, and every node of the tree goes through here.
/**
* @param {Node} node a node of the compressed tree
* @param {Walker} walker the walk visiting it
* @returns {void}
*/
const note = (node, walker) => {
switch (node.type) {
case "VariableDeclaration":
if (node.kind === "const") {
declarations.push(node);
holders.push(walker.parent());
}
break;
case "AssignmentExpression":
markWritten(node.left);
break;
case "UpdateExpression":
markWritten(node.argument);
break;
case "ForInStatement":
case "ForOfStatement":
if (!A.isDefinitionsNode(node.left)) markWritten(node.left);
break;
// Only a named export keeps `const`: `export default a` exports a value.
case "ExportNamedDeclaration":
if (node.source) break;
for (const name of node.exported_names || []) {
if (name.local.definition) exported.add(name.local.definition);
}
break;
default:
if (join && (node[KIND_BITS] & KIND_BLOCK) !== 0) {
blocks.push(node);
}
}
};
/**
* @param {Node} declaration a `const` declaration
* @returns {boolean} whether every name it declares may be `let`
*/
const rewritable = (declaration) => {
let answer = true;
const nameWalker = A.createWalker((/** @type {Node} */ node) => {
if (!answer) return true;
if (!A.isSymbolConstNode(node)) return;
const symbol = node.definition;
answer =
symbol !== undefined &&
symbol !== null &&
(toplevelPrivate || !A.isToplevelNode(symbol.scope.node)) &&
!exported.has(symbol) &&
!symbol.scope.isPinned() &&
!written.has(symbol) &&
!written.has(node.name);
});
for (const definition of declaration.declarations) {
walkNode(definition.id, nameWalker);
}
return answer;
};
/**
* @returns {PendingLets | null} the declarations and where they are, null where there are none
*/
const finish = () => {
let count = 0;
for (let i = 0; i < declarations.length; i++) {
if (A.isExportNode(holders[i]) || !rewritable(declarations[i])) {
continue;
}
declarations[count] = declarations[i];
holders[count] = holders[i];
count++;
}
if (count === 0) return null;
declarations.length = count;
holders.length = count;
/** @type {Node[][]} */
const bodies = [];
for (let i = 0; i < blocks.length; i++) {
const body = A.bodyOf(blocks[i]);
if (hasNeighboringDeclarations(body)) {
bodies.push(/** @type {Node[]} */ (body));
}
}
return { declarations, holders, bodies, join };
};
return { note, finish };
};
/**
* Joins each `let` declaration into a `let` just before it, as terser's
* `join_vars` joins the declarations it wrote.
* @param {Node[]} statements a block's statements
* @param {Set<Node>} converted the declarations written from `const`
* @param {LetCounts} counts where each join is counted
* @returns {void}
*/
const joinLets = (statements, converted, counts) => {
let j = 0;
for (let i = 1; i < statements.length; i++) {
const statement = statements[i];
const previous = statements[j];
if (A.isLetNode(statement) && A.isLetNode(previous)) {
const spaced = !A.isDestructuringNode(statement.declarations[0].id);
if (converted.has(statement)) {
if (spaced) counts.joinedConstSpaced++;
else counts.joinedConstBare++;
} else if (spaced) {
counts.joinedLetSpaced++;
} else {
counts.joinedLetBare++;
}
previous.declarations = [
...previous.declarations,
...statement.declarations
];
} else {
statements[++j] = statement;
}
}
statements.length = j + 1;
};
/**
* A declared pattern with each `const` name written as a `let` name.
* @param {Node} target a declarator's id, or a pattern inside it
* @returns {Node} the pattern, a name replaced
*/
const writeLetNames = (target) => {
if (A.isSymbolConstNode(target)) {
return makeNode(A.SymbolLetNode, target, {
init: target.init,
scope: target.scope,
name: target.name,
definition: target.definition,
flags: target.flags
});
}
if (A.isDestructuringNode(target)) {
const names = target.is_array ? target.elements : target.properties;
for (let i = 0; i < names.length; i++) {
const element = names[i];
if (element === null) continue;
if (A.isObjectKeyValNode(element)) {
element.value = writeLetNames(/** @type {Node} */ (element.value));
} else {
names[i] = writeLetNames(element);
}
}
} else if (A.isDefaultAssignNode(target)) {
target.left = writeLetNames(target.left);
} else if (A.isExpansionNode(target)) {
target.argument = writeLetNames(target.argument);
}
return target;
};
/**
* Writes the declarations `collectLets` found as `let`, each in what holds
* it: a loop's head, or a statement list, where the written ones then join.
* @param {PendingLets} pending the declarations and where they are
* @returns {LetCounts} what changed in the printed text
*/
const writeLets = (pending) => {
const { declarations, holders, bodies, join } = pending;
/** @type {LetCounts} */
const counts = {
converted: declarations.length,
joinedConstSpaced: 0,
joinedConstBare: 0,
joinedLetSpaced: 0,
joinedLetBare: 0
};
/** @type {Set<Node>} */
const converted = new Set();
/** @type {Map<Node, Node>} */
const listed = new Map();
for (let i = 0; i < declarations.length; i++) {
const declaration = declarations[i];
const written = makeNode(A.LetNode, declaration, {
declarations: declaration.declarations,
await: false
});
for (const definition of written.declarations) {
definition.id = writeLetNames(definition.id);
}
if (join) converted.add(written);
const holder = holders[i];
if (A.isForNode(holder)) {
holder.init = written;
} else if (A.isForInNode(holder) || A.isForOfNode(holder)) {
holder.left = written;
} else {
listed.set(declaration, written);
}
}
if (listed.size > 0) {
/** @type {Set<Node[]>} */
const lists = new Set();
for (let i = 0; i < holders.length; i++) {
const body = A.bodyOf(holders[i]);
if (Array.isArray(body)) lists.add(body);
}
for (const statements of lists) {
for (let i = 0; i < statements.length; i++) {
const written = listed.get(statements[i]);
if (written !== undefined) statements[i] = written;
}
}
}
if (join) {
for (const statements of bodies) joinLets(statements, converted, counts);
}
return counts;
};
// A mangled tree takes its `let` once named: the letters mangling counts
// would differ, and with them the names, which gzip reads less well.
/** @type {WeakMap<Node, PendingLets>} */
const pendingLets = new WeakMap();
/**
* Writes the `let` declarations a tree left for after naming, before it is
* named, answering what to count back for the text mangling names by.
* @param {Node} tree the tree compressed
* @returns {LetCounts | null} what changed in the printed text, null where nothing was left
*/
modules.writePendingLets = (tree) => {
const pending = pendingLets.get(tree);
if (pending === undefined) return null;
pendingLets.delete(tree);
return writeLets(pending);
};
const compressTreeReturning = compressorMethods.compress;
/**
* @this {Compressor} the compressor
* @param {Node} toplevel the tree
* @returns {Node} the tree compressed
*/
compressorMethods.compress = function compress(toplevel) {
const compressed = compressTreeReturning.call(this, toplevel);
if (!improvements.enabled) return compressed;
const inline = this.option("inline");
// Tails shorten outermost first; bodies move up innermost first, so a
// body moved up already holds its own. Each reads only its own subtree.
// A moved body leaves behind only a call of it, which declares nothing.
/** @type {Node[]} */
const found = [];
const joinVars = Boolean(this.option("join_vars"));
const lets = improvements.lets.enabled
? collectLets(
joinVars,
Boolean(this.option("module")) || Boolean(this.toplevel.vars)
)
: undefined;
const walker = A.createWalker(
(/** @type {Node} */ node, /** @type {() => void} */ descend) => {
if (lets !== undefined) lets.note(node, walker);
const isLambda = A.isLambdaNode(node);
if (isLambda) {
spreadRestCopies(node, this);
shortenTailReturn(node, this);
}
splitVoidReturns(node, walker);
if (joinVars) {
const statements = A.bodyOf(node);
if (Array.isArray(statements)) {
declareInLoopHeads(statements);
assignIntoDeclarations(
statements,
this,
A.isSwitchBranchNode(node)
);
}
}
if (!isLambda) return;
descend();
if (inline && returnedInPlace(node)) found.push(node);
return true;
}
);
walkNode(compressed, walker);
for (const lambda of found) {
const fn = /** @type {Node} */ (returnedInPlace(lambda));
lambda.body = fn.body;
}
if (A.isToplevelNode(compressed)) {
mergeImports(/** @type {Node[]} */ (compressed.body));
exportImportsFrom(compressed, this.option("ecma") >= 2020);
}
if (lets === undefined) return compressed;
const pending = lets.finish();
if (pending === null) return compressed;
if (this._mangle_options) {
pendingLets.set(compressed, pending);
} else {
writeLets(pending);
}
return compressed;
};
};
// Each equality and the one that answers the opposite.
/** @type {Record<string, string>} */
const NEGATED_EQUALITY = {
"==": "!=",
"!=": "==",
"===": "!==",
"!==": "==="
};
const BITWISE_OPERATORS = new Set(["|", "&", "^", "<<", ">>"]);
// The globals converting a value that `improve` folds a call of on a literal.
const COERCIONS = new Set(["Boolean", "Number", "String", "BigInt"]);
// The binary operators an assignment of a target to itself writes as `op=`.
const COMPOUND_OPERATORS = new Set([
"+",
"-",
"*",
"/",
"%",
"**",
"<<",
">>",
">>>",
"&",
"|",
"^"
]);
// What `constantOf` answers for an expression that is no literal.
const NOT_CONSTANT = Symbol("not constant");
// The operators a binary of two primitive literals computes without throwing.
/** @type {Record<string, (left: EXPECTED_ANY, right: EXPECTED_ANY) => string | number>} */
const ARITHMETIC_OPERATIONS = {
"+": (left, right) => left + right,
"-": (left, right) => left - right,
"*": (left, right) => left * right,
"/": (left, right) => left / right,
"%": (left, right) => left % right
};
// The comparisons a `typeof` guard may make, as each runs on two strings;
// terser writes a strict one of a `typeof` loose before it drops.
/** @type {Record<string, (left: string, right: string) => boolean>} */
const TYPEOF_COMPARISONS = {
"==": (left, right) => left === right,
"===": (left, right) => left === right,
"!=": (left, right) => left !== right,
"!==": (left, right) => left !== right,
"<": (left, right) => left < right,
"<=": (left, right) => left <= right,
">": (left, right) => left > right,
">=": (left, right) => left >= right
};
// The largest number a call `improve` evaluates may take, as a count or length.
const MAX_EVALUATED_COUNT = 0xffff;
const ASCII = /^[\0-\u007F]*$/;
// Latin, Greek and Cyrillic, whose case mappings, special ones included, no
// Unicode version since 3.0 has changed.
const SETTLED_CASE = /^[\0-\u052F]*$/;
const CASE_METHODS = new Set(["toLowerCase", "toUpperCase"]);
// The globals the spec makes non-configurable, which `delete` leaves.
const UNDELETABLE_GLOBALS = new Set(["undefined", "NaN", "Infinity"]);
// What a regular expression's source may hold for terser to build it: no
// pattern that backtracks without bound.
const SAFE_REGEXP_SOURCE = /^[\\/|\0\s\w^$.[\]()]*$/;
const EVALUATED_GLOBALS = { Array, Math, Number, Object, String };
const REGEXP_FLAG_PROPERTIES = new Set([
"dotAll",
"global",
"ignoreCase",
"multiline",
"sticky",
"unicode"
]);
const CONSTANT_UNARY = new Set(["!", "~", "-", "+", "void"]);
const NON_CONVERTING_UNARY = new Set(["!", "typeof", "void"]);
const NON_CONVERTING_BINARY = new Set(["&&", "||", "??", "===", "!=="]);
const IDENTITY_COMPARISON = new Set(["==", "!=", "===", "!=="]);
/**
* @param {EvaluatedValue | Node} value a value
* @returns {boolean} whether two such values compare by identity
*/
const hasIdentity = (value) =>
typeof value === "object" ||
typeof value === "function" ||
typeof value === "symbol";
/**
* Installs webpack's `evaluate`, `is_constant` and each node's `_eval`: what
* the compressor reckons an expression is worth, where it can tell.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installEvaluate = (modules) => {
const { ast, inference, utils } = modules;
const {
isArrayNode,
isConstantNode,
isDotNode,
isExpansionNode,
isFunctionNode,
isLambdaNode,
isSyntaxNode,
isObjectNode,
isPropAccessNode,
isRegExpNode,
isSymbolNode,
isSymbolRefNode,
isUnaryPrefixNode,
isWithNode
} = ast;
const isUndeclaredRef = inference.is_undeclared_ref;
const regexpSourceFix = utils.regexp_source_fix;
// What a chain evaluates to once an optional link short-circuits it.
const NULLISH = Symbol("nullish");
// The references being evaluated, so a cycle of them ends.
/** @type {Set<Node>} */
const evaluating = new Set();
/**
* @param {Node} self the node
* @returns {Node} itself, which the compressor reads as "not evaluated"
*/
function returnThis(self) {
return self;
}
/**
* @param {Node} self an expression
* @param {Compressor} compressor the compressor
* @returns {EvaluatedValue | Node} its value, or itself where it has none worth writing
*/
ast.evaluate = function evaluate(self, compressor) {
if (!compressor.option("evaluate")) return self;
const value = ast.evaluateNode(self, compressor, 1);
if (!value || value instanceof RegExp) return value;
if (
typeof value === "function" ||
typeof value === "object" ||
value === NULLISH
) {
return self;
}
// An evaluated string can print longer than the expression it replaces.
if (
typeof value === "string" &&
value.length + 2 > ast.nodeSize(self, compressor)
) {
return self;
}
return value;
};
/**
* @param {Node} self an expression
* @returns {boolean} whether it is a constant, `!0` and `-1` included
*/
ast.isConstant = function isConstant(self) {
if (isConstantNode(self)) return !isRegExpNode(self);
return (
isUnaryPrefixNode(self) &&
CONSTANT_UNARY.has(self.operator) &&
(isConstantNode(self.argument) || ast.isConstant(self.argument))
);
};
/**
* @param {Node} self a statement
* @returns {never} never: a statement has no value
*/
const evaluateForStatement = (self) => {
throw new Error(
stringTemplate(
"Cannot evaluate a statement [{file}:{line},{col}]",
/** @type {NodeToken} */ (self.startToken)
)
);
};
const evaluateForLambda = returnThis;
const evaluateForClass = returnThis;
const evaluateForNode = returnThis;
const evaluateForNew = returnThis;
/**
* @param {Node} self a constant
* @returns {ReturnType<AstHelpers["constantValue"]>} its value
*/
const evaluateForConstant = (self) => ast.constantValue(self);
/**
* @param {Node} self a BigInt literal
* @returns {bigint} its value
*/
const evaluateForBigInt = (self) =>
BigInt(/** @type {string} */ (self.value));
/**
* @param {Node} self a regular expression literal
* @param {Compressor} compressor the compressor
* @returns {RegExp | Node} the expression built, once per compressor
*/
const evaluateForRegExp = (self, compressor) => {
let evaluated = compressor.evaluated_regexps.get(
/** @type {RegExpValue} */ (self.value)
);
if (
evaluated === undefined &&
SAFE_REGEXP_SOURCE.test(/** @type {RegExpValue} */ (self.value).source)
) {
try {
const { source, flags } = /** @type {RegExpValue} */ (self.value);
evaluated = new RegExp(source, flags);
} catch (_err) {
evaluated = null;
}
compressor.evaluated_regexps.set(
/** @type {RegExpValue} */ (self.value),
evaluated
);
}
return evaluated || self;
};
/**
* @param {Node} self a template string
* @returns {string | Node} its text where it holds no substitution
*/
const evaluateForTemplateString = (self) => {
if (self.segments.length !== 1) return self;
return /** @type {string} */ (self.segments[0].value);
};
/**
* @param {Node} self a function expression
* @param {Compressor} compressor the compressor
* @returns {Node | ((() => undefined) & { node?: Node })} a stand-in function under `unsafe`, else itself
*/
const evaluateForFunction = (self, compressor) => {
if (!compressor.option("unsafe")) return self;
/** @type {(() => undefined) & { node?: Node }} */
const stand = fn;
stand.node = self;
stand.toString = () => modules.printToString(self);
return stand;
/**
* @returns {undefined}
*/
function fn() {}
};
/**
* @param {Node} self an array literal
* @param {Compressor} compressor the compressor
* @param {number} depth how deep in the evaluation it is
* @returns {EvaluatedValue | Node} its elements' values under `unsafe`, else itself
*/
const evaluateForArray = (self, compressor, depth) => {
if (!compressor.option("unsafe")) return self;
/** @type {EvaluatedArray} */
const elements = [];
for (const element of self.elements) {
// A hole reads as undefined, as terser's hole node did; the correct phase
// keeps it a hole, which `indexOf` and `lastIndexOf` skip.
if (element === null) {
if (modules.corrections !== undefined && modules.corrections.enabled) {
elements.length++;
} else {
elements.push(undefined);
}
continue;
}
const value = ast.evaluateNode(element, compressor, depth);
if (element === value) return self;
elements.push(/** @type {EvaluatedValue} */ (value));
}
return elements;
};
/**
* @param {Node} self an object literal
* @param {Compressor} compressor the compressor
* @param {number} depth how deep in the evaluation it is
* @returns {EvaluatedValue | Node} its properties' values under `unsafe`, else itself
*/
const evaluateForObject = (self, compressor, depth) => {
if (!compressor.option("unsafe")) return self;
/** @type {{ [key: string]: EvaluatedValue }} */
const value = {};
for (const property of self.properties) {
if (isExpansionNode(property)) return self;
// A key's name, or its value evaluated, which may be any value.
/** @type {EvaluatedValue | Node} */
let key = property.key;
// terser takes a computed `[a]` for the key named `a`, not for `a`'s value.
if (
isSymbolNode(key) &&
!(
property.computed &&
modules.corrections !== undefined &&
modules.corrections.enabled
)
) {
key = key.name;
} else if (isSyntaxNode(key)) {
key = ast.evaluateNode(key, compressor, depth);
if (key === property.key) return self;
}
if (
typeof (
/** @type {Record<string, BuiltInFunction | undefined>} */ (
Object.prototype
)[/** @type {string} */ (key)]
) === "function"
) {
return self;
}
if (isFunctionNode(property.value)) continue;
value[/** @type {string} */ (key)] = /** @type {EvaluatedValue} */ (
ast.evaluateNode(
/** @type {Node} */ (property.value),
compressor,
depth
)
);
if (value[/** @type {string} */ (key)] === property.value) return self;
}
return value;
};
/**
* @param {Node} self a prefix operation
* @param {Compressor} compressor the compressor
* @param {number} depth how deep in the evaluation it is
* @returns {EvaluatedValue | Node} its value, or itself
*/
const evaluateForUnaryPrefix = (self, compressor, depth) => {
/** @type {EvaluatedValue | Node} */
let expression = self.argument;
if (compressor.option("typeofs") && self.operator === "typeof") {
// A function would evaluate to an array, whose `typeof` is "object".
if (
isLambdaNode(expression) ||
(isSymbolRefNode(expression) &&
isLambdaNode(ast.fixedValue(expression)))
) {
return "function";
}
if (
(isObjectNode(expression) ||
isArrayNode(expression) ||
(isSymbolRefNode(expression) &&
(isObjectNode(ast.fixedValue(expression)) ||
isArrayNode(ast.fixedValue(expression))))) &&
!ast.hasSideEffects(expression, compressor)
) {
return "object";
}
}
if (!NON_CONVERTING_UNARY.has(self.operator)) depth++;
expression = ast.evaluateNode(expression, compressor, depth);
if (expression === self.argument) return self;
switch (self.operator) {
case "!":
return !expression;
case "typeof":
// `typeof` a regular expression differs between engines.
if (expression instanceof RegExp) return self;
return typeof expression;
case "void":
return undefined;
case "~":
return ~(/** @type {number} */ (expression));
case "-":
return -(/** @type {number} */ (expression));
case "+":
// eslint-disable-next-line no-implicit-coercion
return +(/** @type {number} */ (expression));
}
return self;
};
/**
* @param {Node} self a binary operation
* @param {Compressor} compressor the compressor
* @param {number} depth how deep in the evaluation it is
* @returns {EvaluatedValue | Node} its value, or itself
*/
const evaluateForBinary = (self, compressor, depth) => {
if (!NON_CONVERTING_BINARY.has(self.operator)) depth++;
const left = ast.evaluateNode(self.left, compressor, depth);
if (left === self.left) return self;
const right = ast.evaluateNode(self.right, compressor, depth);
if (right === self.right) return self;
if (
left !== null &&
left !== undefined &&
right !== null &&
right !== undefined &&
IDENTITY_COMPARISON.has(self.operator) &&
hasIdentity(left) &&
hasIdentity(right) &&
typeof left === typeof right
) {
return self;
}
// Neither mixes a BigInt with a number, nor divides one by zero, nor
// shifts one unsigned, nor raises one to a power (slow).
if (
(typeof left === "bigint") !== (typeof right === "bigint") ||
(typeof left === "bigint" &&
((self.operator === "/" && Number(right) === 0) ||
self.operator === ">>>" ||
self.operator === "**"))
) {
return self;
}
let result;
switch (self.operator) {
case "&&":
result = left && right;
break;
case "||":
result = left || right;
break;
case "??":
result = left !== null && left !== undefined ? left : right;
break;
case "|":
result = /** @type {number} */ (left) | /** @type {number} */ (right);
break;
case "&":
result = /** @type {number} */ (left) & /** @type {number} */ (right);
break;
case "^":
result = /** @type {number} */ (left) ^ /** @type {number} */ (right);
break;
case "+":
result = /** @type {number} */ (left) + /** @type {number} */ (right);
break;
case "*":
result = /** @type {number} */ (left) * /** @type {number} */ (right);
break;
case "**":
result = /** @type {number} */ (left) ** /** @type {number} */ (right);
break;
case "/":
result = /** @type {number} */ (left) / /** @type {number} */ (right);
break;
case "%":
result = /** @type {number} */ (left) % /** @type {number} */ (right);
break;
case "-":
result = /** @type {number} */ (left) - /** @type {number} */ (right);
break;
case "<<":
result = /** @type {number} */ (left) << /** @type {number} */ (right);
break;
case ">>":
result = /** @type {number} */ (left) >> /** @type {number} */ (right);
break;
case ">>>":
result = /** @type {number} */ (left) >>> /** @type {number} */ (right);
break;
case "==":
// eslint-disable-next-line eqeqeq
result = left == right;
break;
case "===":
result = left === right;
break;
case "!=":
// eslint-disable-next-line eqeqeq
result = left != right;
break;
case "!==":
result = left !== right;
break;
case "<":
result = /** @type {number} */ (left) < /** @type {number} */ (right);
break;
case "<=":
result = /** @type {number} */ (left) <= /** @type {number} */ (right);
break;
case ">":
result = /** @type {number} */ (left) > /** @type {number} */ (right);
break;
case ">=":
result = /** @type {number} */ (left) >= /** @type {number} */ (right);
break;
default:
return self;
}
// Inside `with`, `NaN` may name the object's property instead.
if (
typeof result === "number" &&
Number.isNaN(result) &&
compressor.find_parent(isWithNode)
) {
return self;
}
return result;
};
/**
* @param {Node} self a conditional
* @param {Compressor} compressor the compressor
* @param {number} depth how deep in the evaluation it is
* @returns {EvaluatedValue | Node} the value of the branch taken, or itself
*/
const evaluateForConditional = (self, compressor, depth) => {
const condition = ast.evaluateNode(self.test, compressor, depth);
if (condition === self.test) return self;
const node = condition ? self.consequent : self.alternate;
const value = ast.evaluateNode(
/** @type {Node} */ (node),
compressor,
depth
);
return value === node ? self : value;
};
/**
* @param {Node} self a reference
* @param {Compressor} compressor the compressor
* @param {number} depth how deep in the evaluation it is
* @returns {EvaluatedValue | Node} the value it is known to hold, or itself
*/
const evaluateForSymbolRef = (self, compressor, depth) => {
if (evaluating.has(self)) return self;
const fixed = ast.fixedValue(self);
if (!fixed) return self;
// What `void` wrote is read as `undefined`, whatever ran to write it.
if (
isUnaryPrefixNode(fixed) &&
fixed.operator === "void" &&
modules.improvements !== undefined &&
modules.improvements.enabled
) {
return undefined;
}
evaluating.add(self);
const value = ast.evaluateNode(fixed, compressor, depth);
evaluating.delete(self);
if (value === fixed) return self;
if (value && typeof value === "object") {
const { escaped } = self.definition;
if (escaped && depth > escaped) return self;
}
return value;
};
/**
* @param {Node} self an optional chain
* @param {Compressor} compressor the compressor
* @param {number} depth how deep in the evaluation it is
* @returns {EvaluatedValue | Node} its value, `undefined` where it short-circuits
*/
const evaluateForChain = (self, compressor, depth) => {
const evaluated = ast.evaluateNode(
self.expression,
compressor,
depth,
true
);
if (evaluated === NULLISH) return undefined;
return evaluated === self.expression ? self : evaluated;
};
/**
* @param {Node | false | null | undefined} node the first argument of `hasOwnProperty.call`, if any
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it may be a global nobody declared
*/
const readsUndeclared = (node, compressor) => {
let first = node && ast.evaluate(node, compressor);
first = isDotNode(first) ? /** @type {Node} */ (first).object : first;
return (
first === null ||
first === undefined ||
Boolean(
/** @type {Node} */ (first).definition &&
/** @type {Node} */ (first).definition.undeclared
)
);
};
/**
* @param {Node} self a property access
* @param {Compressor} compressor the compressor
* @param {number} depth how deep in the evaluation it is
* @param {boolean=} inChain whether it is a link of an optional chain
* @returns {EvaluatedValue | Node} the property's value, or itself
*/
const evaluateForPropAccess = (self, compressor, depth, inChain) => {
/** @type {EvaluatedValue | Node} */
let object =
(inChain ||
ast.terserProperty(self) === "length" ||
compressor.option("unsafe")) &&
ast.evaluateNode(self.object, compressor, depth + 1, inChain);
if (
inChain &&
(object === NULLISH ||
(self.optional && (object === null || object === undefined)))
) {
return NULLISH;
}
// The `.length` of a string or an array is always safe to read.
if (ast.terserProperty(self) === "length") {
if (typeof object === "string") return object.length;
if (
isArrayNode(object) &&
/** @type {Node} */ (object).elements.every(
(/** @type {Node | null} */ element) => !isExpansionNode(element)
) &&
/** @type {Node} */ (object).elements.every(
(/** @type {Node | null} */ element) =>
element === null || !ast.hasSideEffects(element, compressor)
)
) {
return /** @type {Node} */ (object).elements.length;
}
}
if (!compressor.option("unsafe")) return self;
/** @type {EvaluatedValue | Node} */
let key = ast.terserProperty(self);
if (isSyntaxNode(key)) {
key = ast.evaluateNode(key, compressor, depth);
if (key === self.property) return self;
}
const expression = self.object;
if (isUndeclaredRef(expression)) {
if (expression.name === "hasOwnProperty" && key === "call") {
const parent = compressor.parent();
const args = parent && parent.arguments;
if (readsUndeclared(args && args[0], compressor)) {
return ast.cloneNode(self);
}
} else if (readsUndeclared(false, compressor)) {
return ast.cloneNode(self);
}
if (
!compressor.is_pure_native_static_property(
expression.name,
/** @type {PropertyKey} */ (key)
)
) {
return self;
}
object = /** @type {{ [key: string]: EvaluatedValue }} */ (
/** @type {(typeof EVALUATED_GLOBALS)[keyof typeof EVALUATED_GLOBALS] | { [key: string]: EvaluatedValue }} */ (
EVALUATED_GLOBALS[
/** @type {keyof typeof EVALUATED_GLOBALS} */ (expression.name)
]
)
);
} else {
if (object instanceof RegExp) {
if (key === "source") return regexpSourceFix(object.source);
if (
key === "flags" ||
REGEXP_FLAG_PROPERTIES.has(/** @type {string} */ (key))
) {
return object[
/** @type {"flags" | "dotAll" | "global" | "ignoreCase" | "multiline" | "sticky" | "unicode"} */ (
key
)
];
}
}
// A hole the correct phase keeps reads the prototype, which `unsafe`
// takes as holding no index.
if (Array.isArray(object) && typeof key !== "symbol") {
const index = Number(key);
if (
String(index) === String(key) &&
Number.isInteger(index) &&
index >= 0 &&
index < object.length &&
!(index in object)
) {
return undefined;
}
}
if (
!object ||
object === expression ||
!Object.prototype.hasOwnProperty.call(
object,
/** @type {PropertyKey} */ (key)
)
) {
return self;
}
if (typeof object === "function") {
switch (key) {
case "name":
return /** @type {FunctionStandIn} */ (object).node.id
? /** @type {FunctionStandIn} */ (object).node.id.name
: "";
case "length":
return ast.lengthProperty(
/** @type {FunctionStandIn} */ (object).node
);
default:
return self;
}
}
}
return /** @type {{ [key: string]: EvaluatedValue }} */ (object)[
/** @type {string} */ (key)
];
};
/**
* @param {Node} self a call
* @param {Compressor} compressor the compressor
* @param {number} depth how deep in the evaluation it is
* @param {boolean=} inChain whether it is a link of an optional chain
* @returns {EvaluatedValue | Node} what a pure built-in returns, or itself
*/
const evaluateForCall = (self, compressor, depth, inChain) => {
const callee = self.callee;
if (inChain) {
const evaluated = ast.evaluateNode(callee, compressor, depth, inChain);
if (
evaluated === NULLISH ||
(self.optional && (evaluated === null || evaluated === undefined))
) {
return NULLISH;
}
}
if (!compressor.option("unsafe") || !isPropAccessNode(callee)) {
return self;
}
/** @type {EvaluatedValue | Node} */
let key = ast.terserProperty(callee);
if (isSyntaxNode(key)) {
key = ast.evaluateNode(key, compressor, depth);
if (typeof key !== "string" && typeof key !== "number") return self;
}
let value;
const object = callee.object;
if (isUndeclaredRef(object)) {
if (
readsUndeclared(
object.name === "hasOwnProperty" &&
key === "call" &&
self.arguments[0],
compressor
)
) {
return ast.cloneNode(self);
}
if (
!compressor.is_pure_native_static_fn(
object.name,
/** @type {PropertyKey} */ (key)
)
) {
return self;
}
value =
EVALUATED_GLOBALS[
/** @type {keyof typeof EVALUATED_GLOBALS} */ (object.name)
];
} else {
// The chain is not passed on: that costs exponential work.
value = ast.evaluateNode(object, compressor, depth + 1);
if (value === object || !value) return self;
if (
!compressor.is_pure_native_method(
value.constructor.name,
/** @type {PropertyKey} */ (key)
)
) {
return self;
}
}
/** @type {EvaluatedArray} */
const args = [];
for (const arg of self.arguments) {
const argument = ast.evaluateNode(arg, compressor, depth);
if (arg === argument || isLambdaNode(arg)) return self;
args.push(/** @type {EvaluatedValue} */ (argument));
}
try {
return /** @type {{ [key: string]: (...args: EvaluatedArray) => EvaluatedValue }} */ (
value
)[/** @type {string} */ (key)](...args);
} catch (_err) {
return self;
}
};
// terser's `_eval`, by its class.
const EVALUATE = kindValues({
Array: evaluateForArray,
BigInt: evaluateForBigInt,
Binary: evaluateForBinary,
Call: evaluateForCall,
Chain: evaluateForChain,
Class: evaluateForClass,
Conditional: evaluateForConditional,
Constant: evaluateForConstant,
Function: evaluateForFunction,
Lambda: evaluateForLambda,
New: evaluateForNew,
Node: evaluateForNode,
Object: evaluateForObject,
PropAccess: evaluateForPropAccess,
RegExp: evaluateForRegExp,
Statement: evaluateForStatement,
SymbolRef: evaluateForSymbolRef,
TemplateString: evaluateForTemplateString,
UnaryPrefix: evaluateForUnaryPrefix
});
/**
* terser's `_eval`: a node's value, or the node where it has none known.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @param {number=} depth how deep the evaluation is
* @param {boolean=} inChain whether an optional chain holds it
* @returns {EvaluatedValue | Node} the value
*/
ast.evaluateNode = (node, compressor, depth, inChain) =>
/** @type {AstHelpers["evaluateNode"]} */ (
/** @type {number | AstHelpers["evaluateNode"]} */ (node[EVALUATE])
)(node, compressor, depth, inChain);
/**
* Replaces the evaluator of each kind of a type with one wrapping it.
* @param {string} type the nodes, by ESTree type
* @param {(original: Evaluate) => Evaluate} wrap builds the replacement
* @returns {void}
*/
ast.wrapEvaluate = (type, wrap) => {
for (const prototype of NODE_KIND_PROTOTYPES.values()) {
if (prototype.type !== type) continue;
const slots = /** @type {Record<symbol, Evaluate>} */ (prototype);
slots[EVALUATE] = wrap(slots[EVALUATE]);
}
};
};
const BOOLEAN_UNARY = new Set(["!", "delete"]);
const BOOLEAN_BINARY = new Set([
"in",
"instanceof",
"==",
"!=",
"===",
"!==",
"<",
"<=",
">=",
">"
]);
const LAZY_OPERATORS = new Set(["&&", "||", "??"]);
const NUMERIC_UNARY = new Set(["+", "-", "~", "++", "--"]);
const NUMERIC_BINARY = new Set([
"-",
"*",
"/",
"%",
"&",
"|",
"^",
"<<",
">>",
">>>"
]);
// `>>>` throws on a BigInt, so it is left out of what keeps one.
const BIGINT_BINARY = new Set(["-", "*", "/", "%", "&", "|", "^", "<<", ">>"]);
const BITWISE_BINARY = new Set(["<<<", ">>", "<<", "&", "|", "^", "~"]);
const UNARY_SIDE_EFFECTS = new Set(["delete", "++", "--"]);
const GLOBAL_PURE_FUNCTIONS = new Set([
"Boolean",
"decodeURI",
"decodeURIComponent",
"Date",
"encodeURI",
"encodeURIComponent",
"Error",
"escape",
"EvalError",
"isFinite",
"isNaN",
"Number",
"Object",
"parseFloat",
"parseInt",
"RangeError",
"ReferenceError",
"String",
"SyntaxError",
"TypeError",
"unescape",
"URIError"
]);
/**
* Installs webpack's inference: what the compressor knows of an expression
* without running it — its type, whether it has effects or may throw, and its
* negation.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installInference = ({ ast, common, flags, nativeObjects, utils }) => {
const A = ast;
const { walk, walk_abort: walkAbort, _PURE } = ast;
const { make_sequence: makeSequence, best_of_expression: bestOfExpression } =
common;
const { has_flag: hasFlag, INLINED, UNDEFINED } = flags;
const {
is_pure_builtin_call: isPureBuiltinCall,
pure_prop_access_globals: purePropertyAccessGlobals
} = nativeObjects;
const { has_annotation: hasAnnotation } = utils;
/**
* @returns {true} true
*/
const returnTrue = () => true;
/**
* @returns {false} false
*/
const returnFalse = () => false;
/**
* @param {Node} node a node
* @returns {boolean} whether it reads a global nobody declared
*/
const isUndeclaredRef = (node) =>
A.isSymbolRefNode(node) && node.definition.undeclared;
// is_boolean
const isBooleanForNode = returnFalse;
/**
* @param {Node} self a prefix operation
* @returns {boolean} whether it yields a boolean
*/
const isBooleanForUnaryPrefix = (self) => BOOLEAN_UNARY.has(self.operator);
/**
* @param {Node} self a binary operation
* @returns {boolean} whether it yields a boolean
*/
const isBooleanForBinary = (self) =>
BOOLEAN_BINARY.has(self.operator) ||
(LAZY_OPERATORS.has(self.operator) &&
ast.isBoolean(self.left) &&
ast.isBoolean(self.right));
/**
* @param {Node} self a conditional
* @returns {boolean} whether it yields a boolean
*/
const isBooleanForConditional = (self) =>
ast.isBoolean(/** @type {Node} */ (self.consequent)) &&
ast.isBoolean(self.alternate);
/**
* @param {Node} self an assignment
* @returns {boolean} whether it yields a boolean
*/
const isBooleanForAssign = (self) =>
self.operator === "=" && ast.isBoolean(self.right);
/**
* @param {Node} self a sequence
* @returns {boolean} whether it yields a boolean
*/
const isBooleanForSequence = (self) => ast.isBoolean(ast.tailNode(self));
const isBooleanForTrue = returnTrue;
const isBooleanForFalse = returnTrue;
// is_number
const isNumberForNode = returnFalse;
const isNumberForNumber = returnTrue;
/**
* @param {Node} self a unary operation
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a number
*/
const isNumberForUnary = (self, compressor) =>
NUMERIC_UNARY.has(self.operator) && ast.isNumber(self.argument, compressor);
/**
* @param {Node} self a binary operation
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a number
*/
const isNumberForBinary = (self, compressor) => {
if (self.operator === "+") {
// One side a number and the other a number or a BigInt.
return (
(ast.isNumber(self.left, compressor) &&
ast.isNumberOrBigInt(self.right, compressor)) ||
(ast.isNumber(self.right, compressor) &&
ast.isNumberOrBigInt(self.left, compressor))
);
}
if (NUMERIC_BINARY.has(self.operator)) {
return (
ast.isNumber(self.left, compressor) ||
ast.isNumber(self.right, compressor)
);
}
return false;
};
/**
* @param {Node} self an assignment
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a number
*/
const isNumberForAssign = (self, compressor) =>
(self.operator === "=" || NUMERIC_BINARY.has(self.operator.slice(0, -1))) &&
ast.isNumber(self.right, compressor);
/**
* @param {Node} self a sequence
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a number
*/
const isNumberForSequence = (self, compressor) =>
ast.isNumber(ast.tailNode(self), compressor);
/**
* @param {Node} self a conditional
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a number
*/
const isNumberForConditional = (self, compressor) =>
ast.isNumber(/** @type {Node} */ (self.consequent), compressor) &&
ast.isNumber(self.alternate, compressor);
// is_bigint
const isBigIntForNode = returnFalse;
const isBigIntForBigInt = returnTrue;
/**
* @param {Node} self a unary operation
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a BigInt
*/
const isBigIntForUnary = (self, compressor) =>
NUMERIC_UNARY.has(self.operator) && ast.isBigInt(self.argument, compressor);
/**
* @param {Node} self a binary operation
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a BigInt
*/
const isBigIntForBinary = (self, compressor) => {
if (self.operator === "+") {
return (
(ast.isBigInt(self.left, compressor) &&
ast.isNumberOrBigInt(self.right, compressor)) ||
(ast.isBigInt(self.right, compressor) &&
ast.isNumberOrBigInt(self.left, compressor))
);
}
if (BIGINT_BINARY.has(self.operator)) {
return (
ast.isBigInt(self.left, compressor) ||
ast.isBigInt(self.right, compressor)
);
}
return false;
};
/**
* @param {Node} self an assignment
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a BigInt
*/
const isBigIntForAssign = (self, compressor) =>
(BIGINT_BINARY.has(self.operator.slice(0, -1)) || self.operator === "=") &&
ast.isBigInt(self.right, compressor);
/**
* @param {Node} self a sequence
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a BigInt
*/
const isBigIntForSequence = (self, compressor) =>
ast.isBigInt(ast.tailNode(self), compressor);
/**
* @param {Node} self a conditional
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a BigInt
*/
const isBigIntForConditional = (self, compressor) =>
ast.isBigInt(/** @type {Node} */ (self.consequent), compressor) &&
ast.isBigInt(self.alternate, compressor);
// is_number_or_bigint
const isNumberOrBigIntForNode = returnFalse;
const isNumberOrBigIntForNumber = returnTrue;
const isNumberOrBigIntForBigInt = returnTrue;
/**
* @param {Node} self a unary operation
* @returns {boolean} whether it yields a number or a BigInt
*/
const isNumberOrBigIntForUnary = (self) => NUMERIC_UNARY.has(self.operator);
/**
* @param {Node} self a binary operation
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a number or a BigInt
*/
const isNumberOrBigIntForBinary = (self, compressor) =>
self.operator === "+"
? ast.isNumberOrBigInt(self.left, compressor) &&
ast.isNumberOrBigInt(self.right, compressor)
: BIGINT_BINARY.has(self.operator);
/**
* @param {Node} self an assignment
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a number or a BigInt
*/
const isNumberOrBigIntForAssign = (self, compressor) =>
BIGINT_BINARY.has(self.operator.slice(0, -1)) ||
(self.operator === "=" && ast.isNumberOrBigInt(self.right, compressor));
/**
* @param {Node} self a sequence
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a number or a BigInt
*/
const isNumberOrBigIntForSequence = (self, compressor) =>
ast.isNumberOrBigInt(ast.tailNode(self), compressor);
/**
* @param {Node} self a conditional
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a number or a BigInt
*/
const isNumberOrBigIntForConditional = (self, compressor) =>
ast.isNumberOrBigInt(/** @type {Node} */ (self.consequent), compressor) &&
ast.isNumberOrBigInt(self.alternate, compressor);
// is_32_bit_integer
const is32BitIntegerForNode = returnFalse;
/**
* @param {Node & { value: number }} self a number
* @returns {boolean} whether it fits in 32 bits
*/
const is32BitIntegerForNumber = (self) => self.value === (self.value | 0);
/**
* @param {Node} self a prefix operation
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a 32-bit integer
*/
const is32BitIntegerForUnaryPrefix = (self, compressor) => {
if (self.operator === "~") return ast.isNumber(self.argument, compressor);
if (self.operator === "+") {
return ast.is32BitInteger(self.argument, compressor);
}
return false;
};
/**
* @param {Node} self a binary operation
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a 32-bit integer
*/
const is32BitIntegerForBinary = (self, compressor) =>
BITWISE_BINARY.has(self.operator) &&
(ast.isNumber(self.left, compressor) ||
ast.isNumber(self.right, compressor));
// is_string
const isStringForNode = returnFalse;
const isStringForString = returnTrue;
const isStringForTemplateString = returnTrue;
/**
* @param {Node} self a prefix operation
* @returns {boolean} whether it yields a string
*/
const isStringForUnaryPrefix = (self) => self.operator === "typeof";
/**
* @param {Node} self a binary operation
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a string
*/
const isStringForBinary = (self, compressor) =>
self.operator === "+" &&
(ast.isString(self.left, compressor) ||
ast.isString(self.right, compressor));
/**
* @param {Node} self an assignment
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a string
*/
const isStringForAssign = (self, compressor) =>
(self.operator === "=" || self.operator === "+=") &&
ast.isString(self.right, compressor);
/**
* @param {Node} self a sequence
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a string
*/
const isStringForSequence = (self, compressor) =>
ast.isString(ast.tailNode(self), compressor);
/**
* @param {Node} self a conditional
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it yields a string
*/
const isStringForConditional = (self, compressor) =>
ast.isString(/** @type {Node} */ (self.consequent), compressor) &&
ast.isString(self.alternate, compressor);
/**
* terser's `is_undefined`.
* @param {Node} node an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it is `undefined`
*/
const isUndefined = (node, compressor) =>
Boolean(hasFlag(node, UNDEFINED)) ||
A.isUndefinedNode(node) ||
(A.isUnaryPrefixNode(node) &&
node.operator === "void" &&
!ast.hasSideEffects(node.argument, compressor));
/**
* terser's `is_nullish`: whether an expression is `null` or `undefined`, or
* short-circuits into it through an optional link.
* @param {Node} node an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it is nullish
*/
const isNullish = (node, compressor) =>
isNullOrUndefined(node, compressor) ||
isNullishShortCircuited(node, compressor);
/**
* @param {Node} node an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it is explicitly `null` or `undefined`
*/
const isNullOrUndefined = (node, compressor) => {
let fixed;
return (
A.isNullNode(node) ||
isUndefined(node, compressor) ||
(A.isSymbolRefNode(node) &&
A.isSyntaxNode((fixed = node.definition.fixed)) &&
isNullish(fixed, compressor))
);
};
/**
* @param {Node} node an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} whether an optional link in it short-circuits
*/
const isNullishShortCircuited = (node, compressor) => {
if (A.isPropAccessNode(node) || A.isCallNode(node)) {
const inner = A.isCallNode(node) ? node.callee : node.object;
return (
(node.optional && isNullOrUndefined(inner, compressor)) ||
isNullishShortCircuited(inner, compressor)
);
}
if (A.isChainNode(node)) {
return isNullishShortCircuited(node.expression, compressor);
}
return false;
};
/**
* @param {(Node | null)[]} list nodes, null for a hole
* @param {Compressor} compressor the compressor
* @returns {boolean} whether any has effects
*/
const anyHasSideEffects = (list, compressor) => {
for (let i = list.length; --i >= 0;) {
const item = list[i];
// A hole, null in an array's list, has none.
if (item !== null && ast.hasSideEffects(item, compressor)) return true;
}
return false;
};
// has_side_effects
const hasSideEffectsForNode = returnTrue;
const hasSideEffectsForEmptyStatement = returnFalse;
const hasSideEffectsForConstant = returnFalse;
const hasSideEffectsForThis = returnFalse;
/**
* @param {Node} self a node holding a statement list, a case's its `consequent`
* @param {Compressor} compressor the compressor
* @returns {boolean} whether any statement in it has effects
*/
function bodyHasSideEffects(self, compressor) {
return anyHasSideEffects(
/** @type {(Node | null)[]} */ (A.bodyOf(self)),
compressor
);
}
const hasSideEffectsForBlock = bodyHasSideEffects;
/**
* @param {Node} self a call
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it has effects
*/
const hasSideEffectsForCall = (self, compressor) => {
if (
!ast.isCalleePure(self, compressor) &&
(!ast.isCallPure(self.callee, compressor) ||
ast.hasSideEffects(self.callee, compressor))
) {
return true;
}
return anyHasSideEffects(self.arguments, compressor);
};
/**
* @param {Node} self a switch
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it has effects
*/
const hasSideEffectsForSwitch = (self, compressor) =>
ast.hasSideEffects(self.discriminant, compressor) ||
anyHasSideEffects(self.cases, compressor);
/**
* @param {Node} self a case
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it has effects
*/
const hasSideEffectsForCase = (self, compressor) =>
ast.hasSideEffects(self.test, compressor) ||
anyHasSideEffects(
/** @type {(Node | null)[]} */ (self.consequent),
compressor
);
/**
* @param {Node} self a try statement
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it has effects
*/
const hasSideEffectsForTry = (self, compressor) =>
ast.hasSideEffects(self.block, compressor) ||
(self.handler && ast.hasSideEffects(self.handler, compressor)) ||
(self.finalizer && ast.hasSideEffects(self.finalizer, compressor));
/**
* @param {Node} self an if statement
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it has effects
*/
const hasSideEffectsForIf = (self, compressor) =>
ast.hasSideEffects(self.test, compressor) ||
(self.consequent &&
ast.hasSideEffects(/** @type {Node} */ (self.consequent), compressor)) ||
(self.alternate && ast.hasSideEffects(self.alternate, compressor));
const hasSideEffectsForImportMeta = returnFalse;
/**
* @param {Node} self a dynamic import
* @returns {boolean} whether it has effects: `import.source()` compiles only
*/
const hasSideEffectsForDynamicImport = (self) => self.phase !== "source";
/**
* @param {Node} self a labeled statement
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its body has effects
*/
const hasSideEffectsForLabeledStatement = (self, compressor) =>
ast.hasSideEffects(/** @type {Node} */ (self.body), compressor);
/**
* @param {Node} self an expression statement
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its expression has effects
*/
const hasSideEffectsForSimpleStatement = (self, compressor) =>
ast.hasSideEffects(self.expression, compressor);
const hasSideEffectsForLambda = returnFalse;
/**
* @param {Node} self a class
* @param {Compressor} compressor the compressor
* @returns {boolean} whether defining it has effects
*/
const hasSideEffectsForClass = (self, compressor) => {
if (self.superClass && ast.hasSideEffects(self.superClass, compressor)) {
return true;
}
return anyHasSideEffects(
/** @type {(Node | null)[]} */ (/** @type {Node} */ (self.body).body),
compressor
);
};
const hasSideEffectsForClassStaticBlock = bodyHasSideEffects;
/**
* @param {Node} self a binary operation
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it has effects
*/
const hasSideEffectsForBinary = (self, compressor) =>
ast.hasSideEffects(self.left, compressor) ||
ast.hasSideEffects(self.right, compressor);
const hasSideEffectsForAssign = returnTrue;
/**
* @param {Node} self a conditional
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it has effects
*/
const hasSideEffectsForConditional = (self, compressor) =>
ast.hasSideEffects(self.test, compressor) ||
ast.hasSideEffects(/** @type {Node} */ (self.consequent), compressor) ||
ast.hasSideEffects(self.alternate, compressor);
/**
* @param {Node} self a unary operation
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it has effects
*/
const hasSideEffectsForUnary = (self, compressor) =>
UNARY_SIDE_EFFECTS.has(self.operator) ||
ast.hasSideEffects(self.argument, compressor);
/**
* @param {Node} self a reference
* @param {Compressor} compressor the compressor
* @returns {boolean} whether reading it may throw a ReferenceError
*/
const hasSideEffectsForSymbolRef = (self, compressor) =>
!ast.isDeclared(self, compressor) &&
!purePropertyAccessGlobals.has(self.name);
const hasSideEffectsForSymbolClassProperty = returnFalse;
const hasSideEffectsForSymbolDeclaration = returnFalse;
/**
* @param {Node} self an object literal
* @param {Compressor} compressor the compressor
* @returns {boolean} whether building it has effects
*/
const hasSideEffectsForObject = (self, compressor) =>
anyHasSideEffects(self.properties, compressor);
/**
* @param {Node & { value: Node }} self a property
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its key or value has effects
*/
const hasSideEffectsForObjectKeyVal = (self, compressor) =>
(self.computed && ast.hasSideEffects(self.key, compressor)) ||
(self.value && ast.hasSideEffects(self.value, compressor));
/**
* @param {Node} self a class field
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its key or static value has effects
*/
function fieldHasSideEffects(self, compressor) {
return (
(self.computed && ast.hasSideEffects(self.key, compressor)) ||
(self.static &&
/** @type {Node} */ (self.value) &&
ast.hasSideEffects(/** @type {Node} */ (self.value), compressor))
);
}
const hasSideEffectsForClassProperty = fieldHasSideEffects;
const hasSideEffectsForClassPrivateProperty = fieldHasSideEffects;
/**
* @param {Node} self a method or accessor
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its key has effects
*/
function keyHasSideEffects(self, compressor) {
return self.computed && ast.hasSideEffects(self.key, compressor);
}
const hasSideEffectsForPrivateMethod = keyHasSideEffects;
const hasSideEffectsForPrivateGetter = keyHasSideEffects;
const hasSideEffectsForPrivateSetter = keyHasSideEffects;
const hasSideEffectsForConciseMethod = keyHasSideEffects;
const hasSideEffectsForObjectGetter = keyHasSideEffects;
const hasSideEffectsForObjectSetter = keyHasSideEffects;
/**
* @param {Node} self an array literal
* @param {Compressor} compressor the compressor
* @returns {boolean} whether building it has effects
*/
const hasSideEffectsForArray = (self, compressor) =>
anyHasSideEffects(self.elements, compressor);
/**
* @param {Node} self a property read
* @param {Compressor} compressor the compressor
* @returns {boolean} whether reading it has effects
*/
const hasSideEffectsForDot = (self, compressor) => {
if (isNullish(self, compressor)) {
return ast.hasSideEffects(self.object, compressor);
}
if (!self.optional && ast.mayThrowOnAccess(self.object, compressor)) {
return true;
}
return ast.hasSideEffects(self.object, compressor);
};
/**
* @param {Node} self a computed property read
* @param {Compressor} compressor the compressor
* @returns {boolean} whether reading it has effects
*/
const hasSideEffectsForSub = (self, compressor) => {
if (isNullish(self, compressor)) {
return ast.hasSideEffects(self.object, compressor);
}
if (!self.optional && ast.mayThrowOnAccess(self.object, compressor)) {
return true;
}
const property = ast.hasSideEffects(self.property, compressor);
// `?.` makes the property's effects conditional.
if (property && self.optional) return true;
return property || ast.hasSideEffects(self.object, compressor);
};
/**
* @param {Node} self an optional chain
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it has effects
*/
const hasSideEffectsForChain = (self, compressor) =>
ast.hasSideEffects(self.expression, compressor);
/**
* @param {Node} self a sequence
* @param {Compressor} compressor the compressor
* @returns {boolean} whether any expression in it has effects
*/
const hasSideEffectsForSequence = (self, compressor) =>
anyHasSideEffects(self.expressions, compressor);
/**
* @param {Node} self a declaration
* @param {Compressor} compressor the compressor
* @returns {boolean} whether any definition in it has effects
*/
const hasSideEffectsForDefinitions = (self, compressor) =>
anyHasSideEffects(self.declarations, compressor);
/**
* @param {Node} self a definition
* @returns {boolean} whether it assigns a value
*/
const hasSideEffectsForVarDef = (self) =>
self.init !== null && self.init !== undefined;
const hasSideEffectsForTemplateSegment = returnFalse;
/**
* @param {Node} self a template string
* @param {Compressor} compressor the compressor
* @returns {boolean} whether any part of it has effects
*/
const hasSideEffectsForTemplateString = (self, compressor) =>
anyHasSideEffects(self.segments, compressor);
/**
* @param {(Node | null)[]} list nodes, null for a hole
* @param {Compressor} compressor the compressor
* @returns {boolean} whether any may throw
*/
const anyMayThrow = (list, compressor) => {
for (let i = list.length; --i >= 0;) {
const item = list[i];
// A hole, null in an array's list, cannot throw.
if (item !== null && ast.mayThrow(item, compressor)) return true;
}
return false;
};
// may_throw
const mayThrowForNode = returnTrue;
const mayThrowForConstant = returnFalse;
const mayThrowForEmptyStatement = returnFalse;
const mayThrowForLambda = returnFalse;
const mayThrowForSymbolDeclaration = returnFalse;
const mayThrowForThis = returnFalse;
const mayThrowForImportMeta = returnFalse;
/**
* @param {Node} self a class
* @param {Compressor} compressor the compressor
* @returns {boolean} whether defining it may throw
*/
const mayThrowForClass = (self, compressor) => {
if (self.superClass && ast.mayThrow(self.superClass, compressor)) {
return true;
}
return anyMayThrow(
/** @type {(Node | null)[]} */ (/** @type {Node} */ (self.body).body),
compressor
);
};
/**
* @param {Node} self a node holding a statement list, a case's its `consequent`
* @param {Compressor} compressor the compressor
* @returns {boolean} whether any statement in it may throw
*/
function bodyMayThrow(self, compressor) {
return anyMayThrow(
/** @type {(Node | null)[]} */ (A.bodyOf(self)),
compressor
);
}
const mayThrowForClassStaticBlock = bodyMayThrow;
/**
* @param {Node} self an array literal
* @param {Compressor} compressor the compressor
* @returns {boolean} whether building it may throw
*/
const mayThrowForArray = (self, compressor) =>
anyMayThrow(self.elements, compressor);
/**
* @param {Node} self an assignment
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it may throw
*/
const mayThrowForAssign = (self, compressor) => {
if (ast.mayThrow(self.right, compressor)) return true;
if (
!compressor.has_directive("use strict") &&
self.operator === "=" &&
A.isSymbolRefNode(self.left)
) {
return false;
}
return ast.mayThrow(self.left, compressor);
};
/**
* @param {Node} self a binary operation
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it may throw
*/
const mayThrowForBinary = (self, compressor) =>
ast.mayThrow(self.left, compressor) || ast.mayThrow(self.right, compressor);
const mayThrowForBlock = bodyMayThrow;
/**
* @param {Node} self a call
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it may throw
*/
const mayThrowForCall = (self, compressor) => {
if (isNullish(self, compressor)) return false;
if (anyMayThrow(self.arguments, compressor)) return true;
if (ast.isCalleePure(self, compressor)) return false;
if (ast.mayThrow(self.callee, compressor)) return true;
return (
!A.isLambdaNode(self.callee) ||
anyMayThrow(
/** @type {(Node | null)[]} */ (
/** @type {Node} */ (self.callee.body).body
),
compressor
)
);
};
/**
* @param {Node} self a case
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it may throw
*/
const mayThrowForCase = (self, compressor) =>
ast.mayThrow(self.test, compressor) ||
anyMayThrow(/** @type {(Node | null)[]} */ (self.consequent), compressor);
/**
* @param {Node} self a conditional
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it may throw
*/
const mayThrowForConditional = (self, compressor) =>
ast.mayThrow(self.test, compressor) ||
ast.mayThrow(/** @type {Node} */ (self.consequent), compressor) ||
ast.mayThrow(self.alternate, compressor);
/**
* @param {Node} self a declaration
* @param {Compressor} compressor the compressor
* @returns {boolean} whether any definition in it may throw
*/
const mayThrowForDefinitions = (self, compressor) =>
anyMayThrow(self.declarations, compressor);
/**
* @param {Node} self an if statement
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it may throw
*/
const mayThrowForIf = (self, compressor) =>
ast.mayThrow(self.test, compressor) ||
(self.consequent &&
ast.mayThrow(/** @type {Node} */ (self.consequent), compressor)) ||
(self.alternate && ast.mayThrow(self.alternate, compressor));
/**
* @param {Node} self a labeled statement
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its body may throw
*/
const mayThrowForLabeledStatement = (self, compressor) =>
ast.mayThrow(/** @type {Node} */ (self.body), compressor);
/**
* @param {Node} self an object literal
* @param {Compressor} compressor the compressor
* @returns {boolean} whether building it may throw
*/
const mayThrowForObject = (self, compressor) =>
anyMayThrow(self.properties, compressor);
/**
* terser's grouping, kept: the key's check decides only whether the value
* is asked.
* @param {Node} self a property
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it may throw
*/
const mayThrowForObjectKeyVal = (self, compressor) =>
(self.computed && ast.mayThrow(self.key, compressor)) || self.value
? ast.mayThrow(/** @type {Node} */ (self.value), compressor)
: false;
/**
* @param {Node} self a class field
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its key or static value may throw
*/
function fieldMayThrow(self, compressor) {
return (
(self.computed && ast.mayThrow(self.key, compressor)) ||
(self.static &&
/** @type {Node} */ (self.value) &&
ast.mayThrow(/** @type {Node} */ (self.value), compressor))
);
}
const mayThrowForClassProperty = fieldMayThrow;
const mayThrowForClassPrivateProperty = fieldMayThrow;
/**
* @param {Node} self a method or accessor
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its key may throw
*/
function keyMayThrow(self, compressor) {
return self.computed && ast.mayThrow(self.key, compressor);
}
const mayThrowForConciseMethod = keyMayThrow;
const mayThrowForObjectGetter = keyMayThrow;
const mayThrowForObjectSetter = keyMayThrow;
const mayThrowForPrivateMethod = returnFalse;
const mayThrowForPrivateGetter = returnFalse;
const mayThrowForPrivateSetter = returnFalse;
/**
* @param {Node} self a return
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its value may throw
*/
const mayThrowForReturn = (self, compressor) =>
self.argument && ast.mayThrow(self.argument, compressor);
/**
* @param {Node} self a sequence
* @param {Compressor} compressor the compressor
* @returns {boolean} whether any expression in it may throw
*/
const mayThrowForSequence = (self, compressor) =>
anyMayThrow(self.expressions, compressor);
/**
* @param {Node} self an expression statement
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its expression may throw
*/
const mayThrowForSimpleStatement = (self, compressor) =>
ast.mayThrow(self.expression, compressor);
/**
* @param {Node} self a property read
* @param {Compressor} compressor the compressor
* @returns {boolean} whether reading it may throw
*/
const mayThrowForDot = (self, compressor) => {
if (isNullish(self, compressor)) return false;
return (
(!self.optional && ast.mayThrowOnAccess(self.object, compressor)) ||
ast.mayThrow(self.object, compressor)
);
};
/**
* @param {Node} self a computed property read
* @param {Compressor} compressor the compressor
* @returns {boolean} whether reading it may throw
*/
const mayThrowForSub = (self, compressor) => {
if (isNullish(self, compressor)) return false;
return (
(!self.optional && ast.mayThrowOnAccess(self.object, compressor)) ||
ast.mayThrow(self.object, compressor) ||
ast.mayThrow(self.property, compressor)
);
};
/**
* @param {Node} self an optional chain
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it may throw
*/
const mayThrowForChain = (self, compressor) =>
ast.mayThrow(self.expression, compressor);
/**
* @param {Node} self a switch
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it may throw
*/
const mayThrowForSwitch = (self, compressor) =>
ast.mayThrow(self.discriminant, compressor) ||
anyMayThrow(self.cases, compressor);
/**
* @param {Node} self a reference
* @param {Compressor} compressor the compressor
* @returns {boolean} whether reading it may throw a ReferenceError
*/
const mayThrowForSymbolRef = (self, compressor) =>
!ast.isDeclared(self, compressor) &&
!purePropertyAccessGlobals.has(self.name);
const mayThrowForSymbolClassProperty = returnFalse;
/**
* terser's grouping, kept: a catch decides alone, else the body or finally.
* @param {Node} self a try statement
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it may throw
*/
const mayThrowForTry = (self, compressor) =>
self.handler
? ast.mayThrow(self.handler, compressor)
: ast.mayThrow(self.block, compressor) ||
(self.finalizer && ast.mayThrow(self.finalizer, compressor));
/**
* @param {Node} self a unary operation
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it may throw
*/
const mayThrowForUnary = (self, compressor) => {
if (self.operator === "typeof" && A.isSymbolRefNode(self.argument)) {
return false;
}
return ast.mayThrow(self.argument, compressor);
};
/**
* @param {Node} self a definition
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its value may throw
*/
const mayThrowForVarDef = (self, compressor) => {
if (!self.init) return false;
return ast.mayThrow(self.init, compressor);
};
/**
* terser's `all_refs_local`: whether every name a function or class reads is
* its own, or, given a scope, one that scope resolves the same way.
* @param {Node} self the function or class
* @param {Scope=} scope the scope it would move to
* @returns {boolean | "f"} true, false, or "f" where the scope resolves them
*/
function allRefsLocal(self, scope) {
/** @type {boolean | "f"} */
let result = true;
const ownScope = self.block_scope;
walk(self, (/** @type {Node} */ node) => {
if (A.isSymbolRefNode(node)) {
if (hasFlag(self, INLINED)) {
result = false;
return walkAbort;
}
const definition = node.definition;
if (
ownScope.enclosed.includes(definition) &&
ownScope.getBinding(definition.name) === undefined
) {
if (scope) {
const scopeDefinition = scope.findVariable(node.name);
if (
definition.undeclared
? !scopeDefinition
: scopeDefinition === definition
) {
result = "f";
return true;
}
}
result = false;
return walkAbort;
}
return true;
}
if (A.isThisNode(node) && A.isArrowNode(self)) {
result = false;
return walkAbort;
}
});
return result;
}
// is_constant_expression
const constantExpressionForNode = returnFalse;
const constantExpressionForConstant = returnTrue;
/**
* @param {Node} self a class
* @param {Scope=} scope the scope it would move to
* @returns {boolean | "f"} whether it is constant
*/
const constantExpressionForClass = (self, scope) => {
if (self.superClass && !ast.isConstantExpression(self.superClass, scope)) {
return false;
}
for (const property of /** @type {Node[]} */ (
/** @type {Node} */ (self.body).body
)) {
if (property.computed && !ast.isConstantExpression(property.key, scope)) {
return false;
}
if (
property.static &&
property.value &&
!ast.isConstantExpression(/** @type {Node} */ (property.value), scope)
) {
return false;
}
if (A.isClassStaticBlockNode(property)) return false;
}
return allRefsLocal(self, scope);
};
const constantExpressionForLambda = allRefsLocal;
/**
* @param {Node} self a unary operation
* @returns {boolean | "f"} whether it is constant
*/
const constantExpressionForUnary = (self) =>
ast.isConstantExpression(self.argument);
/**
* @param {Node} self a binary operation
* @returns {boolean | "f"} whether it is constant
*/
const constantExpressionForBinary = (self) =>
ast.isConstantExpression(self.left) && ast.isConstantExpression(self.right);
/**
* @param {Node} self an array literal
* @returns {boolean} whether it is constant
*/
const constantExpressionForArray = (self) =>
self.elements.every(
(/** @type {Node | null} */ element) =>
element === null || ast.isConstantExpression(element)
);
/**
* @param {Node} self an object literal
* @returns {boolean} whether it is constant
*/
const constantExpressionForObject = (self) =>
self.properties.every((/** @type {Node} */ property) =>
ast.isConstantExpression(property)
);
/**
* @param {Node} self a property
* @returns {boolean} whether it is constant
*/
const constantExpressionForObjectProperty = (self) =>
Boolean(
!A.isSyntaxNode(A.terserKey(self.key)) &&
self.value &&
ast.isConstantExpression(/** @type {Node} */ (self.value))
);
/**
* @param {Node} self an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} whether reading a property of it may throw
*/
ast.mayThrowOnAccess = function mayThrowOnAccess(self, compressor) {
return (
!compressor.option("pure_getters") || ast.dotThrows(self, compressor)
);
};
/**
* @param {Compressor} compressor the compressor
* @returns {boolean} whether `pure_getters` is "strict"
*/
const isStrict = (compressor) =>
/strict/.test(/** @type {string} */ (compressor.option("pure_getters")));
// _dot_throw
/**
* @param {Node} self a node
* @param {Compressor} compressor the compressor
* @returns {boolean} whether reading a property of its value can throw
*/
const dotThrowsForNode = (self, compressor) => isStrict(compressor);
const dotThrowsForNull = returnTrue;
const dotThrowsForUndefined = returnTrue;
const dotThrowsForConstant = returnFalse;
const dotThrowsForArray = returnFalse;
/**
* @param {Node} self an object literal
* @param {Compressor} compressor the compressor
* @returns {boolean} whether reading a property may run a getter
*/
const dotThrowsForObject = (self, compressor) => {
if (!isStrict(compressor)) return false;
for (let i = self.properties.length; --i >= 0;) {
if (ast.dotThrows(self.properties[i], compressor)) return true;
}
return false;
};
// Classes are trusted not to put throwing static getters in the way.
const dotThrowsForClass = returnFalse;
const dotThrowsForObjectProperty = returnFalse;
const dotThrowsForObjectGetter = returnTrue;
/**
* @param {Node} self a spread
* @param {Compressor} compressor the compressor
* @returns {boolean} whether what it spreads may throw on access
*/
const dotThrowsForExpansion = (self, compressor) =>
ast.dotThrows(self.argument, compressor);
const dotThrowsForFunction = returnFalse;
const dotThrowsForArrow = returnFalse;
const dotThrowsForUnaryPostfix = returnFalse;
/**
* @param {Node} self a prefix operation
* @returns {boolean} whether it yields `undefined`
*/
const dotThrowsForUnaryPrefix = (self) => self.operator === "void";
/**
* @param {Node} self a binary operation
* @param {Compressor} compressor the compressor
* @returns {boolean} whether either side it may yield throws on access
*/
const dotThrowsForBinary = (self, compressor) =>
LAZY_OPERATORS.has(self.operator) &&
(ast.dotThrows(self.left, compressor) ||
ast.dotThrows(self.right, compressor));
/**
* @param {Node} self an assignment
* @param {Compressor} compressor the compressor
* @returns {boolean} whether the value it yields may throw on access
*/
const dotThrowsForAssign = (self, compressor) => {
if (self.logical) return true;
return self.operator === "=" && ast.dotThrows(self.right, compressor);
};
/**
* @param {Node} self a conditional
* @param {Compressor} compressor the compressor
* @returns {boolean} whether either branch may throw on access
*/
const dotThrowsForConditional = (self, compressor) =>
ast.dotThrows(/** @type {Node} */ (self.consequent), compressor) ||
ast.dotThrows(self.alternate, compressor);
/**
* @param {Node} self a property read
* @param {Compressor} compressor the compressor
* @returns {boolean} whether the value read may throw on access
*/
const dotThrowsForDot = (self, compressor) => {
if (!isStrict(compressor)) return false;
if (self.property.name === "prototype") {
return !(A.isFunctionNode(self.object) || A.isClassNode(self.object));
}
return true;
};
/**
* @param {Node} self an optional chain
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its value may throw on access
*/
const dotThrowsForChain = (self, compressor) =>
ast.dotThrows(self.expression, compressor);
/**
* @param {Node} self a sequence
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its value may throw on access
*/
const dotThrowsForSequence = (self, compressor) =>
ast.dotThrows(ast.tailNode(self), compressor);
/**
* @param {Node} self a reference
* @param {Compressor} compressor the compressor
* @returns {boolean} whether its value may throw on access
*/
const dotThrowsForSymbolRef = (self, compressor) => {
if (
self.name === "arguments" &&
self.scope !== undefined &&
A.isLambdaNode(self.scope.node)
) {
return false;
}
if (hasFlag(self, UNDEFINED)) return true;
if (!isStrict(compressor)) return false;
if (isUndeclaredRef(self) && ast.isDeclared(self, compressor)) return false;
if (ast.isImmutable(self)) return false;
const fixed = ast.fixedValue(self);
return !fixed || ast.dotThrows(fixed, compressor);
};
/**
* @param {Node} expression an expression
* @returns {Node} `!expression`
*/
const basicNegation = (expression) =>
makeNode(A.UnaryPrefixNode, expression, {
operator: "!",
argument: expression
});
/**
* @param {Node} original the expression negated
* @param {Node} alternative its negation rewritten
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node} the shorter negation
*/
const bestNegation = (original, alternative, firstInStatement) => {
const negated = basicNegation(original);
if (firstInStatement) {
const statement = makeNode(A.SimpleStatementNode, alternative, {
expression: alternative
});
return bestOfExpression(negated, statement) === statement
? alternative
: negated;
}
return bestOfExpression(negated, alternative);
};
/**
* @param {Node} self an expression
* @returns {Node} `!` it
*/
function negateBasic(self) {
return basicNegation(self);
}
// negate
const negateForNode = negateBasic;
/**
* @returns {never} never: a statement has no value to negate
*/
const negateForStatement = () => {
throw new Error("Cannot negate a statement");
};
const negateForFunction = negateBasic;
const negateForClass = negateBasic;
const negateForArrow = negateBasic;
/**
* @param {Node} self a prefix operation
* @returns {Node} its negation
*/
const negateForUnaryPrefix = (self) => {
if (self.operator === "!") return self.argument;
return basicNegation(self);
};
/**
* @param {Node} self a sequence
* @param {Compressor} compressor the compressor
* @returns {Node} it, its last expression negated
*/
const negateForSequence = (self, compressor) => {
const expressions = [...self.expressions];
expressions.push(
ast.negate(/** @type {Node} */ (expressions.pop()), compressor)
);
return makeSequence(self, expressions);
};
/**
* @param {Node} self a conditional
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node} its negation
*/
const negateForConditional = (self, compressor, firstInStatement) => {
const negated = ast.cloneNode(self);
negated.consequent = ast.negate(
/** @type {Node} */ (negated.consequent),
compressor
);
negated.alternate = ast.negate(negated.alternate, compressor);
return bestNegation(self, negated, firstInStatement);
};
/**
* @param {Node} self a binary operation
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node} its negation
*/
const negateForBinary = (self, compressor, firstInStatement) => {
const negated = ast.cloneNode(self);
const operator = self.operator;
if (compressor.option("unsafe_comps")) {
switch (operator) {
case "<=":
negated.operator = ">";
return negated;
case "<":
negated.operator = ">=";
return negated;
case ">=":
negated.operator = "<";
return negated;
case ">":
negated.operator = "<=";
return negated;
}
}
switch (operator) {
case "==":
negated.operator = "!=";
return negated;
case "!=":
negated.operator = "==";
return negated;
case "===":
negated.operator = "!==";
return negated;
case "!==":
negated.operator = "===";
return negated;
case "&&":
case "||":
negated.operator = operator === "&&" ? "||" : "&&";
negated.left = ast.negate(negated.left, compressor, firstInStatement);
negated.right = ast.negate(negated.right, compressor);
return bestNegation(self, negated, firstInStatement);
}
return basicNegation(self);
};
/**
* @param {Node} expression an expression
* @returns {Node} `~expression`
*/
const basicBitwiseNegation = (expression) =>
makeNode(A.UnaryPrefixNode, expression, {
operator: "~",
argument: expression
});
// bitwise_negate
/**
* @param {Node} self an expression
* @returns {Node} `~` it
*/
const bitwiseNegateForNode = (self) => basicBitwiseNegation(self);
/**
* @param {Node} self a number
* @returns {Node} its complement, where no longer than `~` it
*/
const bitwiseNegateForNumber = (self) => {
const negated = ~(/** @type {number} */ (self.value));
if (
negated.toString().length >
/** @type {number} */ (self.value).toString().length
) {
return basicBitwiseNegation(self);
}
return makeNode(A.NumberNode, self, { value: negated });
};
/**
* @param {Node} self a prefix operation
* @param {Compressor} compressor the compressor
* @param {boolean=} in32BitContext whether its value is read as 32 bits
* @returns {Node} its complement
*/
const bitwiseNegateForUnaryPrefix = (self, compressor, in32BitContext) => {
if (
self.operator === "~" &&
(ast.is32BitInteger(self.argument, compressor) ||
(in32BitContext !== null && in32BitContext !== undefined
? in32BitContext
: compressor.in_32_bit_context()))
) {
return self.argument;
}
return basicBitwiseNegation(self);
};
/**
* @param {Node} self a call
* @param {Compressor} compressor the compressor
* @returns {boolean} whether the function it calls is pure
*/
ast.isCalleePure = function isCalleePure(self, compressor) {
if (compressor.option("unsafe")) {
const expression = self.callee;
const inner = /** @type {Node} */ (A.expressionOf(expression));
// What the argument evaluates to, read as a node for its definition.
/** @type {Node | null | undefined} */
let firstArgument;
if (
A.isSymbolNode(inner) &&
inner.name === "hasOwnProperty" &&
((firstArgument =
self.arguments &&
self.arguments[0] &&
/** @type {Node} */ (ast.evaluate(self.arguments[0], compressor))) ===
null ||
firstArgument === undefined ||
(firstArgument.definition && firstArgument.definition.undeclared))
) {
return false;
}
if (
isUndeclaredRef(expression) &&
GLOBAL_PURE_FUNCTIONS.has(expression.name)
) {
return true;
}
if (isPureBuiltinCall(compressor, self)) return true;
} else if (
compressor.option("builtins_pure") &&
isPureBuiltinCall(compressor, self)
) {
return true;
}
if (A.isNewNode(self) && compressor.option("pure_new")) return true;
if (compressor.option("side_effects") && hasAnnotation(self, _PURE)) {
return true;
}
return !compressor.pure_funcs(self);
};
const isCallPureForNode = returnFalse;
/**
* @param {Node} self a method read
* @param {Compressor} compressor the compressor
* @returns {boolean | undefined} whether calling it is pure
*/
const isCallPureForDot = (self, compressor) => {
if (!compressor.option("unsafe")) return;
const expression = self.object;
let nativeObject;
if (A.isArrayNode(expression)) {
nativeObject = "Array";
} else if (ast.isBoolean(expression)) {
nativeObject = "Boolean";
} else if (ast.isNumber(expression, compressor)) {
nativeObject = "Number";
} else if (A.isRegExpNode(expression)) {
nativeObject = "RegExp";
} else if (ast.isString(expression, compressor)) {
nativeObject = "String";
} else if (!ast.mayThrowOnAccess(self, compressor)) {
nativeObject = "Object";
}
return (
nativeObject !== undefined &&
compressor.is_pure_native_method(nativeObject, self.property.name)
);
};
/**
* @param {Node | null | undefined} thing a statement
* @returns {Node | null | false | undefined} what ends it, if it always ends
*/
const statementAborts = (thing) => thing && ast.aborts(thing);
/**
* @param {Node[]} statements a statement list
* @returns {Node | null} the statement that ends it, if one does
*/
const listAborts = (statements) => {
for (let i = 0; i < statements.length; i++) {
if (statementAborts(statements[i])) return statements[i];
}
return null;
};
/**
* @param {Node} self a block
* @returns {Node | null} the statement that ends it, if one does
*/
function blockAborts(self) {
return listAborts(/** @type {Node[]} */ (self.body));
}
// aborts
/**
* @returns {null} null
*/
const returnNull = () => null;
const abortsForStatement = returnNull;
/**
* @param {Node} self a jump
* @returns {Node} itself
*/
const abortsForJump = (self) => self;
const abortsForImport = returnNull;
const abortsForBlockStatement = blockAborts;
/**
* @param {Node} self a case or default
* @returns {Node | null} the statement that ends it, if one does
*/
const abortsForSwitchBranch = (self) =>
listAborts(/** @type {Node[]} */ (self.consequent));
/**
* @param {Node} self a class declaration
* @returns {Node | null} the static block that ends it, if one does
*/
const abortsForDefClass = (self) => {
for (const property of /** @type {Node[]} */ (
/** @type {Node} */ (self.body).body
)) {
if (A.isClassStaticBlockNode(property) && ast.aborts(property)) {
return property;
}
}
return null;
};
const abortsForClassStaticBlock = blockAborts;
/**
* @param {Node} self an if statement
* @returns {Node | null | false | undefined} itself, where both branches end
*/
const abortsForIf = (self) =>
self.alternate &&
statementAborts(/** @type {Node} */ (self.consequent)) &&
statementAborts(self.alternate) &&
self;
/**
* terser's `contains_this`.
* @param {Node} self a node
* @param {boolean=} includeNewTarget whether `new.target` also needs its function's context
* @returns {boolean} whether it reads the function's context outside a nested function
*/
A.containsThis = (self, includeNewTarget) =>
walk(self, (/** @type {Node} */ node, /** @type {Node[]} */ toVisit) => {
if (A.isThisNode(node) || (includeNewTarget && A.isNewTargetNode(node))) {
return walkAbort;
}
if (node !== self && A.isScopeNode(node) && !A.isArrowNode(node)) {
// A class's heritage and computed keys run in the enclosing context.
if (includeNewTarget && A.isClassNode(node)) {
if (node.superClass) toVisit.push(node.superClass);
for (const property of /** @type {Node[]} */ (
/** @type {Node} */ (node.body).body
)) {
if (property.computed) toVisit.push(property.key);
}
}
return true;
}
});
// terser's `is_boolean`, by its class.
const IS_BOOLEAN = kindValues({
Assign: isBooleanForAssign,
Binary: isBooleanForBinary,
Conditional: isBooleanForConditional,
False: isBooleanForFalse,
Node: isBooleanForNode,
Sequence: isBooleanForSequence,
True: isBooleanForTrue,
UnaryPrefix: isBooleanForUnaryPrefix
});
/**
* terser's `is_boolean`: whether a node always yields a boolean.
* @param {Node} node a node
* @returns {boolean} whether it does
*/
ast.isBoolean = (node) =>
/** @type {AstHelpers["isBoolean"]} */ (
/** @type {number | AstHelpers["isBoolean"]} */ (node[IS_BOOLEAN])
)(node);
// terser's `is_number`, by its class.
const IS_NUMBER = kindValues({
Assign: isNumberForAssign,
Binary: isNumberForBinary,
Conditional: isNumberForConditional,
Node: isNumberForNode,
Number: isNumberForNumber,
Sequence: isNumberForSequence,
Unary: isNumberForUnary
});
/**
* terser's `is_number`: whether a node always yields a number.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it does
*/
ast.isNumber = (node, compressor) =>
/** @type {AstHelpers["isNumber"]} */ (
/** @type {number | AstHelpers["isNumber"]} */ (node[IS_NUMBER])
)(node, compressor);
// terser's `is_bigint`, by its class.
const IS_BIGINT = kindValues({
Assign: isBigIntForAssign,
BigInt: isBigIntForBigInt,
Binary: isBigIntForBinary,
Conditional: isBigIntForConditional,
Node: isBigIntForNode,
Sequence: isBigIntForSequence,
Unary: isBigIntForUnary
});
/**
* terser's `is_bigint`: whether a node always yields a bigint.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it does
*/
ast.isBigInt = (node, compressor) =>
/** @type {AstHelpers["isBigInt"]} */ (
/** @type {number | AstHelpers["isBigInt"]} */ (node[IS_BIGINT])
)(node, compressor);
// terser's `is_number_or_bigint`, by its class.
const IS_NUMBER_OR_BIGINT = kindValues({
Assign: isNumberOrBigIntForAssign,
BigInt: isNumberOrBigIntForBigInt,
Binary: isNumberOrBigIntForBinary,
Conditional: isNumberOrBigIntForConditional,
Node: isNumberOrBigIntForNode,
Number: isNumberOrBigIntForNumber,
Sequence: isNumberOrBigIntForSequence,
Unary: isNumberOrBigIntForUnary
});
/**
* terser's `is_number_or_bigint`: whether a node always yields a number or a bigint.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it does
*/
ast.isNumberOrBigInt = (node, compressor) =>
/** @type {AstHelpers["isNumberOrBigInt"]} */ (
/** @type {number | AstHelpers["isNumberOrBigInt"]} */ (
node[IS_NUMBER_OR_BIGINT]
)
)(node, compressor);
// terser's `is_32_bit_integer`, by its class.
const IS_32_BIT_INTEGER = kindValues({
Binary: is32BitIntegerForBinary,
Node: is32BitIntegerForNode,
Number: is32BitIntegerForNumber,
UnaryPrefix: is32BitIntegerForUnaryPrefix
});
/**
* terser's `is_32_bit_integer`: whether a node always yields a 32-bit integer.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it does
*/
ast.is32BitInteger = (node, compressor) =>
/** @type {AstHelpers["is32BitInteger"]} */ (
/** @type {number | AstHelpers["is32BitInteger"]} */ (
node[IS_32_BIT_INTEGER]
)
)(node, compressor);
// terser's `is_string`, by its class.
const IS_STRING = kindValues({
Assign: isStringForAssign,
Binary: isStringForBinary,
Conditional: isStringForConditional,
Node: isStringForNode,
Sequence: isStringForSequence,
String: isStringForString,
TemplateString: isStringForTemplateString,
UnaryPrefix: isStringForUnaryPrefix
});
/**
* terser's `is_string`: whether a node always yields a string.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it does
*/
ast.isString = (node, compressor) =>
/** @type {AstHelpers["isString"]} */ (
/** @type {number | AstHelpers["isString"]} */ (node[IS_STRING])
)(node, compressor);
// terser's `may_throw`, by its class.
const MAY_THROW = kindValues({
Array: mayThrowForArray,
Assign: mayThrowForAssign,
Binary: mayThrowForBinary,
Block: mayThrowForBlock,
Call: mayThrowForCall,
Case: mayThrowForCase,
Chain: mayThrowForChain,
Class: mayThrowForClass,
ClassPrivateProperty: mayThrowForClassPrivateProperty,
ClassProperty: mayThrowForClassProperty,
ClassStaticBlock: mayThrowForClassStaticBlock,
ConciseMethod: mayThrowForConciseMethod,
Conditional: mayThrowForConditional,
Constant: mayThrowForConstant,
Definitions: mayThrowForDefinitions,
Dot: mayThrowForDot,
EmptyStatement: mayThrowForEmptyStatement,
If: mayThrowForIf,
ImportMeta: mayThrowForImportMeta,
LabeledStatement: mayThrowForLabeledStatement,
Lambda: mayThrowForLambda,
Node: mayThrowForNode,
Object: mayThrowForObject,
ObjectGetter: mayThrowForObjectGetter,
ObjectKeyVal: mayThrowForObjectKeyVal,
ObjectSetter: mayThrowForObjectSetter,
PrivateGetter: mayThrowForPrivateGetter,
PrivateMethod: mayThrowForPrivateMethod,
PrivateSetter: mayThrowForPrivateSetter,
Return: mayThrowForReturn,
Sequence: mayThrowForSequence,
SimpleStatement: mayThrowForSimpleStatement,
Sub: mayThrowForSub,
Switch: mayThrowForSwitch,
SymbolClassProperty: mayThrowForSymbolClassProperty,
SymbolDeclaration: mayThrowForSymbolDeclaration,
SymbolRef: mayThrowForSymbolRef,
This: mayThrowForThis,
Try: mayThrowForTry,
Unary: mayThrowForUnary,
VarDef: mayThrowForVarDef
});
/**
* terser's `may_throw`: whether running a node can throw.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it can
*/
ast.mayThrow = (node, compressor) =>
/** @type {AstHelpers["mayThrow"]} */ (
/** @type {number | AstHelpers["mayThrow"]} */ (node[MAY_THROW])
)(node, compressor);
/**
* Replaces whether a node of the type has effects with one wrapping it.
* @param {string} type the nodes, by ESTree type, one the table answers for
* @param {(original: (self: Node, compressor: Compressor) => boolean) => (self: Node, compressor: Compressor) => boolean} wrap builds the replacement
* @returns {void}
*/
ast.wrapHasSideEffects = (type, wrap) => {
for (const prototype of NODE_KIND_PROTOTYPES.values()) {
if (prototype.type !== type) continue;
const slots =
/** @type {Record<symbol, (self: Node, compressor: Compressor) => boolean>} */ (
prototype
);
slots[HAS_SIDE_EFFECTS] = wrap(slots[HAS_SIDE_EFFECTS]);
}
};
/**
* Replaces whether a node of the type may throw with one wrapping it.
* @param {string} type the nodes, by ESTree type, one the table answers for
* @param {(original: (self: Node, compressor: Compressor) => boolean) => (self: Node, compressor: Compressor) => boolean} wrap builds the replacement
* @returns {void}
*/
ast.wrapMayThrow = (type, wrap) => {
for (const prototype of NODE_KIND_PROTOTYPES.values()) {
if (prototype.type !== type) continue;
const slots =
/** @type {Record<symbol, (self: Node, compressor: Compressor) => boolean>} */ (
prototype
);
slots[MAY_THROW] = wrap(slots[MAY_THROW]);
}
};
// terser's `is_constant_expression`, by its class.
const IS_CONSTANT_EXPRESSION = kindValues({
Array: constantExpressionForArray,
Binary: constantExpressionForBinary,
Class: constantExpressionForClass,
Constant: constantExpressionForConstant,
Lambda: constantExpressionForLambda,
Node: constantExpressionForNode,
Object: constantExpressionForObject,
ObjectProperty: constantExpressionForObjectProperty,
Unary: constantExpressionForUnary
});
/**
* terser's `is_constant_expression`: whether a node's value is fixed where it is written.
* @param {Node} node a node
* @param {Scope=} scope the scope it is read in
* @returns {boolean | "f"} whether it is
*/
ast.isConstantExpression = (node, scope) =>
/** @type {AstHelpers["isConstantExpression"]} */ (
/** @type {number | AstHelpers["isConstantExpression"]} */ (
node[IS_CONSTANT_EXPRESSION]
)
)(node, scope);
// terser's `_dot_throw`, by its class.
const DOT_THROWS = kindValues({
Array: dotThrowsForArray,
Arrow: dotThrowsForArrow,
Assign: dotThrowsForAssign,
Binary: dotThrowsForBinary,
Chain: dotThrowsForChain,
Class: dotThrowsForClass,
Conditional: dotThrowsForConditional,
Constant: dotThrowsForConstant,
Dot: dotThrowsForDot,
Expansion: dotThrowsForExpansion,
Function: dotThrowsForFunction,
Node: dotThrowsForNode,
Null: dotThrowsForNull,
Object: dotThrowsForObject,
ObjectGetter: dotThrowsForObjectGetter,
ObjectProperty: dotThrowsForObjectProperty,
Sequence: dotThrowsForSequence,
SymbolRef: dotThrowsForSymbolRef,
UnaryPostfix: dotThrowsForUnaryPostfix,
UnaryPrefix: dotThrowsForUnaryPrefix,
Undefined: dotThrowsForUndefined
});
/**
* terser's `_dot_throw`: whether reading a property of a node's value can throw.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it can
*/
ast.dotThrows = (node, compressor) =>
/** @type {AstHelpers["dotThrows"]} */ (
/** @type {number | AstHelpers["dotThrows"]} */ (node[DOT_THROWS])
)(node, compressor);
// terser's `has_side_effects`, by its class.
const HAS_SIDE_EFFECTS = kindValues({
Array: hasSideEffectsForArray,
Assign: hasSideEffectsForAssign,
Binary: hasSideEffectsForBinary,
Block: hasSideEffectsForBlock,
Call: hasSideEffectsForCall,
Case: hasSideEffectsForCase,
Chain: hasSideEffectsForChain,
Class: hasSideEffectsForClass,
ClassPrivateProperty: hasSideEffectsForClassPrivateProperty,
ClassProperty: hasSideEffectsForClassProperty,
ClassStaticBlock: hasSideEffectsForClassStaticBlock,
ConciseMethod: hasSideEffectsForConciseMethod,
Conditional: hasSideEffectsForConditional,
Constant: hasSideEffectsForConstant,
Definitions: hasSideEffectsForDefinitions,
Dot: hasSideEffectsForDot,
DynamicImport: hasSideEffectsForDynamicImport,
EmptyStatement: hasSideEffectsForEmptyStatement,
If: hasSideEffectsForIf,
ImportMeta: hasSideEffectsForImportMeta,
LabeledStatement: hasSideEffectsForLabeledStatement,
Lambda: hasSideEffectsForLambda,
Node: hasSideEffectsForNode,
Object: hasSideEffectsForObject,
ObjectGetter: hasSideEffectsForObjectGetter,
ObjectKeyVal: hasSideEffectsForObjectKeyVal,
ObjectSetter: hasSideEffectsForObjectSetter,
PrivateGetter: hasSideEffectsForPrivateGetter,
PrivateMethod: hasSideEffectsForPrivateMethod,
PrivateSetter: hasSideEffectsForPrivateSetter,
Sequence: hasSideEffectsForSequence,
SimpleStatement: hasSideEffectsForSimpleStatement,
Sub: hasSideEffectsForSub,
Switch: hasSideEffectsForSwitch,
SymbolClassProperty: hasSideEffectsForSymbolClassProperty,
SymbolDeclaration: hasSideEffectsForSymbolDeclaration,
SymbolRef: hasSideEffectsForSymbolRef,
TemplateSegment: hasSideEffectsForTemplateSegment,
TemplateString: hasSideEffectsForTemplateString,
This: hasSideEffectsForThis,
Try: hasSideEffectsForTry,
Unary: hasSideEffectsForUnary,
VarDef: hasSideEffectsForVarDef
});
/**
* terser's `has_side_effects`: whether running a node can change anything.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it can
*/
ast.hasSideEffects = (node, compressor) =>
/** @type {AstHelpers["hasSideEffects"]} */ (
/** @type {number | AstHelpers["hasSideEffects"]} */ (
node[HAS_SIDE_EFFECTS]
)
)(node, compressor);
// terser's `bitwise_negate`, by its class.
const BITWISE_NEGATE = kindValues({
Node: bitwiseNegateForNode,
Number: bitwiseNegateForNumber,
UnaryPrefix: bitwiseNegateForUnaryPrefix
});
/**
* terser's `bitwise_negate`: a node yielding the bitwise complement of a node's value.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @param {boolean=} in32BitContext whether only its 32-bit integer value is read
* @returns {Node} the complement
*/
ast.bitwiseNegate = (node, compressor, in32BitContext) =>
/** @type {AstHelpers["bitwiseNegate"]} */ (
/** @type {number | AstHelpers["bitwiseNegate"]} */ (node[BITWISE_NEGATE])
)(node, compressor, in32BitContext);
// terser's `is_call_pure`, by its class.
const IS_CALL_PURE = kindValues({
Dot: isCallPureForDot,
Node: isCallPureForNode
});
/**
* terser's `is_call_pure`: whether calling a node's value, a method read off a native, has no effect.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {boolean | undefined} whether it has none
*/
ast.isCallPure = (node, compressor) =>
/** @type {AstHelpers["isCallPure"]} */ (
/** @type {number | AstHelpers["isCallPure"]} */ (node[IS_CALL_PURE])
)(node, compressor);
// terser's `aborts`, by its class.
const ABORTS = kindValues({
BlockStatement: abortsForBlockStatement,
ClassStaticBlock: abortsForClassStaticBlock,
DefClass: abortsForDefClass,
If: abortsForIf,
Import: abortsForImport,
Jump: abortsForJump,
Statement: abortsForStatement,
SwitchBranch: abortsForSwitchBranch
});
/**
* terser's `aborts`: the statement that always leaves a statement, if any.
* @param {Node} node a node
* @returns {Node | null | false | undefined} that statement
*/
ast.aborts = (node) =>
/** @type {AstHelpers["aborts"]} */ (
/** @type {number | AstHelpers["aborts"]} */ (node[ABORTS])
)(node);
// terser's `negate`, by its class.
const NEGATE = kindValues({
Arrow: negateForArrow,
Binary: negateForBinary,
Class: negateForClass,
Conditional: negateForConditional,
Function: negateForFunction,
Node: negateForNode,
Sequence: negateForSequence,
Statement: negateForStatement,
UnaryPrefix: negateForUnaryPrefix
});
/**
* terser's `negate`: a node yielding the logical opposite of a node's value.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node} the negation
*/
ast.negate = (node, compressor, firstInStatement) =>
/** @type {AstHelpers["negate"]} */ (
/** @type {number | AstHelpers["negate"]} */ (node[NEGATE])
)(node, compressor, firstInStatement);
};
/**
* Installs webpack's `drop_side_effect_free`: what is left of an expression
* whose value nobody reads, or null when nothing is.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installDropSideEffectFree = (modules) => {
const { ast, common, flags, inference, nativeObjects } = modules;
const A = ast;
const { is_nullish_shortcircuited: isNullishShortCircuited } = inference;
const { pure_prop_access_globals: purePropertyAccessGlobals } = nativeObjects;
const {
make_sequence: makeSequence,
is_func_expr: isFunctionExpression,
is_iife_call: isIifeCall
} = common;
const { WRITE_ONLY, set_flag: setFlag, clear_flag: clearFlag } = flags;
/**
* @returns {null} null
*/
const returnNull = () => null;
/**
* terser's `trim`: the expressions of a list that keep effects.
* @param {(Node | null)[]} nodes the expressions, null for a hole
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether the first starts a statement
* @returns {Node[] | null} what is left, the list itself when nothing changed
*/
const trim = (nodes, compressor, firstInStatement) => {
const length = nodes.length;
if (!length) return null;
/** @type {Node[]} */
const kept = [];
let changed = false;
for (let i = 0; i < length; i++) {
const item = nodes[i];
// A hole is dropped, as terser's hole node was.
const node =
item === null
? null
: ast.dropSideEffectFree(item, compressor, firstInStatement);
if (node !== item || item === null) changed = true;
if (node) {
kept.push(node);
firstInStatement = false;
}
}
// Unchanged, so the list holds no hole.
if (!changed) return /** @type {Node[]} */ (nodes);
return kept.length ? kept : null;
};
/**
* @param {Node} self an expression
* @returns {Node} itself
*/
const dropSideEffectFreeForNode = (self) => self;
const dropSideEffectFreeForConstant = returnNull;
const dropSideEffectFreeForThis = returnNull;
/**
* @param {Node} self a call
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left
*/
const dropSideEffectFreeForCall = (self, compressor, firstInStatement) => {
if (isNullishShortCircuited(self, compressor)) {
return ast.dropSideEffectFree(self.callee, compressor, firstInStatement);
}
if (!ast.isCalleePure(self, compressor)) {
if (ast.isCallPure(self.callee, compressor)) {
const kept = trim(
[self.callee.object, ...self.arguments],
compressor,
firstInStatement
);
return kept && makeSequence(self, kept);
}
if (
isFunctionExpression(self.callee) &&
(!self.callee.id || !self.callee.id.definition.references.length) &&
// An async function's promise calls the `then` its value may have.
!(
self.callee.async &&
modules.corrections !== undefined &&
modules.corrections.enabled
)
) {
// A shallow clone: the callee itself is what drops its value.
const node = ast.cloneNode(self);
ast.processExpression(node.callee, false, compressor);
return node;
}
return self;
}
const args = trim(self.arguments, compressor, firstInStatement);
return args && makeSequence(self, args);
};
/**
* @param {Node} self a dynamic import
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left
*/
const dropSideEffectFreeForDynamicImport = (
self,
compressor,
firstInStatement
) => {
if (self.phase !== "source") return self;
const args = trim(self.args, compressor, firstInStatement);
return args && makeSequence(self, args);
};
const dropSideEffectFreeForAccessor = returnNull;
const dropSideEffectFreeForFunction = returnNull;
const dropSideEffectFreeForArrow = returnNull;
/**
* @param {Node} self a class
* @param {Compressor} compressor the compressor
* @returns {Node | null} what is left
*/
const dropSideEffectFreeForClass = (self, compressor) => {
/** @type {Node[]} */
const withEffects = [];
if (
ast.isSelfReferential(self) &&
ast.hasSideEffects(self, compressor) &&
!ast.runsOnlyHeritage(self, compressor)
) {
return self;
}
const trimmedExtends =
self.superClass && ast.dropSideEffectFree(self.superClass, compressor);
if (trimmedExtends) withEffects.push(trimmedExtends);
for (const property of /** @type {Node[]} */ (
/** @type {Node} */ (self.body).body
)) {
if (A.isClassStaticBlockNode(property)) {
if (ast.hasSideEffects(property, compressor)) return self;
} else {
const trimmed = ast.dropSideEffectFree(property, compressor);
if (trimmed) withEffects.push(trimmed);
}
}
if (!withEffects.length) return null;
const expressions = makeSequence(self, withEffects);
if (A.isDefClassNode(self)) {
return makeNode(A.SimpleStatementNode, self, {
expression: expressions
});
}
return expressions;
};
/**
* @param {Node} self a class field
* @param {Compressor} compressor the compressor
* @returns {Node | null} what is left of its key and static value
*/
function dropField(self, compressor) {
const key = self.computed && ast.dropSideEffectFree(self.key, compressor);
const value =
self.static &&
self.value &&
ast.dropSideEffectFree(/** @type {Node} */ (self.value), compressor);
if (key && value) return makeSequence(self, [key, value]);
return key || value || null;
}
const dropSideEffectFreeForClassProperty = dropField;
const dropSideEffectFreeForClassPrivateProperty = dropField;
/**
* @param {Node} self a binary operation
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left
*/
const dropSideEffectFreeForBinary = (self, compressor, firstInStatement) => {
const right = ast.dropSideEffectFree(self.right, compressor);
if (!right) {
return ast.dropSideEffectFree(self.left, compressor, firstInStatement);
}
if (LAZY_OPERATORS.has(self.operator)) {
if (right === self.right) return self;
const node = ast.cloneNode(self);
node.right = right;
return node;
}
const left = ast.dropSideEffectFree(
self.left,
compressor,
firstInStatement
);
if (!left) {
return ast.dropSideEffectFree(self.right, compressor, firstInStatement);
}
return makeSequence(self, [left, right]);
};
/**
* @param {Node} self an assignment
* @param {Compressor} compressor the compressor
* @returns {Node | null} what is left
*/
const dropSideEffectFreeForAssign = (self, compressor) => {
if (self.logical) return self;
let left = self.left;
if (
ast.hasSideEffects(left, compressor) ||
(compressor.has_directive("use strict") &&
A.isPropAccessNode(left) &&
ast.isConstant(left.object))
) {
return self;
}
setFlag(self, WRITE_ONLY);
while (A.isPropAccessNode(left)) left = left.object;
if (
ast.isConstantExpression(
left,
/** @type {Node} */ (compressor.find_parent(A.isScopeNode)).block_scope
)
) {
return ast.dropSideEffectFree(self.right, compressor);
}
return self;
};
/**
* @param {Node} self a conditional
* @param {Compressor} compressor the compressor
* @returns {Node | null} what is left
*/
const dropSideEffectFreeForConditional = (self, compressor) => {
const consequent = ast.dropSideEffectFree(
/** @type {Node} */ (self.consequent),
compressor
);
const alternative = ast.dropSideEffectFree(self.alternate, compressor);
if (consequent === self.consequent && alternative === self.alternate) {
return self;
}
if (!consequent) {
return alternative
? makeNode(A.BinaryNode, self, {
operator: "||",
left: self.test,
right: alternative
})
: ast.dropSideEffectFree(self.test, compressor);
}
if (!alternative) {
return makeNode(A.BinaryNode, self, {
operator: "&&",
left: self.test,
right: consequent
});
}
const node = ast.cloneNode(self);
node.consequent = consequent;
node.alternate = alternative;
return node;
};
/**
* @param {Node} self a unary operation
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left
*/
const dropSideEffectFreeForUnary = (self, compressor, firstInStatement) => {
if (UNARY_SIDE_EFFECTS.has(self.operator)) {
if (ast.hasSideEffects(self.argument, compressor)) {
clearFlag(self, WRITE_ONLY);
} else {
setFlag(self, WRITE_ONLY);
}
return self;
}
if (self.operator === "typeof" && A.isSymbolRefNode(self.argument)) {
return null;
}
const expression = ast.dropSideEffectFree(
self.argument,
compressor,
firstInStatement
);
if (firstInStatement && expression && isIifeCall(expression)) {
if (expression === self.argument && self.operator === "!") {
return self;
}
return ast.negate(expression, compressor, firstInStatement);
}
return expression;
};
/**
* @param {Node} self a reference
* @param {Compressor} compressor the compressor
* @returns {Node | null} itself, where reading it may throw
*/
const dropSideEffectFreeForSymbolRef = (self, compressor) =>
ast.isDeclared(self, compressor) || purePropertyAccessGlobals.has(self.name)
? null
: self;
/**
* @param {Node} self an object literal
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left
*/
const dropSideEffectFreeForObject = (self, compressor, firstInStatement) => {
const values = trim(self.properties, compressor, firstInStatement);
return values && makeSequence(self, values);
};
/**
* @param {Node} self a property
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left of its key and value
*/
const dropSideEffectFreeForObjectKeyVal = (
self,
compressor,
firstInStatement
) => {
const key =
self.computed &&
ast.dropSideEffectFree(self.key, compressor, firstInStatement);
const value = ast.dropSideEffectFree(
/** @type {Node} */ (self.value),
compressor,
firstInStatement
);
if (key && value) return makeSequence(self, [key, value]);
return key || value;
};
/**
* @param {Node} self a method or accessor
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left of its key
*/
function dropMethod(self, compressor, firstInStatement) {
return self.computed
? ast.dropSideEffectFree(self.key, compressor, firstInStatement)
: null;
}
const dropSideEffectFreeForConciseMethod = dropMethod;
const dropSideEffectFreeForObjectGetter = dropMethod;
const dropSideEffectFreeForObjectSetter = dropMethod;
const dropSideEffectFreeForPrivateMethod = returnNull;
const dropSideEffectFreeForPrivateGetter = returnNull;
const dropSideEffectFreeForPrivateSetter = returnNull;
/**
* @param {Node} self an array literal
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left
*/
const dropSideEffectFreeForArray = (self, compressor, firstInStatement) => {
const values = trim(self.elements, compressor, firstInStatement);
return values && makeSequence(self, values);
};
/**
* @param {Node} self a property read
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left
*/
const dropSideEffectFreeForDot = (self, compressor, firstInStatement) => {
if (
!isNullishShortCircuited(self, compressor) &&
!self.optional &&
ast.mayThrowOnAccess(self.object, compressor)
) {
return self;
}
return ast.dropSideEffectFree(self.object, compressor, firstInStatement);
};
/**
* @param {Node} self a computed property read
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left
*/
const dropSideEffectFreeForSub = (self, compressor, firstInStatement) => {
if (isNullishShortCircuited(self, compressor)) {
return ast.dropSideEffectFree(self.object, compressor, firstInStatement);
}
if (!self.optional && ast.mayThrowOnAccess(self.object, compressor)) {
return self;
}
const property = ast.dropSideEffectFree(self.property, compressor);
if (property && self.optional) return self;
const expression = ast.dropSideEffectFree(
self.object,
compressor,
firstInStatement
);
if (expression && property) {
return makeSequence(self, [expression, property]);
}
return expression || property;
};
/**
* @param {Node} self an optional chain
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left of its expression
*/
const dropSideEffectFreeForChain = (self, compressor, firstInStatement) =>
ast.dropSideEffectFree(self.expression, compressor, firstInStatement);
/**
* @param {Node} self a sequence
* @param {Compressor} compressor the compressor
* @returns {Node | null} what is left
*/
const dropSideEffectFreeForSequence = (self, compressor) => {
const last = ast.tailNode(self);
const expression = ast.dropSideEffectFree(last, compressor);
if (expression === last) return self;
const expressions = self.expressions.slice(0, -1);
if (expression) expressions.push(expression);
if (!expressions.length) {
return makeNode(A.NumberNode, self, { value: 0 });
}
return makeSequence(self, expressions);
};
/**
* @param {Node} self a spread
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left of what it spreads
*/
const dropSideEffectFreeForExpansion = (
self,
compressor,
firstInStatement
) => {
const { arrayPrototypePatched } = modules.webpackInline;
// Spreading an array literal runs the iterator the program may patch;
// kept in an array literal of its own, it stands alone as `[...[1]]`.
if (
A.isArrayNode(self.argument) &&
arrayPrototypePatched &&
arrayPrototypePatched()
) {
return makeNode(A.ArrayNode, self, { elements: [self] });
}
return ast.dropSideEffectFree(self.argument, compressor, firstInStatement);
};
const dropSideEffectFreeForTemplateSegment = returnNull;
/**
* terser hands its segments a truthy `first_in_statement`, kept here.
* @param {Node} self a template string
* @param {Compressor} compressor the compressor
* @returns {Node | null} what is left
*/
const dropSideEffectFreeForTemplateString = (self, compressor) => {
const values = trim(self.segments, compressor, true);
return values && makeSequence(self, values);
};
// terser's `drop_side_effect_free`, by its class.
const DROP_SIDE_EFFECT_FREE = kindValues({
Accessor: dropSideEffectFreeForAccessor,
Array: dropSideEffectFreeForArray,
Arrow: dropSideEffectFreeForArrow,
Assign: dropSideEffectFreeForAssign,
Binary: dropSideEffectFreeForBinary,
Call: dropSideEffectFreeForCall,
Chain: dropSideEffectFreeForChain,
Class: dropSideEffectFreeForClass,
ClassPrivateProperty: dropSideEffectFreeForClassPrivateProperty,
ClassProperty: dropSideEffectFreeForClassProperty,
ConciseMethod: dropSideEffectFreeForConciseMethod,
Conditional: dropSideEffectFreeForConditional,
Constant: dropSideEffectFreeForConstant,
Dot: dropSideEffectFreeForDot,
DynamicImport: dropSideEffectFreeForDynamicImport,
Expansion: dropSideEffectFreeForExpansion,
Function: dropSideEffectFreeForFunction,
Node: dropSideEffectFreeForNode,
Object: dropSideEffectFreeForObject,
ObjectGetter: dropSideEffectFreeForObjectGetter,
ObjectKeyVal: dropSideEffectFreeForObjectKeyVal,
ObjectSetter: dropSideEffectFreeForObjectSetter,
PrivateGetter: dropSideEffectFreeForPrivateGetter,
PrivateMethod: dropSideEffectFreeForPrivateMethod,
PrivateSetter: dropSideEffectFreeForPrivateSetter,
Sequence: dropSideEffectFreeForSequence,
Sub: dropSideEffectFreeForSub,
SymbolRef: dropSideEffectFreeForSymbolRef,
TemplateSegment: dropSideEffectFreeForTemplateSegment,
TemplateString: dropSideEffectFreeForTemplateString,
This: dropSideEffectFreeForThis,
Unary: dropSideEffectFreeForUnary
});
/**
* terser's `drop_side_effect_free`: what of a node is left to run once its value is dropped, null where nothing is.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @param {boolean=} firstInStatement whether it starts a statement
* @returns {Node | null} what is left
*/
ast.dropSideEffectFree = (node, compressor, firstInStatement) =>
/** @type {AstHelpers["dropSideEffectFree"]} */ (
/** @type {number | AstHelpers["dropSideEffectFree"]} */ (
node[DROP_SIDE_EFFECT_FREE]
)
)(node, compressor, firstInStatement);
/**
* Replaces what a node of the type is left as with one wrapping it.
* @param {string} type the nodes, by ESTree type
* @param {(original: (self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null) => (self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null} wrap builds the replacement
* @returns {void}
*/
ast.wrapDropSideEffectFree = (type, wrap) => {
for (const prototype of NODE_KIND_PROTOTYPES.values()) {
if (prototype.type !== type) continue;
const slots =
/** @type {Record<symbol, (self: Node, compressor: Compressor, firstInStatement?: boolean) => Node | null>} */ (
prototype
);
slots[DROP_SIDE_EFFECT_FREE] = wrap(slots[DROP_SIDE_EFFECT_FREE]);
}
};
};
/**
* @typedef {object} CollapseHooks what later phases add to `collapse_vars`
* @property {((node: Node, compressor: Compressor, movesEffects: boolean) => boolean) | null} readsBuiltIn whether a node only reads a global or one of its members, which no value moved past it is taken to change, given whether that value has effects
* @property {((target: Node, parent: Node, defunScope: Scope, compressor: Compressor) => boolean) | null} storesWithoutReading whether a node is a property a plain `=` in its parent stores to, its object a name of the function given or `this`, whose evaluation no value moved past it changes
* @property {((node: Node, candidate: Node, compressor: Compressor) => boolean) | null} readsSettled whether a name read keeps its value however the code a candidate runs before it moves past
* @property {(() => boolean) | null} foldsUnobserved whether a value only the reads it folds into can see may move past any global read
* @property {((node: Node, parent: Node, candidate: Node) => boolean) | null} readsUnchangedBinding whether a name the moved value calls a method on is read bare, the binding itself unchanged
* @property {((target: Node, defunScope: Scope, compressor: Compressor) => boolean) | null} assignsLet whether an assignment to a `let` may be folded into the first read of it
* @property {((literal: Node) => boolean) | null} keepsAssignmentsOut whether `join_vars` must leave an assignment after an object literal out of it, as one runs a setter the literal may hold
* @property {((compressor: Compressor) => boolean) | null} joinsArrayStores whether `join_vars` moves a store to the next index of an array literal just declared into the literal, which skips a setter `Array.prototype` may hold
* @property {((value: Node, compressor: Compressor) => boolean) | null} keepsNamedValue whether a value named after the binding it is stored in stays there, as `keep_fnames` or `keep_classnames` asks
* @property {((node: Node, compressor: Compressor) => Node[]) | null} argumentsAliases the parameters a read of `arguments` also reads, which a value holding it may not move past a write of
* @property {((toplevel: Node) => void) | null} probeProgram what a later phase reads off the program once its scope is known, before the first pass
* @property {boolean} finalPass whether the pass running is the last one asked for
*/
/**
* Creates terser's `tighten_body`, which joins, reorders and drops the
* statements of a block and folds assignments into their first use.
* @param {TerserModules} modules terser's modules
* @returns {{ tightenBody: OptimizerHelpers["tightenBody"], extractFromUnreachableCode: OptimizerHelpers["extractFromUnreachableCode"] }} terser's `tighten_body` and `extract_from_unreachable_code`
*/
const createTightenBody = (modules) => {
const { transformNode, walkNode } = modules.ast;
const { ast, common, flags, inference, nativeObjects, utils } = modules;
// What a later phase lets `collapse_vars` move a value past.
/** @type {CollapseHooks} */
const collapseHooks = {
readsBuiltIn: null,
storesWithoutReading: null,
readsSettled: null,
assignsLet: null,
argumentsAliases: null,
keepsNamedValue: null,
keepsAssignmentsOut: null,
joinsArrayStores: null,
foldsUnobserved: null,
readsUnchangedBinding: null,
probeProgram: null,
finalPass: true
};
modules.webpackCollapse = collapseHooks;
const {
ArrayNode,
isArrayNode,
isArrowNode,
AssignNode,
isAssignNode,
isAwaitNode,
isBinaryNode,
isBlockNode,
BlockStatementNode,
isBlockStatementNode,
isBreakNode,
isCallNode,
isCaseNode,
isChainNode,
isClassNode,
isConditionalNode,
isConstantNode,
isContinueNode,
isDebuggerNode,
isDefaultNode,
isDefinitionsNode,
isDefinitionsLikeNode,
isDefunNode,
isDestructuringNode,
isDirectiveNode,
isDotNode,
EmptyStatementNode,
isEmptyStatementNode,
isExitNode,
isExpansionNode,
isExportNode,
isForNode,
isForInNode,
isForOfNode,
IfNode,
isIfNode,
isImportNode,
isIterationStatementNode,
isLambdaNode,
isLoopControlNode,
isSyntaxNode,
NumberNode,
isNumberNode,
isObjectNode,
ObjectKeyValNode,
isObjectKeyValNode,
isObjectPropertyNode,
isPropAccessNode,
isRegExpNode,
ReturnNode,
isReturnNode,
isScopeNode,
SequenceNode,
isSequenceNode,
SimpleStatementNode,
isSimpleStatementNode,
isSubNode,
isSwitchNode,
isSymbolNode,
isSymbolConstNode,
isSymbolDeclarationNode,
isSymbolDefunNode,
SymbolPropertyNode,
isSymbolFunargNode,
isSymbolLambdaNode,
isSymbolLetNode,
SymbolRefNode,
isSymbolRefNode,
isSymbolUsingNode,
SymbolVarNode,
isThisNode,
isTryNode,
isTryBlockNode,
isUnaryNode,
isUnaryPostfixNode,
UnaryPrefixNode,
isUnaryPrefixNode,
isUsingNode,
VarNode,
isVarNode,
VarDefNode,
isVarDefNode,
isWithNode,
isYieldNode,
createTransformer,
createStackTransformer,
createWalker,
terserKey,
terserProperty,
walk,
walk_abort: walkAbort,
_NOINLINE
} = ast;
const {
make_void_0: makeVoidZero,
MAP,
member,
remove,
has_annotation: hasAnnotation
} = utils;
const { pure_prop_access_globals: purePropertyAccessGlobals } = nativeObjects;
const {
lazy_op: lazyOperators,
unary_side_effects: unarySideEffects,
is_modified: isModified,
is_lhs: isLhs,
aborts
} = inference;
const { WRITE_ONLY, clear_flag: clearFlag } = flags;
// The kinds `extractCandidates` reads further, one bit for all of them.
// What the scan of a candidate tests a node for, one bit a set of kinds, as
// a node's kind takes no prototype walk to read.
const SCAN_ABORT = 1;
const SCAN_STOP = 1 << 1;
const SCAN_CUSTOM_ORDER = 1 << 2;
const scanKindOf = ast.kindBits(
/** @type {[string, number][]} */ ([
...[
"Await",
"Debugger",
"Destructuring",
"Expansion",
"Export",
"IterationStatement",
"LoopControl",
"ObjectProperty",
"Try",
"With",
"Yield",
"Class"
].map((name) => [name, SCAN_ABORT]),
...["Exit", "ForIn", "Unary", "VarDef"].map((name) => [name, SCAN_STOP]),
...["Scope", "Switch"].map((name) => [name, SCAN_CUSTOM_ORDER]),
...["Assign", "Call", "PropAccess", "SymbolRef", "Using"].map((name) => [
name,
SCAN_ABORT | SCAN_STOP
])
])
);
const EXTRACT_ASSIGN = 1;
const EXTRACT_BINARY = 1 << 1;
const EXTRACT_CALL = 1 << 2;
const EXTRACT_CASE = 1 << 3;
const EXTRACT_CONDITIONAL = 1 << 4;
const EXTRACT_DEFINITIONS = 1 << 5;
const EXTRACT_DW_LOOP = 1 << 6;
const EXTRACT_EXIT = 1 << 7;
const EXTRACT_FOR = 1 << 8;
const EXTRACT_FOR_IN = 1 << 9;
const EXTRACT_IF = 1 << 10;
const EXTRACT_SEQUENCE = 1 << 11;
const EXTRACT_SIMPLE_STATEMENT = 1 << 12;
const EXTRACT_SWITCH = 1 << 13;
const EXTRACT_UNARY = 1 << 14;
const EXTRACT_VAR_DEF = 1 << 15;
// A kind that is several of these, as an assignment is a binary, takes
// the lowest bit: the first test terser's chain of `instanceof` passes.
const extractKindOf = ast.kindBits([
["Assign", EXTRACT_ASSIGN],
["Binary", EXTRACT_BINARY],
["Call", EXTRACT_CALL],
["Case", EXTRACT_CASE],
["Conditional", EXTRACT_CONDITIONAL],
["Definitions", EXTRACT_DEFINITIONS],
["DWLoop", EXTRACT_DW_LOOP],
["Exit", EXTRACT_EXIT],
["For", EXTRACT_FOR],
["ForIn", EXTRACT_FOR_IN],
["If", EXTRACT_IF],
["Sequence", EXTRACT_SEQUENCE],
["SimpleStatement", EXTRACT_SIMPLE_STATEMENT],
["Switch", EXTRACT_SWITCH],
["Unary", EXTRACT_UNARY],
["VarDef", EXTRACT_VAR_DEF]
]);
const {
make_sequence: makeSequence,
merge_sequence: mergeSequence,
maintain_this_binding: maintainThisBinding,
is_func_expr: isFunctionExpression,
is_identifier_atom: isIdentifierAtom,
is_ref_of: isRefOf,
can_be_evicted_from_block: canBeEvictedFromBlock,
as_statement_array: asStatementArray
} = common;
/**
* terser's `has_multiple_if_returns`.
* @param {Node[]} statements the statements
* @returns {boolean} whether more than one is an `if` whose body returns
*/
function hasMultipleIfReturns(statements) {
let n = 0;
for (let i = statements.length; --i >= 0;) {
const statement = statements[i];
if (
isIfNode(statement) &&
isReturnNode(statement.consequent) &&
++n > 1
) {
return true;
}
}
return false;
}
/**
* @returns {boolean} whether webpack's corrections to terser are on
*/
function correcting() {
return modules.corrections !== undefined && modules.corrections.enabled;
}
/**
* Moves the comments before a statement dropped onto the one after it,
* which terser drops with it.
* @param {Compressor} compressor the compressor, holding the token tables
* @param {Node} dropped the statement dropped
* @param {Node} next the statement after it
* @returns {void}
*/
function keepCommentsOf(compressor, dropped, next) {
const tables = compressor.tokenTables;
const from = tables.of(dropped.startToken);
const to = tables.of(next.startToken);
// A tree read from ESTree holds no tokens, nor comments.
if (!from || !to || from.comments_before.length === 0) return;
to.comments_before = [...from.comments_before, ...to.comments_before];
from.comments_before = [];
// The line break after them moves too: a banner before a dropped `;`
// still ends its line, as terser prints it.
if (from.nlb) to.nlb = true;
}
/**
* @param {Compressor} compressor the compressor
* @returns {boolean} whether a `return` there awaits its value, `undefined` included, where a bare `return` does not
*/
function awaitsReturnValue(compressor) {
if (!correcting()) return false;
const lambda = compressor.find_parent(isLambdaNode);
return Boolean(lambda && lambda.async && lambda.generator);
}
/**
* terser's `is_return_void`.
* @param {Node | null | undefined} value a `return`'s value
* @returns {boolean} whether it returns `undefined` for sure
*/
function isReturnVoid(value) {
return !value || (isUnaryPrefixNode(value) && value.operator === "void");
}
/**
* terser's `as_statement_array_with_return`.
* @param {Node} node a branch
* @param {Node} exit the jump it ends in
* @returns {Node[] | undefined} its statements without the jump, where each can leave the block
*/
function asStatementArrayWithReturn(node, exit) {
let body = asStatementArray(node);
if (exit !== body[body.length - 1]) {
return undefined;
}
body = body.slice(0, -1);
if (
!body.every((/** @type {Node} */ statement) =>
canBeEvictedFromBlock(statement)
)
) {
return undefined;
}
if (exit.argument) {
body.push(
makeNode(SimpleStatementNode, exit.argument, {
expression: exit.argument.argument
})
);
}
return body;
}
/**
* terser's `next_index`.
* @param {Node[]} statements the statements
* @param {number} index a statement's index
* @returns {number} the index of the next statement that is not a bare `var`
*/
function nextIndex(statements, index) {
let j = index + 1;
for (const length = statements.length; j < length; j++) {
const statement = statements[j];
if (!(isVarNode(statement) && declarationsOnly(statement))) {
break;
}
}
return j;
}
/**
* terser's `prev_index`.
* @param {Node[]} statements the statements
* @param {number} index a statement's index
* @returns {number} the index of the previous statement that is not a bare `var`
*/
function prevIndex(statements, index) {
let j = index;
while (--j >= 0) {
const statement = statements[j];
if (!(isVarNode(statement) && declarationsOnly(statement))) {
break;
}
}
return j;
}
/**
* terser's `extract_candidates`: pushes the path to each assignment
* of an expression, in the order it runs.
* @param {Node} expression the expression or statement
* @param {Node[]} hitStack the path to it, left as it was found
* @param {Node[][]} candidates the paths found
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const extractCandidates = (expression, hitStack, candidates, compressor) => {
const kinds = expression[extractKindOf];
if (kinds === 0) return;
hitStack.push(expression);
switch (kinds & -kinds) {
case EXTRACT_ASSIGN:
if (
!ast.hasSideEffects(expression.left, compressor) &&
!isChainNode(expression.right) &&
!(
collapseHooks.keepsNamedValue !== null &&
expression.operator === "=" &&
isSymbolRefNode(expression.left) &&
collapseHooks.keepsNamedValue(expression.right, compressor)
)
) {
candidates.push([...hitStack]);
}
extractCandidates(expression.right, hitStack, candidates, compressor);
break;
case EXTRACT_BINARY:
extractCandidates(expression.left, hitStack, candidates, compressor);
extractCandidates(expression.right, hitStack, candidates, compressor);
break;
case EXTRACT_CALL:
if (hasAnnotation(expression, _NOINLINE)) break;
extractCandidates(expression.callee, hitStack, candidates, compressor);
for (const argument of expression.arguments) {
extractCandidates(argument, hitStack, candidates, compressor);
}
break;
case EXTRACT_CASE:
extractCandidates(expression.test, hitStack, candidates, compressor);
break;
case EXTRACT_CONDITIONAL:
extractCandidates(expression.test, hitStack, candidates, compressor);
extractCandidates(
/** @type {Node} */ (expression.consequent),
hitStack,
candidates,
compressor
);
extractCandidates(
expression.alternate,
hitStack,
candidates,
compressor
);
break;
case EXTRACT_DEFINITIONS: {
const length = expression.declarations.length;
// Limit number of trailing variable definitions for consideration.
let i = length - 200;
if (i < 0) i = 0;
for (; i < length; i++) {
extractCandidates(
expression.declarations[i],
hitStack,
candidates,
compressor
);
}
break;
}
case EXTRACT_DW_LOOP:
extractCandidates(expression.test, hitStack, candidates, compressor);
if (!isBlockNode(expression.body)) {
extractCandidates(
/** @type {Node} */ (expression.body),
hitStack,
candidates,
compressor
);
}
break;
case EXTRACT_EXIT:
if (expression.argument) {
extractCandidates(
expression.argument,
hitStack,
candidates,
compressor
);
}
break;
case EXTRACT_FOR:
if (expression.init) {
extractCandidates(expression.init, hitStack, candidates, compressor);
}
if (expression.test) {
extractCandidates(expression.test, hitStack, candidates, compressor);
}
if (expression.update) {
extractCandidates(
expression.update,
hitStack,
candidates,
compressor
);
}
if (!isBlockNode(expression.body)) {
extractCandidates(
/** @type {Node} */ (expression.body),
hitStack,
candidates,
compressor
);
}
break;
case EXTRACT_FOR_IN:
extractCandidates(expression.right, hitStack, candidates, compressor);
if (!isBlockNode(expression.body)) {
extractCandidates(
/** @type {Node} */ (expression.body),
hitStack,
candidates,
compressor
);
}
break;
case EXTRACT_IF:
extractCandidates(expression.test, hitStack, candidates, compressor);
if (!isBlockNode(expression.consequent)) {
extractCandidates(
/** @type {Node} */ (expression.consequent),
hitStack,
candidates,
compressor
);
}
if (expression.alternate && !isBlockNode(expression.alternate)) {
extractCandidates(
expression.alternate,
hitStack,
candidates,
compressor
);
}
break;
case EXTRACT_SEQUENCE:
for (const item of expression.expressions) {
extractCandidates(item, hitStack, candidates, compressor);
}
break;
case EXTRACT_SIMPLE_STATEMENT:
extractCandidates(
expression.expression,
hitStack,
candidates,
compressor
);
break;
case EXTRACT_SWITCH:
extractCandidates(
expression.discriminant,
hitStack,
candidates,
compressor
);
for (const branch of expression.cases) {
extractCandidates(branch, hitStack, candidates, compressor);
}
break;
case EXTRACT_UNARY:
if (expression.operator === "++" || expression.operator === "--") {
candidates.push([...hitStack]);
}
break;
default:
if (expression.init && !isChainNode(expression.init)) {
if (
collapseHooks.keepsNamedValue === null ||
!collapseHooks.keepsNamedValue(expression.init, compressor)
) {
candidates.push([...hitStack]);
}
extractCandidates(expression.init, hitStack, candidates, compressor);
}
}
hitStack.pop();
};
/**
* terser's `loop_body`.
* @param {Node} node a statement
* @returns {Node} the block a loop's `continue` ends, or the statement itself
*/
const loopBody = (node) => {
if (isIterationStatementNode(node)) {
return isBlockStatementNode(node.body)
? /** @type {Node} */ (node.body)
: node;
}
return node;
};
/**
* terser's `is_lhs_read_only`.
* @param {Node} lhs an assignment's target
* @returns {boolean} whether assigning to it does nothing or throws
*/
const isLhsReadOnly = (lhs) => {
if (isThisNode(lhs)) return true;
if (isSymbolRefNode(lhs)) {
return isSymbolLambdaNode(lhs.definition.identifiers[0]);
}
if (isPropAccessNode(lhs)) {
let object = lhs.object;
if (isSymbolRefNode(object)) {
if (ast.isImmutable(object)) return false;
object = /** @type {Node} */ (ast.fixedValue(object));
}
if (!object) return true;
if (isRegExpNode(object)) return false;
if (isConstantNode(object)) return true;
return isLhsReadOnly(object);
}
return false;
};
/**
* terser's `remove_initializers`: `var a = 1` becomes `var a`.
* @param {Node} varStatement a `var` statement
* @returns {Node | null} the statement without values, or null when it declares nothing
*/
const removeInitializers = (varStatement) => {
/** @type {Node[]} */
const declarations = [];
for (const definition of varStatement.declarations) {
if (isSymbolDeclarationNode(definition.id)) {
definition.init = /** @type {Node} */ (
/** @type {Node | null} */ (null)
);
declarations.push(definition);
} else {
for (const name of ast.declarationsAsNames(definition)) {
declarations.push(
makeNode(VarDefNode, definition, {
id: name,
init: null
})
);
}
}
}
return declarations.length
? makeNode(VarNode, varStatement, { declarations })
: null;
};
/**
* terser's `extract_from_unreachable_code`: what code that never runs still
* declares or exports, moved into a list.
* @param {Compressor} compressor the compressor
* @param {Node} statement the unreachable statement
* @param {Node[]} target where what it declares goes
* @param {boolean=} listed whether it stands in a statement list, not a clause
* @returns {void}
*/
const extractFromUnreachableCode = (
compressor,
statement,
target,
listed
) => {
walk(statement, (/** @type {Node} */ node) => {
if (isVarNode(node)) {
const noInitializers = removeInitializers(node);
if (noInitializers) target.push(noInitializers);
return true;
}
// A case's function is the switch block's, set before any case runs,
// so a case kept may call it.
if (
isDefunNode(node) &&
(isCaseNode(statement) || isDefaultNode(statement)) &&
/** @type {Node[]} */ (statement.consequent).includes(node) &&
node.id.definition &&
node.id.definition.references.length > 0 &&
correcting()
) {
target.push(node);
return true;
}
// Annex B hoists a block's plain function only.
if (
isDefunNode(node) &&
node !== statement &&
(node.async || node.generator) &&
correcting()
) {
return true;
}
// A function a clause declares is Annex B's, unset until it runs.
const hoisted = node === statement && (listed === true || !correcting());
if (
isDefunNode(node) &&
(hoisted || !compressor.has_directive("use strict"))
) {
target.push(
hoisted
? node
: makeNode(VarNode, node, {
declarations: [
makeNode(VarDefNode, node, {
id: makeNode(SymbolVarNode, node.id, node.id),
init: null
})
]
})
);
return true;
}
if (isExportNode(node) || isImportNode(node)) {
target.push(node);
return true;
}
// Nested scopes are not entered.
if (isScopeNode(node) || isClassNode(node)) {
return true;
}
});
};
/**
* terser's `find_loop_scope_try`.
* @param {Compressor} compressor the compressor
* @returns {{ inLoop: boolean, inTry: boolean }} whether the node visited sits in a loop or a `try` block of its scope
*/
const findLoopScopeTry = (compressor) => {
let node = compressor.self();
let level = 0;
let inLoop = false;
let inTry = false;
do {
if (isIterationStatementNode(node)) {
inLoop = true;
} else if (isScopeNode(node)) {
break;
} else if (isTryBlockNode(node)) {
inTry = true;
}
} while ((node = compressor.parent(level++)));
return { inLoop, inTry };
};
/**
* terser's `declarations_only`.
* @param {Node} node a definitions statement
* @returns {boolean} whether none of its definitions has a value
*/
const declarationsOnly = (node) =>
node.declarations.every((/** @type {Node} */ varDef) => !varDef.init);
/**
* terser's `to_simple_statement`: the one statement of a block, its `var`
* declarations moved out.
* @param {Node} block a branch of an `if`
* @param {Node[]} declarations where its declarations go
* @returns {Node | null | false} the statement, or false when there is more than one
*/
const toSimpleStatement = (block, declarations) => {
if (!isBlockStatementNode(block)) return block;
let statement = null;
for (
let i = 0, length = /** @type {Node[]} */ (block.body).length;
i < length;
i++
) {
const line = /** @type {Node[]} */ (block.body)[i];
if (isVarNode(line) && declarationsOnly(line)) {
declarations.push(line);
} else if (
statement ||
(isDefinitionsLikeNode(line) && !isVarNode(line))
) {
return false;
} else {
statement = line;
}
}
return statement;
};
/**
* terser's `arg_is_injectable`.
* @param {Node} argument an argument of a call
* @returns {boolean} whether it can become a parameter's value
*/
const argIsInjectable = (argument) => {
if (isExpansionNode(argument)) return false;
const containsAwait = walk(argument, (/** @type {Node} */ node) => {
if (isAwaitNode(node)) return walkAbort;
});
if (containsAwait) return false;
return true;
};
/**
* terser's `redefined_within_scope`.
* @param {Definition} definition a variable
* @param {Scope} scope a scope around its own
* @returns {boolean} whether a scope between them declares its name again
*/
const redefinedWithinScope = (definition, scope) => {
if (definition.global) return false;
for (
let current = /** @type {Scope | null} */ (definition.scope);
current && current !== scope;
current = current.upper
) {
if (current.getBinding(definition.name) !== undefined) return true;
}
return false;
};
/**
* terser's `get_rvalue`.
* @param {Node} expression an assignment or a definition
* @returns {Node} the value it assigns
*/
const getRvalue = (expression) =>
isAssignNode(expression) ? expression.right : expression.init;
/**
* `value != name`, which terser compares keys with: a key is a string, a node
* or missing, and a node reads as `"[object Object]"`.
* @param {Node | string} value a key, or a key's name
* @param {string} name a property name
* @returns {boolean} whether they differ loosely
*/
const differsLoosely = (value, name) => value != name; // eslint-disable-line eqeqeq
/** @type {Node[]} */
const EMPTY_PATH = [];
/**
* What terser's `collapse` keeps in function-scoped `var`s its closures share
* across candidates, held once per nesting depth so a run allocates none of
* it: the statements folded into, the candidate scanned, and the scanners.
*/
class CollapseState {
constructor() {
/** @type {Compressor} */
this.compressor = /** @type {Compressor} */ (
/** @type {Compressor | null} */ (null)
);
/** @type {Node[]} */
this.statements = /** @type {Node[]} */ (
/** @type {Node[] | null} */ (null)
);
this.statementIndex = 0;
/** @type {Scope} */
this.nearestScope = /** @type {Scope} */ (
/** @type {Scope | null} */ (null)
);
/** @type {Scope} */
this.defunScope = /** @type {Scope} */ (
/** @type {Scope | null} */ (null)
);
this.inLoop = false;
this.inTry = false;
this.changed = false;
/** @type {Node[][]} */
this.candidates = [];
/** @type {Node[]} */
this.path = [];
/** @type {Node[] | undefined} */
this.iifeArguments = undefined;
/** @type {Node[]} */
this.hitStack = [];
this.hitIndex = 0;
/** @type {Node} */
this.candidate = /** @type {Node} */ (/** @type {Node | null} */ (null));
/** @type {Definition | null | undefined} */
this.valueDefinition = null;
/** @type {Node | null} */
this.stopAfter = null;
/** @type {Node | null} */
this.stopIfHit = null;
/** @type {Node} */
this.lhs = /** @type {Node} */ (/** @type {Node | null} */ (null));
// The candidate's left values, read only once a scan needs them: most
// scans stop before any read, and building them walks the whole value.
/** @type {Map<string, { definition: Definition, modified: Node | boolean | undefined }>} */
this.leftValues = new Map();
this.leftValuesKnown = false;
// Whether a target that is a name has been added to them, or none is.
this.leftValuesWithTarget = false;
this.lhsLocal = false;
this.sideEffects = false;
// Whether only the reads the scan meets can see the candidate run.
this.unobserved = false;
// Whether the scan moved the candidate past a built-in it reads.
this.passedBuiltIn = false;
this.replaceAll = false;
this.mayThrow = false;
this.funarg = false;
this.hit = false;
this.abort = false;
this.replaced = 0;
this.canReplace = false;
/** @type {Definition} */
this.definition = /** @type {Definition} */ (
/** @type {Definition | null} */ (null)
);
// The candidate `removeCandidate` looks for, and whether it was found.
/** @type {Node | null} */
this.removing = null;
this.found = false;
// The scanners read no directive, only parents.
this.scanner = createStackTransformer(
(/** @type {Node} */ node) => scanForCandidate(this, node),
(/** @type {Node} */ node) => leaveScanned(this, node)
);
this.multiReplacer = createStackTransformer((/** @type {Node} */ node) =>
replaceRead(this, node)
);
// A left value reads no directive, so its walk keeps only the parents
// `isModified` reads, not the directives every other walk tracks.
const leftValueWalker = createWalker();
const leftValueStack = leftValueWalker.stack;
leftValueWalker._visit = (
/** @type {Node} */ node,
/** @type {((node: Node, visitor: Visitor) => void) | undefined} */ descend
) => {
leftValueStack.push(node);
noteLeftValue(this, node);
if (descend !== undefined) descend(node, leftValueWalker);
leftValueStack.pop();
};
this.leftValueWalker = leftValueWalker;
this.remover = createTransformer(
(
/** @type {Node} */ node,
/** @type {(node: Node, walker: Walker) => void} */ descend,
/** @type {boolean | undefined} */ inList
) => removeFound(this, node, /** @type {boolean} */ (inList)),
unwrapSequence
);
}
}
/** @type {CollapseState[]} */
const collapseStates = [];
// How many `collapse` runs are inside one another: optimizing a value it
// folds in can tighten a body of its own.
let collapseDepth = 0;
/**
* terser's `collapse`: from the last statement back, folds each assignment
* into the first use of its target, never into or past a branch or loop.
* @param {Node[]} statements the statements
* @param {Compressor} compressor the compressor
* @param {Scope} nearestScope the scope the statements declare into
* @param {Scope} defunScope the function around them
* @param {boolean} inLoop whether they can run more than once
* @param {boolean} inTry whether a `try` around them catches what they throw
* @returns {boolean} whether a statement changed
*/
const collapse = (
statements,
compressor,
nearestScope,
defunScope,
inLoop,
inTry
) => {
if (nearestScope.isPinned() || defunScope.isPinned()) return false;
let state = collapseStates[collapseDepth];
if (state === undefined) {
state = new CollapseState();
collapseStates[collapseDepth] = state;
}
collapseDepth++;
state.compressor = compressor;
state.statements = statements;
state.nearestScope = nearestScope;
state.defunScope = defunScope;
state.inLoop = inLoop;
state.inTry = inTry;
state.changed = false;
try {
// Most lists hold nothing to fold, so the statements after the last one
// that does are skipped before the scanners run.
const mayExtractArguments =
compressor.option("unused") && isFunctionExpression(compressor.self());
const { candidates, path } = state;
let statementIndex = statements.length;
while (--statementIndex >= 0) {
if (statementIndex === 0 && mayExtractArguments) break;
extractCandidates(
statements[statementIndex],
path,
candidates,
compressor
);
if (candidates.length > 0) break;
}
if (statementIndex >= 0) {
state.statementIndex = statementIndex;
collapseFrom(state);
}
return state.changed;
} finally {
collapseDepth--;
releaseCollapseState(state);
}
};
/**
* Empties a reused walker a run left inside a node: a transform pops what
* it pushed unless it throws, and the next run reads its parents off it.
* @param {Walker} walker the walker
* @returns {void}
*/
const clearWalker = (walker) => {
if (walker.stack.length === 0) return;
walker.stack.length = 0;
walker.directives = Object.create(null);
};
/**
* Lets go of what a run read, so none of it outlives the run.
* @param {CollapseState} state the run's state
* @returns {void}
*/
const releaseCollapseState = (state) => {
clearWalker(state.scanner);
clearWalker(state.multiReplacer);
clearWalker(state.remover);
clearWalker(state.leftValueWalker);
state.compressor = /** @type {Compressor} */ (
/** @type {Compressor | null} */ (null)
);
state.statements = /** @type {Node[]} */ (
/** @type {Node[] | null} */ (null)
);
state.nearestScope = /** @type {Scope} */ (
/** @type {Scope | null} */ (null)
);
state.defunScope = /** @type {Scope} */ (
/** @type {Scope | null} */ (null)
);
state.candidates.length = 0;
state.path.length = 0;
state.iifeArguments = undefined;
state.hitStack = EMPTY_PATH;
state.candidate = /** @type {Node} */ (/** @type {Node | null} */ (null));
state.valueDefinition = null;
state.stopAfter = null;
state.stopIfHit = null;
state.lhs = /** @type {Node} */ (/** @type {Node | null} */ (null));
state.leftValues.clear();
state.leftValuesKnown = false;
state.definition = /** @type {Definition} */ (
/** @type {Definition | null} */ (null)
);
state.removing = null;
};
/**
* Runs `collapse` from one statement back.
* @param {CollapseState} state the run's state, its candidates already read
* @returns {void}
*/
const collapseFrom = (state) => {
const { compressor, statements, candidates, scanner, multiReplacer } =
state;
for (; state.statementIndex >= 0; state.statementIndex--) {
const { statementIndex } = state;
if (candidates.length === 0) {
// Treat parameters as collapsible in IIFE, i.e. `function(a, b){ ... }(x());`
// would be translated into equivalent assignments `var a = x(), b = undefined;`.
if (statementIndex === 0 && compressor.option("unused")) {
extractArguments(state);
}
// Find collapsible assignments.
state.path.length = 0;
extractCandidates(
statements[statementIndex],
state.path,
candidates,
compressor
);
}
while (candidates.length > 0) {
const hitStack = /** @type {Node[]} */ (candidates.pop());
state.hitStack = hitStack;
state.hitIndex = 0;
const candidate = hitStack[hitStack.length - 1];
state.candidate = candidate;
state.valueDefinition = null;
state.stopAfter = null;
state.stopIfHit = null;
const lhs = /** @type {Node} */ (getLhs(state, candidate));
state.lhs = lhs;
if (!lhs || isLhsReadOnly(lhs) || ast.hasSideEffects(lhs, compressor)) {
continue;
}
// Locate symbols which may execute code outside of scanning range.
state.leftValuesKnown = false;
state.leftValuesWithTarget = !isSymbolRefNode(lhs);
state.lhsLocal = isLhsLocal(state, lhs);
state.sideEffects = valueHasSideEffects(state, candidate);
// Assigned in its dead zone, a `let` throws.
state.mayThrow =
ast.mayThrow(candidate, compressor) ||
(!isVarDefNode(candidate) && isRefOf(lhs, isSymbolLetNode) === true);
state.unobserved = isUnobserved(state);
if (state.unobserved) state.sideEffects = false;
state.passedBuiltIn = false;
state.replaceAll = replaceAllSymbols(state);
const funarg = isSymbolFunargNode(candidate.id);
state.funarg = funarg;
state.hit = funarg;
state.abort = false;
state.replaced = 0;
state.canReplace = !state.iifeArguments || !funarg;
if (!state.canReplace) {
const extra = /** @type {Node[]} */ (state.iifeArguments);
for (
let j = compressor.self().params.lastIndexOf(candidate.id) + 1;
!state.abort && j < extra.length;
j++
) {
transformNode(extra[j], scanner);
}
state.canReplace = true;
}
for (
let i = statementIndex;
!state.abort && i < statements.length;
i++
) {
transformNode(statements[i], scanner);
}
if (state.valueDefinition) {
const definition = candidate.id.definition;
state.definition = definition;
if (
state.abort &&
definition.references.length - definition.replaced > state.replaced
) {
// terser sets `false` here; 0 reads the same where it is read.
state.replaced = 0;
} else {
state.abort = false;
state.hitIndex = 0;
state.hit = funarg;
for (
let i = statementIndex;
!state.abort && i < statements.length;
i++
) {
transformNode(statements[i], multiReplacer);
}
/** @type {Definition} */ (state.valueDefinition).single_use =
false;
}
}
if (state.replaced && !removeCandidate(state, candidate)) {
statements.splice(statementIndex, 1);
}
}
}
};
/**
* Whether a write's value reads the name collapsed, so the candidate moves
* into it and runs, as before, ahead of the write.
* @param {Node} value the value written
* @param {Node} lhs the name collapsed
* @returns {boolean} true when it reads it
*/
const readsCandidate = (value, lhs) =>
isSymbolRefNode(lhs) &&
Boolean(
walk(value, (/** @type {Node} */ node) =>
isSymbolRefNode(node) && node.definition === lhs.definition
? walkAbort
: undefined
)
);
/**
* Whether a global is only written, by `=`, which reads nothing of it and,
* where it throws, throws only once the value is worked out.
* @param {Node} node an undeclared name
* @param {Node} parent the node holding it
* @returns {boolean} true when the improve phase may move a value past it
*/
const writesGlobal = (node, parent) =>
modules.improvements !== undefined &&
modules.improvements.enabled &&
isAssignNode(parent) &&
parent.operator === "=" &&
parent.left === node;
/**
* Whether a chain evaluates a name before anything else: its head, read
* whether or not an optional link then short-circuits the rest.
* @param {Node} node an optional call or property access
* @param {Node} lhs the name collapsed
* @returns {boolean} true when the chain's head is that name
*/
const readsFirst = (node, lhs) => {
if (
modules.improvements === undefined ||
!modules.improvements.enabled ||
!isSymbolRefNode(lhs)
) {
return false;
}
let head = node;
while (isCallNode(head) || isPropAccessNode(head) || isChainNode(head)) {
head = isPropAccessNode(head)
? head.object
: isCallNode(head)
? head.callee
: head.expression;
}
return ast.isEquivalent(lhs, head);
};
/**
* What the scanner does with a node: skips to the candidate, stops where
* the value cannot be moved past, and folds the value into the first read.
* @param {CollapseState} state the run's state
* @param {Node} node the node visited
* @returns {Node | undefined} the node or what replaces it, undefined to descend
*/
const scanForCandidate = (state, node) => {
if (state.abort) return node;
// Skip nodes before `candidate` as quickly as possible.
if (!state.hit) {
if (node !== state.hitStack[state.hitIndex]) return node;
state.hitIndex++;
if (state.hitIndex < state.hitStack.length) {
return handleCustomScanOrder(state, node);
}
state.hit = true;
state.stopAfter = findStop(state, node, 0);
if (state.stopAfter === node) state.abort = true;
return node;
}
const { compressor, scanner, lhs, candidate } = state;
// Stop immediately if these node types are encountered.
const parent = scanner.parent();
const kind = node[scanKindOf];
if ((kind & SCAN_ABORT) === 0) {
if (isForNode(parent) && node !== parent.init) {
state.abort = true;
return node;
}
} else if (
(isAssignNode(node) &&
(node.logical ||
(node.operator !== "=" && ast.isEquivalent(lhs, node.left)))) ||
isAwaitNode(node) ||
isUsingNode(node) ||
(isCallNode(node) &&
isPropAccessNode(lhs) &&
ast.isEquivalent(lhs, node.callee)) ||
((isCallNode(node) || isPropAccessNode(node)) &&
node.optional &&
!readsFirst(node, lhs)) ||
isDebuggerNode(node) ||
isDestructuringNode(node) ||
(isExpansionNode(node) &&
isSymbolNode(node.argument) &&
(isThisNode(node.argument) ||
node.argument.definition.references.length > 1)) ||
(isIterationStatementNode(node) && !isForNode(node)) ||
isLoopControlNode(node) ||
isTryNode(node) ||
isWithNode(node) ||
isYieldNode(node) ||
isExportNode(node) ||
isClassNode(node) ||
(isForNode(parent) && node !== parent.init) ||
(!state.replaceAll &&
!state.unobserved &&
isSymbolRefNode(node) &&
!ast.isDeclared(node, compressor) &&
!writesGlobal(node, parent) &&
// Likely a terser bug: a set of names asked for a node is never true.
!(
/** @type {Set<string | Node>} */ (purePropertyAccessGlobals).has(
node
)
) &&
!readsBuiltIn(state, node)) ||
(isSymbolRefNode(node) &&
isCallNode(parent) &&
hasAnnotation(parent, _NOINLINE)) ||
(isObjectPropertyNode(node) && isSyntaxNode(terserKey(node.key)))
) {
state.abort = true;
return node;
}
// Stop only if candidate is found within conditional branches.
if (
!state.stopIfHit &&
(!state.lhsLocal || !state.replaceAll) &&
((isBinaryNode(parent) &&
lazyOperators.has(parent.operator) &&
parent.left !== node) ||
(isConditionalNode(parent) && parent.test !== node) ||
(isIfNode(parent) && parent.test !== node))
) {
state.stopIfHit = parent;
}
// Replace variable with assignment when found.
if (
state.canReplace &&
!isSymbolDeclarationNode(node) &&
ast.isEquivalent(lhs, node) &&
!shadows(scanner.find_scope() || state.nearestScope, leftValues(state))
) {
if (state.stopIfHit) {
state.abort = true;
return node;
}
if (isLhs(node, parent)) {
if (state.valueDefinition) state.replaced++;
return node;
}
// Measured: `(a = b).c` past a built-in gzips worse than `a = b, a.c`.
if (
state.passedBuiltIn &&
isAssignNode(candidate) &&
isPropAccessNode(parent) &&
parent.object === node
) {
state.abort = true;
return node;
}
state.replaced++;
if (state.valueDefinition && isVarDefNode(candidate)) {
return node;
}
state.changed = true;
state.abort = true;
if (isUnaryPostfixNode(candidate)) {
return makeNode(UnaryPrefixNode, candidate, candidate);
}
if (isVarDefNode(candidate)) {
const candidateDefinition = candidate.id.definition;
const value = candidate.init;
if (
candidateDefinition.references.length -
candidateDefinition.replaced ===
1 &&
!compressor.exposed(candidateDefinition)
) {
candidateDefinition.replaced++;
if (state.funarg && isIdentifierAtom(value)) {
return transformNode(value, compressor);
}
return maintainThisBinding(parent, node, value);
}
return makeNode(AssignNode, candidate, {
operator: "=",
logical: false,
left: makeNode(SymbolRefNode, candidate.id, candidate.id),
right: value
});
}
clearFlag(candidate, WRITE_ONLY);
return candidate;
}
// These node types have child nodes that execute sequentially,
// but are otherwise not safe to scan into or beyond them.
if (
collapseHooks.storesWithoutReading !== null &&
isPropAccessNode(node) &&
collapseHooks.storesWithoutReading(
node,
parent,
state.defunScope,
compressor
)
) {
return undefined;
}
/** @type {Node | undefined} */
let symbol;
if ((kind & SCAN_STOP) === 0) {
if (
state.mayThrow &&
(state.inTry
? ast.hasSideEffects(node, compressor)
: sideEffectsExternal(state, node))
) {
// Not a scope: a class aborted above, and a function has no effects.
state.stopAfter = node;
}
} else if (
(isCallNode(node) && !state.unobserved) ||
(isExitNode(node) &&
(state.sideEffects ||
isPropAccessNode(lhs) ||
mayModify(state, lhs))) ||
(isPropAccessNode(node) &&
!state.unobserved &&
(state.sideEffects || ast.mayThrowOnAccess(node.object, compressor)) &&
!readsBuiltIn(state, node)) ||
(isSymbolRefNode(node) &&
((hasLeftValue(state, node.name) &&
/** @type {{ modified: boolean }} */ (
leftValues(state).get(node.name)
).modified &&
!readsBuiltIn(state, node) &&
!(
collapseHooks.readsUnchangedBinding !== null &&
!mayModify(state, node) &&
collapseHooks.readsUnchangedBinding(node, parent, state.candidate)
)) ||
(state.sideEffects &&
mayModify(state, node) &&
!readsBuiltIn(state, node) &&
!readsSettled(state, node) &&
!(
node.definition.undeclared &&
writesGlobal(node, parent) &&
readsCandidate(parent.right, lhs)
)))) ||
(isVarDefNode(node) &&
node.init &&
(hasLeftValue(state, node.id.name) ||
(state.sideEffects && mayModify(state, node.id)))) ||
// The target's own `let`: written before it, the name is in its dead zone.
(isVarDefNode(node) &&
isSymbolLetNode(node.id) &&
isSymbolRefNode(lhs) &&
node.id.definition === lhs.definition) ||
isUsingNode(node) ||
((symbol = isLhs(node.left, node)) &&
// A setter cannot see a candidate nothing else observes.
((isPropAccessNode(symbol) && !state.unobserved) ||
(isSymbolNode(symbol) && hasLeftValue(state, symbol.name)))) ||
(state.mayThrow &&
(state.inTry
? ast.hasSideEffects(node, compressor)
: sideEffectsExternal(state, node)))
) {
state.stopAfter = node;
if (isScopeNode(node)) state.abort = true;
}
// Annex B sets the `var` of a sloppy block's function where it is declared.
if (isDefunNode(node) && correcting()) {
const value = leftValues(state).get(node.id.name);
if (
value !== undefined &&
value.definition === node.id.definition &&
!(
/** @type {Node[]} */ (
ast.bodyOf(value.definition.scope.node)
).includes(node)
)
) {
state.abort = true;
return node;
}
}
return handleCustomScanOrder(state, node);
};
/**
* @param {CollapseState} state the run's state
* @param {Node} node the node the scanner left
* @returns {void}
*/
const leaveScanned = (state, node) => {
if (state.abort) return;
if (state.stopAfter === node) state.abort = true;
if (state.stopIfHit === node) state.stopIfHit = null;
};
/**
* What the replacer does with a node once a scan found every read of the
* candidate's target can take its value: puts the value in each.
* @param {CollapseState} state the run's state
* @param {Node} node the node visited
* @returns {Node | undefined} the node or what replaces it, undefined to descend
*/
const replaceRead = (state, node) => {
if (state.abort) return node;
// Skip nodes before `candidate` as quickly as possible.
if (!state.hit) {
if (node !== state.hitStack[state.hitIndex]) return node;
state.hitIndex++;
if (state.hitIndex < state.hitStack.length) return;
state.hit = true;
return node;
}
// Replace variable when found.
if (isSymbolRefNode(node) && node.name === state.definition.name) {
if (!--state.replaced) state.abort = true;
if (isLhs(node, state.multiReplacer.parent())) return node;
state.definition.replaced++;
/** @type {Definition} */ (state.valueDefinition).replaced--;
return state.candidate.init;
}
// Skip (non-executed) functions and (leading) default case in switch statements.
if (isDefaultNode(node) || isScopeNode(node)) {
return node;
}
};
/**
* terser's `handle_custom_scan_order`: skips functions, and scans a
* switch's case expressions first.
* @param {CollapseState} state the run's state
* @param {Node} node the node visited
* @returns {Node | undefined} the node when handled here, undefined to descend
*/
const handleCustomScanOrder = (state, node) => {
if ((node[scanKindOf] & SCAN_CUSTOM_ORDER) === 0) {
return;
}
// Skip (non-executed) functions.
if (isScopeNode(node)) return node;
// Scan case expressions first in a switch statement.
if (isSwitchNode(node)) {
const { scanner } = state;
node.discriminant = transformNode(node.discriminant, scanner);
for (
let i = 0, length = node.cases.length;
!state.abort && i < length;
i++
) {
const branch = node.cases[i];
if (isCaseNode(branch)) {
if (!state.hit) {
if (branch !== state.hitStack[state.hitIndex]) continue;
state.hitIndex++;
}
branch.test = transformNode(branch.test, scanner);
if (!state.replaceAll) break;
}
}
state.abort = true;
return node;
}
};
/**
* terser's `has_overlapping_symbol`.
* @param {CollapseState} state the run's state
* @param {Node} lambda the function called
* @param {Node} argument an argument of the call
* @param {Node | boolean | undefined} lambdaStrict its `"use strict"` directive, true where an option sets it
* @returns {boolean} whether the argument reads a name or `this` the function sees differently
*/
const hasOverlappingSymbol = (state, lambda, argument, lambdaStrict) => {
const { defunScope } = state;
let found = false;
let scanThis = !isArrowNode(lambda);
walkNode(
argument,
createWalker(
(/** @type {Node} */ node, /** @type {() => void} */ descend) => {
if (found) return true;
if (
isSymbolRefNode(node) &&
(lambda.block_scope.getBinding(node.name) !== undefined ||
redefinedWithinScope(node.definition, lambda.block_scope))
) {
let scope = /** @type {Scope | null} */ (node.definition.scope);
if (scope !== defunScope) {
while ((scope = /** @type {Scope} */ (scope).upper)) {
if (scope === defunScope) return true;
}
}
found = true;
return true;
}
if ((lambdaStrict || scanThis) && isThisNode(node)) {
found = true;
return true;
}
if (isScopeNode(node) && !isArrowNode(node)) {
const previous = scanThis;
scanThis = false;
descend();
scanThis = previous;
return true;
}
}
)
);
return found;
};
/**
* terser's `extract_args`: an IIFE's parameters as candidates, each
* assigned its argument.
* @param {CollapseState} state the run's state
* @returns {void}
*/
const extractArguments = (state) => {
const { compressor, candidates } = state;
/** @type {Node} */
let iife;
const lambda = compressor.self();
if (
isFunctionExpression(lambda) &&
!lambda.id &&
!ast.usesArguments(lambda) &&
!ast.isPinned(lambda) &&
isCallNode((iife = compressor.parent())) &&
iife.callee === lambda &&
iife.arguments.every(argIsInjectable)
) {
/** @type {Node | boolean | undefined} */
let lambdaStrict = compressor.has_directive("use strict");
if (
lambdaStrict &&
!member(
lambdaStrict,
/** @type {Node[]} */ (/** @type {Node} */ (lambda.body).body)
)
) {
lambdaStrict = false;
}
const length = lambda.params.length;
const extra = iife.arguments.slice(length);
state.iifeArguments = extra;
/** @type {Set<string>} */
const names = new Set();
for (let i = length; --i >= 0;) {
const symbol = lambda.params[i];
/** @type {Node | null} */ let argument = iife.arguments[i];
// A duplicate of the fix in terser 011d3eb, which reduce_vars
// may be doing the exact same thing for.
const symbolDefinition = symbol.definition && symbol.definition;
const isReassigned =
symbolDefinition && symbolDefinition.identifiers.length > 1;
if (isReassigned) continue;
extra.unshift(
makeNode(VarDefNode, symbol, {
id: symbol,
init: argument
})
);
if (names.has(symbol.name)) continue;
names.add(symbol.name);
if (isExpansionNode(symbol)) {
const elements = iife.arguments.slice(i);
if (
elements.every(
(/** @type {Node} */ element) =>
!hasOverlappingSymbol(state, lambda, element, lambdaStrict)
)
) {
candidates.unshift([
makeNode(VarDefNode, symbol, {
id: symbol.argument,
init: makeNode(ArrayNode, iife, {
elements
})
})
]);
}
} else {
if (!argument) {
argument = transformNode(makeVoidZero(symbol), compressor);
} else if (
(isLambdaNode(argument) && ast.isPinned(argument)) ||
hasOverlappingSymbol(state, lambda, argument, lambdaStrict)
) {
argument = null;
}
if (argument) {
candidates.unshift([
makeNode(VarDefNode, symbol, {
id: symbol,
init: argument
})
]);
}
}
}
}
};
/**
* terser's `find_stop`: the outermost node around the candidate whose
* end still ends the scan.
* @param {CollapseState} state the run's state
* @param {Node} node the candidate or a node around it
* @param {number} level how far up the scanner's stack its parent is
* @param {boolean=} writeOnly whether the value of `node` is unused
* @returns {Node | null} the node to stop after
*/
const findStop = (state, node, level, writeOnly) => {
const parent = state.scanner.parent(level);
if (isAssignNode(parent)) {
if (
writeOnly &&
!parent.logical &&
!(
isPropAccessNode(parent.left) || hasLeftValue(state, parent.left.name)
)
) {
return findStop(state, parent, level + 1, writeOnly);
}
return node;
}
if (isBinaryNode(parent)) {
if (
writeOnly &&
(!lazyOperators.has(parent.operator) || parent.left === node)
) {
return findStop(state, parent, level + 1, writeOnly);
}
return node;
}
if (isCallNode(parent)) return node;
if (isCaseNode(parent)) return node;
if (isConditionalNode(parent)) {
if (writeOnly && parent.test === node) {
return findStop(state, parent, level + 1, writeOnly);
}
return node;
}
if (isDefinitionsNode(parent)) {
return findStop(state, parent, level + 1, true);
}
if (isExitNode(parent)) {
return writeOnly ? findStop(state, parent, level + 1, writeOnly) : node;
}
if (isIfNode(parent)) {
if (writeOnly && parent.test === node) {
return findStop(state, parent, level + 1, writeOnly);
}
return node;
}
if (isIterationStatementNode(parent)) return node;
if (isSequenceNode(parent)) {
return findStop(state, parent, level + 1, ast.tailNode(parent) !== node);
}
if (isSimpleStatementNode(parent)) {
return findStop(state, parent, level + 1, true);
}
if (isSwitchNode(parent)) return node;
if (isVarDefNode(parent)) return node;
return null;
};
/**
* terser's `mangleable_var`: records the variable a definition copies.
* @param {CollapseState} state the run's state
* @param {Node} varDef a definition
* @returns {Definition | undefined} the variable its value reads, where it is a declared one
*/
const mangleableVar = (state, varDef) => {
const value = varDef.init;
if (!isSymbolRefNode(value)) return;
if (value.name === "arguments") return;
const valueDef = value.definition;
if (valueDef.undeclared) return;
return (state.valueDefinition = valueDef);
};
/**
* terser's `get_lhs`.
* @param {CollapseState} state the run's state
* @param {Node} expression a candidate
* @returns {Node | false | undefined} what it assigns to, where it can be folded
*/
const getLhs = (state, expression) => {
if (isAssignNode(expression) && expression.logical) {
return false;
}
if (isVarDefNode(expression) && isSymbolDeclarationNode(expression.id)) {
const nameDefinition = expression.id.definition;
if (!member(expression.id, nameDefinition.identifiers)) return;
const referenced =
nameDefinition.references.length - nameDefinition.replaced;
if (!referenced) return;
const declared =
nameDefinition.identifiers.length - nameDefinition.eliminated;
if (
(declared > 1 && !isSymbolFunargNode(expression.id)) ||
(referenced > 1
? mangleableVar(state, expression)
: !state.compressor.exposed(nameDefinition))
) {
return makeNode(SymbolRefNode, expression.id, expression.id);
}
return;
}
const target = isAssignNode(expression)
? expression.left
: expression.argument;
return (
!isRefOf(target, isSymbolConstNode) &&
!(
isRefOf(target, isSymbolLetNode) &&
(collapseHooks.assignsLet === null ||
!collapseHooks.assignsLet(target, state.defunScope, state.compressor))
) &&
!isRefOf(target, isSymbolUsingNode) &&
target
);
};
/**
* terser's `get_lvalues`, read once per candidate.
* @param {CollapseState} state the run's state
* @returns {Map<string, { definition: Definition, modified: Node | boolean | undefined }>} the variables the candidate's value reads, without its target
*/
const leftValuesWithoutTarget = (state) => {
const values = state.leftValues;
if (!state.leftValuesKnown) {
state.leftValuesKnown = true;
values.clear();
if (!isUnaryNode(state.candidate)) {
walkNode(getRvalue(state.candidate), state.leftValueWalker);
}
}
return values;
};
/**
* Whether the left-value walker stands inside a function of the value.
* @param {CollapseState} state the run's state
* @returns {boolean} true inside one
*/
const inMadeFunction = (state) =>
state.leftValueWalker.stack.some((/** @type {Node} */ node) =>
isLambdaNode(node)
);
/**
* What the left-value walker does with a node: notes each variable read,
* and whether the value modifies it.
* @param {CollapseState} state the run's state
* @param {Node} node the node visited
* @returns {void}
*/
const noteLeftValue = (state, node) => {
let symbol = node;
while (isPropAccessNode(symbol)) symbol = symbol.object;
if (
isSymbolRefNode(symbol) &&
// A name a function the value only makes declares reads the same
// wherever it moves.
!(
state.unobserved &&
symbol.definition &&
state.leftValueWalker.stack.includes(symbol.definition.scope.node)
)
) {
const values = state.leftValues;
const previous = values.get(symbol.name);
if (!previous || !previous.modified) {
const { definition } = symbol;
const modified =
// A function only made does not run what it holds.
!(state.unobserved && inMadeFunction(state)) &&
isModified(state.compressor, state.leftValueWalker, node, node, 0);
if (previous) {
previous.definition = definition;
previous.modified = modified;
} else {
values.set(symbol.name, { definition, modified });
}
}
if (collapseHooks.argumentsAliases !== null) {
for (const parameter of collapseHooks.argumentsAliases(
symbol,
state.compressor
)) {
if (!values.has(parameter.name)) {
values.set(parameter.name, {
definition: parameter.definition,
modified: false
});
}
}
}
}
};
/**
* @param {CollapseState} state the run's state
* @returns {Map<string, { definition: Definition, modified: Node | boolean | undefined }>} the variables the candidate's value reads, with its target
*/
const leftValues = (state) => {
const values = leftValuesWithoutTarget(state);
if (!state.leftValuesWithTarget) {
state.leftValuesWithTarget = true;
const { lhs } = state;
values.set(lhs.name, { definition: lhs.definition, modified: false });
}
return values;
};
/**
* @param {CollapseState} state the run's state
* @param {string} name a variable's name
* @returns {boolean} whether the candidate's value or target reads it
*/
const hasLeftValue = (state, name) => leftValues(state).has(name);
/**
* terser's `remove_candidate`: drops a folded assignment from where it was.
* @param {CollapseState} state the run's state
* @param {Node} expression the candidate
* @returns {Node | null | true} what is left of its statement, null when nothing
*/
const removeCandidate = (state, expression) => {
const { compressor } = state;
if (isSymbolFunargNode(expression.id)) {
const iife = compressor.parent();
const argnames = compressor.self().params;
const index = argnames.indexOf(expression.id);
if (index < 0) {
iife.arguments.length = Math.min(
iife.arguments.length,
argnames.length - 1
);
} else {
const args = iife.arguments;
if (args[index]) {
args[index] = makeNode(NumberNode, args[index], {
value: 0
});
}
}
return true;
}
state.removing = expression;
state.found = false;
return transformNode(state.statements[state.statementIndex], state.remover);
};
/**
* What the remover does with a node: takes the candidate out of its
* statement, leaving a `const` its value.
* @param {CollapseState} state the run's state
* @param {Node} node the node visited
* @param {boolean} inList whether it sits in a list
* @returns {Node | SkipMarker | null | undefined} what replaces it, undefined to descend
*/
const removeFound = (state, node, inList) => {
if (state.found) return node;
if (node === state.removing || ast.bodyOf(node) === state.removing) {
state.found = true;
if (isVarDefNode(node)) {
// `const` always needs a value.
node.init = /** @type {Node} */ (
isSymbolConstNode(node.id) ? makeVoidZero(node.init) : null
);
return node;
}
return inList ? MAP.skip : null;
}
};
/**
* @param {Node} node the node the remover left
* @returns {Node | null | undefined} what replaces an emptied sequence
*/
const unwrapSequence = (node) => {
if (isSequenceNode(node)) {
const length = node.expressions.length;
if (length === 0) return null;
if (length === 1) return node.expressions[0];
}
};
/**
* terser's `is_lhs_local`.
* @param {CollapseState} state the run's state
* @param {Node} target the candidate's target
* @returns {boolean} whether it is a variable of this function no loop assigns again
*/
const isLhsLocal = (state, target) => {
let symbol = target;
while (isPropAccessNode(symbol)) symbol = symbol.object;
const { candidate } = state;
return (
isSymbolRefNode(symbol) &&
symbol.definition.scope.getDefunScope() === state.defunScope &&
!(
state.inLoop &&
(leftValuesWithoutTarget(state).has(symbol.name) ||
isUnaryNode(candidate) ||
(isAssignNode(candidate) &&
!candidate.logical &&
candidate.operator !== "="))
)
);
};
/**
* terser's `value_has_side_effects`.
* @param {CollapseState} state the run's state
* @param {Node} expression a candidate
* @returns {boolean} whether its value has side effects
*/
const valueHasSideEffects = (state, expression) => {
if (isUnaryNode(expression)) {
return unarySideEffects.has(expression.operator);
}
return ast.hasSideEffects(getRvalue(expression), state.compressor);
};
/**
* terser's `replace_all_symbols`.
* @param {CollapseState} state the run's state
* @returns {boolean} whether every read of the candidate's target can be replaced
*/
const replaceAllSymbols = (state) => {
if (state.sideEffects) return false;
if (state.valueDefinition) return true;
const { lhs } = state;
if (isSymbolRefNode(lhs)) {
const lhsDefinition = lhs.definition;
if (
lhsDefinition.references.length - lhsDefinition.replaced ===
(isVarDefNode(state.candidate) ? 1 : 2)
) {
return true;
}
}
return false;
};
/**
* Whether a node only reads a built-in global or one of its members, noting
* in the state when a candidate kept as an assignment moves past it.
* @param {CollapseState} state the run's state
* @param {Node} node a node
* @returns {boolean} true where a later phase reads it so
*/
const readsBuiltIn = (state, node) => {
// Moved earlier, a value would hide from later passes what they fold.
if (!collapseHooks.finalPass || collapseHooks.readsBuiltIn === null) {
return false;
}
const drops = dropsCandidate(state);
if (
!(drops || assignsHiddenName(state)) ||
!collapseHooks.readsBuiltIn(node, state.compressor, state.sideEffects)
) {
return false;
}
if (!drops) state.passedBuiltIn = true;
return true;
};
/**
* Whether a candidate is an assignment or update of a name no code outside
* the program reads, or of a property: folded, nothing else sees it move.
* @param {CollapseState} state the run's state
* @returns {boolean} true for such a candidate
*/
const assignsHiddenName = (state) =>
!isVarDefNode(state.candidate) &&
// Measured: a global the program shares gzips worse folded, and wins nothing.
!(
isSymbolRefNode(state.lhs) &&
state.compressor.exposed(state.lhs.definition)
);
/**
* Whether a node reads a name no value moved past it can rebind.
* @param {CollapseState} state the run's state
* @param {Node} node a name read
* @returns {boolean} true where a later phase knows the name keeps its value
*/
const readsSettled = (state, node) =>
collapseHooks.finalPass &&
collapseHooks.readsSettled !== null &&
collapseHooks.readsSettled(node, state.candidate, state.compressor);
/**
* Whether collapsing the candidate leaves nothing of it behind.
* @param {CollapseState} state the run's state
* @returns {boolean} true for a declaration of a name read once and written nowhere else
*/
const dropsCandidate = (state) => {
const { candidate, compressor } = state;
if (!isVarDefNode(candidate) || isSymbolFunargNode(candidate.id)) {
return false;
}
const candidateDefinition = candidate.id.definition;
return (
candidateDefinition.references.length - candidateDefinition.replaced ===
1 &&
candidateDefinition.identifiers.length === 1 &&
!candidateDefinition.assignments &&
!compressor.exposed(candidateDefinition) &&
!(
candidateDefinition.global &&
compressor.top_retain &&
compressor.top_retain(candidateDefinition)
)
);
};
/**
* Whether a name is one only this function reads and writes, so no code
* elsewhere sees what it holds.
* @param {CollapseState} state the run's state
* @param {Definition} definition the name's definition
* @returns {boolean} true for such a name
*/
const isUnsharedLocal = (state, definition) =>
!definition.undeclared &&
!definition.scope.usesArguments &&
!state.compressor.exposed(definition) &&
!definitionMayBeModified(state, definition);
/**
* Whether a name `reduce_vars` saw set once, by its declaration, and never
* written again, so no code run elsewhere can change it.
* @param {CollapseState} state the run's state
* @param {Definition} definition the name's definition
* @returns {boolean} true for such a name
*/
const neverWritten = (state, definition) =>
state.compressor.option("reduce_vars") &&
!definition.undeclared &&
!state.compressor.exposed(definition) &&
definition.identifiers.length === 1 &&
definition.assignments === 0 &&
Boolean(definition.fixed);
/**
* Whether a value reads nothing but names only this function sees and
* constants, so code run while it waits to run cannot change it.
* @param {CollapseState} state the run's state
* @param {Node} node the value
* @returns {boolean} true for such a value
*/
const readsOnlyUnsharedLocals = (state, node) => {
if (isConstantNode(node)) return true;
if (isSymbolRefNode(node)) {
return (
isUnsharedLocal(state, node.definition) ||
neverWritten(state, node.definition)
);
}
if (isUnaryPrefixNode(node)) {
return (
!unarySideEffects.has(node.operator) &&
readsOnlyUnsharedLocals(state, node.argument)
);
}
if (isConditionalNode(node)) {
return (
readsOnlyUnsharedLocals(state, node.test) &&
readsOnlyUnsharedLocals(state, /** @type {Node} */ (node.consequent)) &&
readsOnlyUnsharedLocals(state, node.alternate)
);
}
// `in` may run a proxy's trap, and `instanceof` a `Symbol.hasInstance`.
return (
isBinaryNode(node) &&
node.operator !== "in" &&
node.operator !== "instanceof" &&
readsOnlyUnsharedLocals(state, node.left) &&
readsOnlyUnsharedLocals(state, node.right)
);
};
/**
* Whether a value only makes functions and objects of them, and reads
* what `readsOnlyUnsharedLocals` allows: making it runs no code.
* @param {CollapseState} state the run's state
* @param {Node} node the value
* @returns {boolean} true for such a value
*/
const makesOnly = (state, node) => {
if (isLambdaNode(node)) return !isClassNode(node);
// A function declaration never written holds it from its scope's start,
// unless declared in a block, which Annex B sets only as the block runs.
if (isSymbolRefNode(node)) {
const { definition } = node;
if (
definition &&
definition.assignments === 0 &&
definition.identifiers.length === 1 &&
isSymbolDefunNode(definition.identifiers[0])
) {
const body = ast.bodyOf(definition.scope.node);
return (
Array.isArray(body) &&
body.includes(/** @type {Node} */ (definition.init))
);
}
}
if (isObjectNode(node)) {
return node.properties.every(
(/** @type {Node} */ property) =>
isObjectKeyValNode(property) &&
!isSyntaxNode(terserKey(property.key)) &&
makesOnly(state, /** @type {Node} */ (property.value))
);
}
return readsOnlyUnsharedLocals(state, node);
};
/**
* Whether nothing but the reads the scan meets sees the candidate run: it
* sets a name only this function sees, from such names, and cannot throw.
* @param {CollapseState} state the run's state
* @returns {boolean} true for such a candidate
*/
const isUnobserved = (state) => {
const { candidate, lhs } = state;
if (
collapseHooks.foldsUnobserved === null ||
!collapseHooks.foldsUnobserved() ||
// Folded early, an assignment hides from later passes the value they fold.
!collapseHooks.finalPass ||
state.inTry ||
state.mayThrow ||
!isSymbolRefNode(lhs) ||
!isLambdaNode(state.defunScope.node) ||
!isUnsharedLocal(state, lhs.definition)
) {
return false;
}
if (isUnaryNode(candidate)) return true;
// A `var` read once goes with its value, so nothing else reads it.
if (isVarDefNode(candidate)) {
return (
Boolean(candidate.init) &&
dropsCandidate(state) &&
makesOnly(state, candidate.init)
);
}
return (
isAssignNode(candidate) &&
candidate.operator === "=" &&
readsOnlyUnsharedLocals(state, candidate.right)
);
};
/**
* terser's `may_modify`.
* @param {CollapseState} state the run's state
* @param {Node} symbol a symbol or a destructuring
* @returns {boolean} whether code outside this function may change it
*/
const mayModify = (state, symbol) => {
// An `DestructuringNode`.
if (!symbol.definition) return true;
const symbolDefinition = symbol.definition;
if (
symbolDefinition.identifiers.length === 1 &&
isSymbolDefunNode(symbolDefinition.identifiers[0])
) {
return false;
}
return definitionMayBeModified(state, symbolDefinition);
};
/**
* @param {CollapseState} state the run's state
* @param {Definition} symbolDefinition a name's definition
* @returns {boolean} whether another function declares or reads the name
*/
const definitionMayBeModified = (state, symbolDefinition) => {
const { defunScope } = state;
if (symbolDefinition.scope.getDefunScope() !== defunScope) {
return true;
}
return symbolDefinition.references.some(
(/** @type {Node} */ reference) =>
reference.scope.getDefunScope() !== defunScope
);
};
/**
* terser's `side_effects_external`.
* @param {CollapseState} state the run's state
* @param {Node} node a node
* @param {boolean=} isTarget whether it is assigned to
* @returns {boolean | null | undefined} whether it assigns to something outside this function
*/
const sideEffectsExternal = (state, node, isTarget) => {
if (isAssignNode(node)) {
return sideEffectsExternal(state, node.left, true);
}
if (isUnaryNode(node)) {
return sideEffectsExternal(state, node.argument, true);
}
if (isVarDefNode(node)) {
return node.init && sideEffectsExternal(state, node.init);
}
if (isTarget) {
if (isDotNode(node)) {
return sideEffectsExternal(state, node.object, true);
}
if (isSubNode(node)) {
return sideEffectsExternal(state, node.object, true);
}
if (isSymbolRefNode(node)) {
return node.definition.scope.getDefunScope() !== state.defunScope;
}
}
return false;
};
/**
* terser's `shadows`: whether a variable the candidate reads would
* resolve to another one where it is moved to.
* @param {Scope} scope the scope it is moved into
* @param {Map<string, { definition: Definition, modified: Node | boolean | undefined }>} values the variables it reads
* @returns {boolean} whether one is shadowed there
*/
const shadows = (scope, values) => {
for (const value of values.values()) {
const lookedUp = scope.findVariable(value.definition.name);
if (lookedUp) {
if (lookedUp === value.definition) continue;
return true;
}
}
return false;
};
/**
* What one `tightenBody` run shares between its steps, held once per
* nesting depth so a run allocates none of it: the compressor, the scope
* the statements declare into, and whether a step changed one.
*/
class TightenState {
constructor() {
/** @type {Compressor} */
this.compressor = /** @type {Compressor} */ (
/** @type {Compressor | null} */ (null)
);
/** @type {Scope} */
this.nearestScope = /** @type {Scope} */ (
/** @type {Scope | null} */ (null)
);
this.changed = false;
}
}
/** @type {TightenState[]} */
const tightenStates = [];
// How many runs are inside one another: a step optimizes what it joins or
// moves, which can tighten a body of its own.
let tightenDepth = 0;
/**
* terser's `tighten_body`: repeats the statement optimizations over a list
* until one pass changes nothing, ten passes at most.
* @param {Node[]} statements the statements, changed in place
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const tightenBody = (statements, compressor) => {
const nearestScope = /** @type {Scope} */ (compressor.find_scope());
const defunScope = nearestScope.getDefunScope();
const { inLoop, inTry } = findLoopScopeTry(compressor);
let state = tightenStates[tightenDepth];
if (state === undefined) {
state = new TightenState();
tightenStates[tightenDepth] = state;
}
tightenDepth++;
state.compressor = compressor;
state.nearestScope = nearestScope;
try {
let maxIterations = 10;
do {
state.changed = false;
eliminateSpuriousBlocks(state, statements);
if (compressor.option("dead_code")) {
eliminateDeadCode(state, statements, compressor);
}
if (compressor.option("if_return")) {
handleIfReturn(state, statements, compressor);
if (
modules.improvements !== undefined &&
modules.improvements.enabled
) {
mergeThrows(state, statements, compressor);
}
}
if (compressor.sequences_limit > 0) {
joinIntoSequences(state, statements, compressor);
joinSequencesIntoStatements(state, statements, compressor);
}
if (compressor.option("join_vars")) {
joinConsecutiveVars(state, statements);
}
if (
compressor.option("collapse_vars") &&
collapse(
statements,
compressor,
nearestScope,
defunScope,
inLoop,
inTry
)
) {
state.changed = true;
}
} while (state.changed && maxIterations-- > 0);
} finally {
tightenDepth--;
state.compressor = /** @type {Compressor} */ (
/** @type {Compressor | null} */ (null)
);
state.nearestScope = /** @type {Scope} */ (
/** @type {Scope | null} */ (null)
);
}
};
/**
* `if (a) throw b; throw c` as `throw a ? b : c`.
* @param {TightenState} state the run's state
* @param {Node[]} statements the statements
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const mergeThrows = (state, statements, compressor) => {
for (let i = statements.length - 2; i >= 0; i--) {
const statement = statements[i];
const next = statements[i + 1];
if (
!modules.ast.isIfNode(statement) ||
statement.alternate ||
!modules.ast.isThrowNode(next)
) {
continue;
}
let thrown = /** @type {Node} */ (statement.consequent);
const block = /** @type {Node[]} */ (thrown.body);
if (isBlockStatementNode(thrown) && block.length === 1) {
thrown = block[0];
}
// A sequence thrown repeats the text gzip matches in the other branch.
if (
!modules.ast.isThrowNode(thrown) ||
isSequenceNode(thrown.argument) ||
isSequenceNode(next.argument)
) {
continue;
}
const value = makeNode(modules.ast.ConditionalNode, statement, {
test: statement.test,
consequent: thrown.argument,
alternate: next.argument
});
statements.splice(
i,
2,
makeNode(modules.ast.ThrowNode, statement, {
argument: ast.optimize(value, compressor)
})
);
state.changed = true;
}
};
/**
* terser's `eliminate_spurious_blocks`: inlines blocks, drops empty
* statements and repeated directives.
* @param {TightenState} state the run's state
* @param {Node[]} statements the statements
* @returns {void}
*/
const eliminateSpuriousBlocks = (state, statements) => {
/** @type {string[]} */
const seenDirectives = [];
for (let i = 0; i < statements.length;) {
const statement = statements[i];
if (
isBlockStatementNode(statement) &&
/** @type {Node[]} */ (statement.body).every(canBeEvictedFromBlock)
) {
state.changed = true;
eliminateSpuriousBlocks(state, /** @type {Node[]} */ (statement.body));
statements.splice(i, 1, .../** @type {Node[]} */ (statement.body));
i += /** @type {Node[]} */ (statement.body).length;
} else if (isEmptyStatementNode(statement)) {
state.changed = true;
if (correcting() && i + 1 < statements.length) {
keepCommentsOf(state.compressor, statement, statements[i + 1]);
}
statements.splice(i, 1);
} else if (isDirectiveNode(statement)) {
if (!seenDirectives.includes(statement.directive)) {
i++;
seenDirectives.push(statement.directive);
} else {
state.changed = true;
statements.splice(i, 1);
}
} else {
i++;
}
}
};
/**
* terser's `can_merge_flow`.
* @param {Node[]} statements the statements
* @param {number} i the index of the `if`
* @param {Compressor} compressor the compressor
* @param {Node} self the node holding the statements
* @param {boolean} inLambda whether it is a function
* @param {Node | false | null | undefined} exit the jump a branch ends in, falsy where it ends in none
* @param {boolean} awaited whether a `return` there awaits its value
* @returns {boolean} whether the statements after the `if` can move into its other branch
*/
const canMergeFlow = (
statements,
i,
compressor,
self,
inLambda,
exit,
awaited
) => {
if (!exit) return false;
for (let j = i + 1, length = statements.length; j < length; j++) {
const statement = statements[j];
if (isDefinitionsLikeNode(statement) && !isVarNode(statement)) {
return false;
}
}
const target = isLoopControlNode(exit)
? compressor.loopcontrol_target(exit)
: null;
return (
(isReturnNode(exit) &&
inLambda &&
isReturnVoid(exit.argument) &&
!(awaited && exit.argument)) ||
(isContinueNode(exit) &&
self === loopBody(/** @type {Node} */ (target))) ||
(isBreakNode(exit) && isBlockStatementNode(target) && self === target)
);
};
/**
* terser's `extract_defuns`: cuts the statements after the `if`, but
* keeps function declarations in place.
* @param {Node[]} statements the statements
* @param {number} i the index of the `if`
* @returns {Node[]} the statements cut
*/
const extractDefuns = (statements, i) => {
const tail = statements.slice(i + 1);
statements.length = i + 1;
return tail.filter((/** @type {Node} */ statement) => {
if (isDefunNode(statement)) {
statements.push(statement);
return false;
}
return true;
});
};
/**
* terser's `handle_if_return`: turns `if`s that end in a jump into
* branches, and returns after them into conditionals.
* @param {TightenState} state the run's state
* @param {Node[]} statements the statements
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const handleIfReturn = (state, statements, compressor) => {
const self = compressor.self();
const multipleIfReturns = hasMultipleIfReturns(statements);
const inLambda = isLambdaNode(self);
const awaited = awaitsReturnValue(compressor);
// Prevent extremely deep nesting: terser#1432, webpack#17548.
const iterationStart = Math.min(statements.length, 500);
let i = iterationStart;
while (--i >= 0) {
let statement = statements[i];
const j = nextIndex(statements, i);
const next = statements[j];
if (inLambda && !next && isReturnNode(statement)) {
if (!statement.argument) {
state.changed = true;
statements.splice(i, 1);
continue;
}
if (
!awaited &&
isUnaryPrefixNode(statement.argument) &&
statement.argument.operator === "void"
) {
state.changed = true;
statements[i] = makeNode(SimpleStatementNode, statement, {
expression: statement.argument.argument
});
continue;
}
}
if (isIfNode(statement)) {
let exit = aborts(/** @type {Node} */ (statement.consequent));
/** @type {Node[] | undefined} */
let newElse;
if (
canMergeFlow(
statements,
i,
compressor,
self,
inLambda,
exit,
awaited
) &&
(newElse = asStatementArrayWithReturn(
/** @type {Node} */ (statement.consequent),
/** @type {Node} */ (exit)
))
) {
if (/** @type {Node} */ (exit).label) {
remove(
/** @type {Node} */ (exit).label.definition.references,
/** @type {Node} */ (exit)
);
}
state.changed = true;
statement = ast.cloneNode(statement);
statement.test = ast.negate(statement.test, compressor);
statement.consequent = makeNode(BlockStatementNode, statement, {
body: [
...asStatementArray(statement.alternate),
...extractDefuns(statements, i)
]
});
statement.alternate = makeNode(BlockStatementNode, statement, {
body: newElse
});
statements[i] = transformNode(statement, compressor);
continue;
}
exit = aborts(statement.alternate);
if (
canMergeFlow(
statements,
i,
compressor,
self,
inLambda,
exit,
awaited
) &&
(newElse = asStatementArrayWithReturn(
statement.alternate,
/** @type {Node} */ (exit)
))
) {
if (/** @type {Node} */ (exit).label) {
remove(
/** @type {Node} */ (exit).label.definition.references,
/** @type {Node} */ (exit)
);
}
state.changed = true;
statement = ast.cloneNode(statement);
statement.consequent = makeNode(
BlockStatementNode,
/** @type {Node | undefined} */ (statement.consequent),
{
body: [
...asStatementArray(/** @type {Node} */ (statement.consequent)),
...extractDefuns(statements, i)
]
}
);
statement.alternate = makeNode(
BlockStatementNode,
statement.alternate,
{
body: newElse
}
);
statements[i] = transformNode(statement, compressor);
continue;
}
}
if (isIfNode(statement) && isReturnNode(statement.consequent)) {
const value = /** @type {Node} */ (statement.consequent).argument;
// if (foo()) return; return; ==> foo(); return;
if (
!value &&
!statement.alternate &&
((inLambda && !next) || (isReturnNode(next) && !next.argument))
) {
state.changed = true;
statements[i] = makeNode(SimpleStatementNode, statement.test, {
expression: statement.test
});
continue;
}
// if (foo()) return x; return y; ==> return foo() ? x : y;
if (
value &&
!statement.alternate &&
isReturnNode(next) &&
next.argument
) {
state.changed = true;
statement = ast.cloneNode(statement);
statement.alternate = next;
statements[i] = transformNode(statement, compressor);
statements.splice(j, 1);
continue;
}
// if (foo()) return x; [ return ; ] ==> return foo() ? x : undefined;
if (
value &&
!awaited &&
!statement.alternate &&
((!next && inLambda && multipleIfReturns) || isReturnNode(next))
) {
state.changed = true;
statement = ast.cloneNode(statement);
statement.alternate =
next ||
makeNode(ReturnNode, statement, {
argument: null
});
statements[i] = transformNode(statement, compressor);
if (next) statements.splice(j, 1);
continue;
}
// if (a) return b; if (c) return d; e; ==> return a ? b : c ? d : void e;
// Without `sequences` this could loop endlessly (terser#866); with it,
// it produces slightly better output.
const previous = statements[prevIndex(statements, i)];
if (
compressor.option("sequences") &&
inLambda &&
!awaited &&
!statement.alternate &&
isIfNode(previous) &&
isReturnNode(previous.consequent) &&
nextIndex(statements, j) === statements.length &&
isSimpleStatementNode(next)
) {
state.changed = true;
statement = ast.cloneNode(statement);
statement.alternate = makeNode(BlockStatementNode, next, {
body: [
next,
makeNode(ReturnNode, next, {
argument: null
})
]
});
statements[i] = transformNode(statement, compressor);
statements.splice(j, 1);
continue;
}
}
}
};
/**
* terser's `eliminate_dead_code`: drops what follows a jump, and jumps to
* where the code goes anyway.
* @param {TightenState} state the run's state
* @param {Node[]} statements the statements
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const eliminateDeadCode = (state, statements, compressor) => {
/**
* Whether a `try` never completes normally: its block leaves and so does its
* `catch`, which a `finally` leaves as they are or leaves itself.
* @param {Node} statement a statement
* @returns {boolean} true when the improve phase reads what follows as dead
*/
const tryAborts = (statement) =>
isTryNode(statement) &&
modules.improvements !== undefined &&
modules.improvements.enabled &&
endsLeaving(statement.block) &&
(!statement.handler ||
endsLeaving(/** @type {Node} */ (statement.handler.body)));
/**
* @param {Node} block a block of the `try`
* @returns {boolean} whether its last statement leaves
*/
const endsLeaving = (block) => {
const body = /** @type {Node[]} */ (block.body);
return body.length > 0 && Boolean(aborts(body[body.length - 1]));
};
/** @type {Node[] | undefined} */
let hasQuit;
const self = compressor.self();
let n = 0;
const length = statements.length;
for (let i = 0; i < length; i++) {
const statement = statements[i];
if (isLoopControlNode(statement)) {
const target = compressor.loopcontrol_target(statement);
if (
(isBreakNode(statement) &&
!isIterationStatementNode(target) &&
loopBody(/** @type {Node} */ (target)) === self) ||
(isContinueNode(statement) &&
loopBody(/** @type {Node} */ (target)) === self)
) {
if (statement.label) {
remove(statement.label.definition.references, statement);
}
} else {
statements[n++] = statement;
}
} else {
statements[n++] = statement;
}
if (aborts(statement) || tryAborts(statement)) {
hasQuit = statements.slice(i + 1);
break;
}
}
statements.length = n;
// Likely a terser bug: this also clears what earlier steps of the pass set.
state.changed = n !== length;
if (hasQuit) {
for (const statement of hasQuit) {
extractFromUnreachableCode(compressor, statement, statements, true);
}
}
};
/**
* terser's `push_seq`: writes the expressions joined so far as one statement.
* @param {Node[]} statements the statements
* @param {number} n where the statement goes
* @param {Node[]} sequence the expressions, at least one
* @returns {void}
*/
const pushSequence = (statements, n, sequence) => {
const body = makeSequence(sequence[0], sequence);
statements[n] = makeNode(SimpleStatementNode, body, {
expression: body
});
};
/**
* Whether a function declared in the statements being tightened is Annex B's,
* which sets its `var` where its statement runs, so it moves before nothing.
* @param {Compressor} compressor the compressor
* @returns {boolean} true in a sloppy block
*/
const assignsBlockFunction = (compressor) =>
correcting() &&
!isScopeNode(compressor.self()) &&
!compressor.has_directive("use strict");
/**
* terser's `sequencesize`: joins consecutive expression statements into
* one sequence.
* @param {TightenState} state the run's state
* @param {Node[]} statements the statements
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const joinIntoSequences = (state, statements, compressor) => {
if (statements.length < 2) return;
// The expressions joined so far, which the statement they become keeps.
/** @type {Node[]} */
let sequence = [];
let n = 0;
const length = statements.length;
for (let i = 0; i < length; i++) {
const statement = statements[i];
if (isSimpleStatementNode(statement)) {
// The limit is above zero here, so a sequence at it holds something.
if (sequence.length >= compressor.sequences_limit) {
pushSequence(statements, n++, sequence);
sequence = [];
}
/** @type {Node | null} */
let body = statement.expression;
if (sequence.length > 0) {
body = ast.dropSideEffectFree(body, compressor);
}
if (body) mergeSequence(sequence, body);
} else if (
(isDefinitionsNode(statement) &&
declarationsOnly(statement) &&
// A `let` moved before the expressions ends their reads' TDZ.
(isVarNode(statement) ||
modules.corrections === undefined ||
!modules.corrections.enabled)) ||
(isDefunNode(statement) && !assignsBlockFunction(compressor))
) {
statements[n++] = statement;
} else {
if (sequence.length !== 0) {
pushSequence(statements, n++, sequence);
sequence = [];
}
statements[n++] = statement;
}
}
if (sequence.length !== 0) pushSequence(statements, n++, sequence);
statements.length = n;
if (n !== length) state.changed = true;
};
/**
* terser's `cons_seq`: the previous statement's expression, then one of
* the statement after it; the caller takes the previous statement out.
* @param {TightenState} state the run's state
* @param {Compressor} compressor the compressor
* @param {Node} previous the expression statement before
* @param {Node} right an expression of the statement after it
* @returns {Node} the two as one, optimized
*/
const consSequence = (state, compressor, previous, right) => {
state.changed = true;
const left = previous.expression;
return transformNode(makeSequence(left, [left, right]), compressor);
};
/**
* @param {Node} node a node of an expression moved into a `for` head
* @returns {boolean | symbol | undefined} true at a scope, abort at an `in`, which the head cannot hold bare
*/
const stopsAtInOrScope = (node) => {
if (isScopeNode(node)) return true;
if (isBinaryNode(node) && node.operator === "in") {
return walkAbort;
}
};
/**
* Whether an expression statement assigns the name an expression reads, so
* the two join as the value: `a=[1];for(let b of a)` as `for(let b of[1])`.
* Joined otherwise, an `of` needs its sequence in parens, costing its `;`.
* @param {Node} previous the expression statement moved
* @param {Node} right the expression of the statement after it
* @returns {boolean} true where the two join without a sequence
*/
const assignsWhatIsRead = (previous, right) => {
const assign = previous.expression;
return (
isAssignNode(assign) &&
assign.operator === "=" &&
isSymbolRefNode(assign.left) &&
isSymbolRefNode(right) &&
assign.left.definition === right.definition
);
};
/**
* Whether an expression moved into a `for…in` or `for…of` head declaring
* its own names reads none of them, which the head holds in a dead zone.
* @param {Node} previous the expression statement moved
* @param {Node} left the head's declaration
* @returns {boolean} true where it reads none
*/
const readsNoneDeclared = (previous, left) => {
if (modules.improvements === undefined || !modules.improvements.enabled) {
return false;
}
/** @type {Set<string>} */
const names = new Set();
walk(left, (/** @type {Node} */ node) => {
if (isSymbolDeclarationNode(node)) names.add(node.name);
});
// A direct `eval` may read any of them.
return !walk(previous.expression, (/** @type {Node} */ node) =>
isSymbolRefNode(node) && (names.has(node.name) || node.name === "eval")
? walkAbort
: undefined
);
};
/**
* terser's `sequencesize_2`: moves an expression statement into the
* head of the statement after it.
* @param {TightenState} state the run's state
* @param {Node[]} statements the statements
* @param {Compressor} compressor the compressor
* @returns {void}
*/
const joinSequencesIntoStatements = (state, statements, compressor) => {
const awaited = awaitsReturnValue(compressor);
let n = 0;
/** @type {Node | null | undefined} */
let previous;
for (let i = 0; i < statements.length; i++) {
const statement = statements[i];
if (previous) {
if (isExitNode(statement)) {
// `return void x` would await what `x; return` does not
if (statement.argument || !awaited || !isReturnNode(statement)) {
n--;
statement.argument = consSequence(
state,
compressor,
previous,
statement.argument ||
transformNode(makeVoidZero(statement), compressor)
);
}
} else if (isForNode(statement)) {
if (!isDefinitionsLikeNode(statement.init)) {
const aborted = walk(previous.expression, stopsAtInOrScope);
if (!aborted) {
if (statement.init) {
n--;
statement.init = consSequence(
state,
compressor,
previous,
statement.init
);
} else {
statement.init = previous.expression;
n--;
state.changed = true;
}
}
}
} else if (isForInNode(statement)) {
if (
!isDefinitionsLikeNode(statement.left) ||
isVarNode(statement.left) ||
((!isForOfNode(statement) ||
assignsWhatIsRead(previous, statement.right)) &&
readsNoneDeclared(previous, statement.left))
) {
n--;
statement.right = consSequence(
state,
compressor,
previous,
statement.right
);
}
} else if (isIfNode(statement)) {
n--;
statement.test = consSequence(
state,
compressor,
previous,
statement.test
);
} else if (isSwitchNode(statement)) {
n--;
statement.discriminant = consSequence(
state,
compressor,
previous,
statement.discriminant
);
} else if (isWithNode(statement)) {
n--;
statement.object = consSequence(
state,
compressor,
previous,
statement.object
);
}
}
if (compressor.option("conditionals") && isIfNode(statement)) {
/** @type {Node[]} */
const declarations = [];
const body = toSimpleStatement(
/** @type {Node} */ (statement.consequent),
declarations
);
const alternative = toSimpleStatement(
statement.alternate,
declarations
);
if (
body !== false &&
alternative !== false &&
declarations.length > 0
) {
const length = declarations.length;
declarations.push(
makeNode(IfNode, statement, {
test: statement.test,
consequent:
body ||
makeNode(
EmptyStatementNode,
/** @type {Node | undefined} */ (statement.consequent)
),
alternate: alternative
})
);
statements.splice(n, 1, ...declarations);
i += length;
n += length + 1;
previous = null;
state.changed = true;
continue;
}
}
statements[n++] = statement;
previous = isSimpleStatementNode(statement) ? statement : null;
}
statements.length = n;
};
/**
* Whether a property of the literal is named something else than a key,
* as terser reads the two: loosely, and under old strict code by the key
* itself too.
* @param {Node} property a property of the literal
* @param {string} key the key an assignment adds
* @param {boolean} byKeyToo whether the key itself is read against the name
* @returns {boolean | string} whether the property is named something else
*/
const namedOtherwise = (property, key, byKeyToo) => {
const name = terserKey(property.key);
if (byKeyToo && !differsLoosely(name, key)) return false;
return name && differsLoosely(/** @type {Node} */ (name).name, key);
};
/**
* Moves stores to the next index of an array literal just declared into
* the literal, as `join_object_assignments` does for an object's keys.
* @param {TightenState} state the run's state
* @param {Node} declaration the declaration of the literal
* @param {Node | null | undefined} body the expression that may store
* @returns {Node[] | false | undefined} the expressions left, where any moved
*/
const joinArrayStores = (state, declaration, body) => {
const { elements } = declaration.init;
for (let i = 0; i < elements.length; i++) {
// A spread leaves the literal's length unknown.
if (isExpansionNode(elements[i])) return;
}
const head = /** @type {Node} */ (body);
/** @type {Node[] | undefined} */
let expressions;
if (isAssignNode(head) && !head.logical) {
expressions = [head];
} else if (isSequenceNode(head)) {
expressions = [...head.expressions];
}
if (!expressions) return;
let trimmed = false;
while (expressions.length) {
const node = expressions[0];
if (
!isAssignNode(node) ||
node.operator !== "=" ||
!isSubNode(node.left) ||
!isSymbolRefNode(node.left.object) ||
node.left.object.name !== declaration.id.name ||
!isNumberNode(node.left.property) ||
node.left.property.value !== elements.length ||
!ast.isConstantExpression(node.right, state.nearestScope)
) {
break;
}
elements.push(node.right);
expressions.shift();
trimmed = true;
}
return trimmed && expressions;
};
/**
* terser's `join_object_assignments`: moves assignments to properties of
* an object literal just declared into the literal.
* @param {TightenState} state the run's state
* @param {Node | undefined} definitions the statement before
* @param {Node | null | undefined} body the expression that may assign
* @returns {Node[] | false | undefined} the expressions left, where any moved
*/
const joinObjectAssignments = (state, definitions, body) => {
if (!isDefinitionsNode(definitions)) return;
const list = /** @type {Node} */ (definitions).declarations;
const last = list[list.length - 1];
if (isArrayNode(last.init)) {
return collapseHooks.joinsArrayStores !== null &&
collapseHooks.joinsArrayStores(state.compressor)
? joinArrayStores(state, last, body)
: undefined;
}
if (!isObjectNode(last.init)) return;
if (
collapseHooks.keepsAssignmentsOut !== null &&
collapseHooks.keepsAssignmentsOut(last.init)
) {
return;
}
const { compressor, nearestScope } = state;
/** @type {Node[] | undefined} */
let expressions;
const head = /** @type {Node} */ (body);
if (isAssignNode(head) && !head.logical) {
expressions = [head];
} else if (isSequenceNode(head)) {
expressions = [...head.expressions];
}
if (!expressions) return;
let trimmed = false;
do {
const node = expressions[0];
if (!isAssignNode(node)) break;
if (node.operator !== "=") break;
if (!isPropAccessNode(node.left)) break;
const symbol = node.left.object;
if (!isSymbolRefNode(symbol)) break;
if (last.id.name !== symbol.name) break;
if (!ast.isConstantExpression(node.right, nearestScope)) break;
/** @type {EvaluatedValue | Node} */
let key = terserProperty(node.left);
if (isSyntaxNode(key)) {
key = ast.evaluate(key, compressor);
}
if (isSyntaxNode(key)) break;
key = String(key);
const byKeyToo =
compressor.option("ecma") < 2015 &&
compressor.has_directive("use strict");
const { properties } = last.init;
let differs = true;
/** @type {Node | undefined} */
let existing;
for (let i = 0; i < properties.length; i++) {
const property = properties[i];
if (!namedOtherwise(property, key, /** @type {boolean} */ (byKeyToo))) {
differs = false;
break;
}
if (existing === undefined && terserKey(property.key) === key) {
existing = property;
}
}
if (!differs) break;
if (!existing) {
properties.push(
makeNode(ObjectKeyValNode, node, {
key: SymbolPropertyNode({ name: key }),
value: node.right,
computed: false,
kind: "init",
method: false,
shorthand: false
})
);
} else {
existing.value = SequenceNode({
startToken: existing.startToken,
expressions: [
ast.cloneNode(/** @type {Node} */ (existing.value)),
ast.cloneNode(node.right)
],
endToken: existing.endToken
});
}
expressions.shift();
trimmed = true;
} while (expressions.length);
return trimmed && expressions;
};
/**
* terser's `extract_object_assignments`: moves object assignments out of
* a statement's head; the caller keeps the statement.
* @param {TightenState} state the run's state
* @param {Node | undefined} previous the statement before it
* @param {Node} value the statement's head
* @returns {Node} what is left of it
*/
const extractObjectAssignments = (state, previous, value) => {
const expressions = joinObjectAssignments(state, previous, value);
if (expressions) {
state.changed = true;
if (expressions.length) {
return makeSequence(value, expressions);
} else if (isSequenceNode(value)) {
return ast.tailNode(value).left;
}
return value.left;
}
return value;
};
/**
* terser's `join_consecutive_vars`: joins neighboring definitions, and
* moves object assignments into the literal just declared.
* @param {TightenState} state the run's state
* @param {Node[]} statements the statements
* @returns {void}
*/
const joinConsecutiveVars = (state, statements) => {
/** @type {Node | undefined} */
let definitions;
let j = -1;
for (let i = 0, length = statements.length; i < length; i++) {
const statement = statements[i];
const previous = statements[j];
if (isDefinitionsNode(statement)) {
if (previous && ast.kindOf(previous) === ast.kindOf(statement)) {
previous.declarations = [
...previous.declarations,
...statement.declarations
];
state.changed = true;
} else if (
definitions &&
ast.kindOf(definitions) === ast.kindOf(statement) &&
declarationsOnly(statement)
) {
definitions.declarations = [
...definitions.declarations,
...statement.declarations
];
state.changed = true;
} else {
statements[++j] = statement;
definitions = statement;
}
} else if (
isUsingNode(statement) &&
isUsingNode(previous) &&
previous.await === statement.await
) {
previous.declarations = [
...previous.declarations,
...statement.declarations
];
} else if (isExitNode(statement)) {
statements[++j] = statement;
statement.argument = extractObjectAssignments(
state,
previous,
statement.argument
);
} else if (isForNode(statement)) {
const expressions = joinObjectAssignments(
state,
previous,
statement.init
);
if (expressions) {
state.changed = true;
statement.init = /** @type {Node} */ (
expressions.length
? makeSequence(statement.init, expressions)
: null
);
statements[++j] = statement;
} else if (
isVarNode(previous) &&
(!statement.init ||
ast.kindOf(statement.init) === ast.kindOf(previous))
) {
if (statement.init) {
previous.declarations = [
...previous.declarations,
...statement.init.declarations
];
}
statement.init = previous;
statements[j] = statement;
state.changed = true;
} else if (
isVarNode(definitions) &&
isVarNode(statement.init) &&
declarationsOnly(statement.init)
) {
const declared = /** @type {Node} */ (definitions);
declared.declarations = [
...declared.declarations,
...statement.init.declarations
];
statement.init = /** @type {Node} */ (
/** @type {Node | null} */ (null)
);
statements[++j] = statement;
state.changed = true;
} else {
statements[++j] = statement;
}
} else if (isForInNode(statement)) {
statements[++j] = statement;
statement.right = extractObjectAssignments(
state,
previous,
statement.right
);
} else if (isIfNode(statement)) {
statements[++j] = statement;
statement.test = extractObjectAssignments(
state,
previous,
statement.test
);
} else if (isSimpleStatementNode(statement)) {
const expressions = joinObjectAssignments(
state,
previous,
statement.expression
);
if (expressions) {
state.changed = true;
if (!expressions.length) continue;
statement.expression = makeSequence(
statement.expression,
expressions
);
}
statements[++j] = statement;
} else if (isSwitchNode(statement)) {
statements[++j] = statement;
statement.discriminant = extractObjectAssignments(
state,
previous,
statement.discriminant
);
} else if (isWithNode(statement)) {
statements[++j] = statement;
statement.object = extractObjectAssignments(
state,
previous,
statement.object
);
} else {
statements[++j] = statement;
}
}
statements.length = j + 1;
};
return { tightenBody, extractFromUnreachableCode };
};
/**
* @typedef {object} HoistHooks what later phases add to splitting objects
* @property {((scope: Node, definition: Definition, value: Node) => boolean) | null} splits whether an object read only through its properties splits, which terser decides from `escaped` alone
*/
/**
* @typedef {object} InlineHooks what later phases add to inlining
* @property {((call: Node, reference: Node, value: Node, compressor: Compressor) => boolean) | null} inlinesIntoPureArgument whether a value read once may replace its read in a call terser holds pure, which it otherwise keeps out
* @property {((reference: Node, definition: Definition, compressor: Compressor) => boolean) | null} inlinesIntoCallee whether a function read once, in another scope, is inlined where its value is only called
* @property {((call: Node, lambda: Node, index: number, compressor: Compressor) => boolean) | null} aliasesParameter whether a parameter of a function flattened into its call, its name taken around the call, reads the argument's variable instead
* @property {((call: Node, lambda: Node, index: number, scope: Node, inLoop: boolean, compressor: Compressor) => Definition | undefined) | null} reusesVariable the variable of a parameter's name, read nowhere but in its argument, that a function flattened into its call assigns the argument and reads instead
* @property {((node: Node, definition: Definition) => boolean) | null} writesVariable whether a node writes a variable, counting the pattern writes `reduce_vars` does not
* @property {((lambda: Node, compressor: Compressor) => boolean | undefined) | null} keepsSloppyLambda whether a sloppy function read once stays where strict code would run it
* @property {((call: Node, lambda: Node, index: number) => boolean) | null} readsConstant whether a parameter of a function flattened into its call reads its argument, a constant, in place of a variable
* @property {((value: Node, definition: Definition) => boolean) | null} keepsReassignedDeclaration whether a function declaration read once stays a declaration, its name being written
* @property {((value: Node, compressor: Compressor) => boolean) | null} keepsInferredName whether an anonymous function or class read once stays bound, its name coming from the binding
* @property {((lambda: Node, compressor: Compressor) => boolean) | null} flattensInLoop whether a function read once, called in a loop, is inlined to be flattened into its call there, which terser keeps out of loops
* @property {((lambda: Node, compressor: Compressor) => boolean) | null} inlinesIntoDefaultExport whether a named function read once, by `export default`, becomes the exported function, which terser keeps declared apart
* @property {(() => boolean) | null} arrayPrototypePatched whether the program patches `Array.prototype`, whose iterator or `toString` may then run code
*/
/**
* Builds webpack's `compress/inline.js`: a variable's value inlined into its
* reference, and a function inlined into its call.
* @param {TerserModules} modules terser's modules
* @returns {{ inlineIntoSymbolRef: OptimizerHelpers["inlineIntoSymbolRef"], inlineIntoCall: OptimizerHelpers["inlineIntoCall"] }} terser's `inline_into_symbolref` and `inline_into_call`
*/
const createInline = (modules) => {
const { ast, common, flags, utils } = modules;
const {
isArrayNode,
AssignNode,
isBlockNode,
CallNode,
isCallNode,
isCatchNode,
isClassNode,
ClassExpressionNode,
isDefaultAssignNode,
isDefClassNode,
isDefunNode,
isDestructuringNode,
isEmptyStatementNode,
isExpansionNode,
isExportNode,
FunctionNode,
isFunctionNode,
isIterationStatementNode,
isLambdaNode,
isSyntaxNode,
NumberNode,
isNumberNode,
isObjectNode,
isObjectKeyValNode,
isPropAccessNode,
isReturnNode,
isScopeNode,
isSimpleStatementNode,
isStatementNode,
isStringNode,
isSymbolDefunNode,
isSymbolFunargNode,
SymbolLambdaNode,
isSymbolLambdaNode,
isSymbolNode,
SymbolRefNode,
isSymbolRefNode,
SymbolVarNode,
isThisNode,
isToplevelNode,
UnaryPrefixNode,
VarNode,
isVarNode,
VarDefNode,
walk,
_INLINE,
_NOINLINE,
_PURE
} = ast;
const { make_void_0: makeVoid0, has_annotation: hasAnnotation } = utils;
const {
SQUEEZED,
INLINED,
UNUSED,
has_flag: hasFlag,
set_flag: setFlag
} = flags;
const {
make_sequence: makeSequence,
best_of: bestOf,
make_node_from_constant: makeNodeFromConstant,
identifier_atom: identifierAtom,
is_empty: isEmpty,
is_func_expr: isFunctionExpression,
is_iife_call: isIifeCall,
is_reachable: isReachable,
is_recursive_ref: isRecursiveRef,
retain_top_func: retainTopFunction
} = common;
/** @type {InlineHooks} */
const inlineHooks = {
inlinesIntoPureArgument: null,
inlinesIntoCallee: null,
aliasesParameter: null,
reusesVariable: null,
writesVariable: null,
keepsSloppyLambda: null,
readsConstant: null,
keepsReassignedDeclaration: null,
arrayPrototypePatched: null,
keepsInferredName: null,
inlinesIntoDefaultExport: null,
flattensInLoop: null
};
modules.webpackInline = inlineHooks;
/**
* @param {Compressor} compressor the compressor
* @returns {boolean} whether the node visited sits in an array or object literal of its statement
*/
const withinArrayOrObjectLiteral = (compressor) => {
let node;
let level = 0;
while ((node = compressor.parent(level++))) {
if (isStatementNode(node)) return false;
if (isArrayNode(node) || isObjectKeyValNode(node) || isObjectNode(node)) {
return true;
}
}
return false;
};
/**
* Whether a scope reads, under a name the pulled scope encloses, some other
* variable than the pulled scope does.
* @param {Scope} scope the scope pulled into
* @param {Node} pulled the function or class whose scope is pulled
* @returns {boolean} true when pulling it in would rebind a name
*/
const scopeEnclosesVariablesInThisScope = (scope, pulled) => {
const pulledScope = pulled.block_scope;
for (const enclosed of pulledScope.enclosed) {
if (pulledScope.getBinding(enclosed.name) !== undefined) {
continue;
}
const lookedUp = scope.findVariable(enclosed.name);
if (lookedUp) {
if (lookedUp === enclosed) continue;
return true;
}
}
return false;
};
/**
* The `top_retain` check of a constant: a retained one is still inlined when
* its value prints no longer than its name.
* @param {Definition} definition the constant
* @param {Node | undefined} fixedValue its value
* @returns {boolean} true when it is kept
*/
const isConstSymbolShorterThanInitValue = (definition, fixedValue) => {
if (definition.identifiers.length === 1 && fixedValue) {
const initValueLength = ast.nodeSize(fixedValue);
const identifierLength = definition.name.length;
return initValueLength > identifierLength;
}
return true;
};
/**
* Keeps functions and classes out of loops, for performance.
* @param {Compressor} compressor the compressor
* @param {Node | undefined} maybeLambda what would be inlined
* @returns {boolean} true when it is not inlined
*/
const mustNotInlineLambdaInLoop = (compressor, maybeLambda) =>
(isLambdaNode(maybeLambda) || isClassNode(maybeLambda)) &&
Boolean(compressor.is_within_loop());
/**
* terser's `inline_into_symbolref`.
* @param {Node} self a reference
* @param {Compressor} compressor the compressor
* @returns {Node} what replaces it
*/
const inlineIntoSymbolRef = (self, compressor) => {
let fixed = ast.fixedValue(self);
// Without a value, a reference stays as it is, but where `top_retain`
// forgets the value of what it keeps: the key check walks every parent.
if (!fixed && !compressor.top_retain) return self;
if (
compressor.in_computed_key() &&
!(
modules.improvements !== undefined &&
modules.improvements.enabled &&
(isStringNode(fixed) || isNumberNode(fixed)) &&
// Read elsewhere too, the binding stays and the key only grows.
self.definition.references.length === 1
)
) {
return self;
}
const parent = compressor.parent();
const definition = self.definition;
// Read only where a function or class may be inlined.
/** @type {Scope | undefined} */
let nearestScope;
if (
compressor.top_retain &&
definition.global &&
compressor.top_retain(definition) &&
isConstSymbolShorterThanInitValue(
definition,
/** @type {Node | undefined} */ (fixed)
)
) {
definition.fixed = false;
definition.single_use = false;
return self;
}
if (
mustNotInlineLambdaInLoop(
compressor,
/** @type {Node | undefined} */ (fixed)
)
) {
return self;
}
/** @type {boolean | string | undefined} */
let singleUse =
definition.single_use &&
!(
(isCallNode(parent) &&
ast.isCalleePure(parent, compressor) &&
!(
inlineHooks.inlinesIntoPureArgument !== null &&
inlineHooks.inlinesIntoPureArgument(
parent,
self,
/** @type {Node} */ (fixed),
compressor
)
)) ||
hasAnnotation(parent, _NOINLINE)
) &&
!(
isExportNode(parent) &&
isLambdaNode(fixed) &&
/** @type {Node} */ (fixed).id &&
!(
inlineHooks.inlinesIntoDefaultExport !== null &&
inlineHooks.inlinesIntoDefaultExport(
/** @type {Node} */ (fixed),
compressor
)
)
);
if (singleUse && isSyntaxNode(fixed)) {
singleUse =
!ast.hasSideEffects(fixed, compressor) &&
!ast.mayThrow(fixed, compressor);
}
if (isClassNode(fixed) && definition.scope !== self.scope) {
return self;
}
if (singleUse && (isLambdaNode(fixed) || isClassNode(fixed))) {
if (retainTopFunction(/** @type {Node} */ (fixed), compressor)) {
singleUse = false;
} else if (
definition.scope !== self.scope &&
(definition.escaped === 1 ||
hasFlag(/** @type {Node} */ (fixed), INLINED) ||
withinArrayOrObjectLiteral(compressor) ||
!compressor.option("reduce_funcs"))
) {
singleUse = false;
} else if (isRecursiveRef(compressor, definition)) {
singleUse = false;
} else if (
definition.scope !== self.scope ||
isSymbolFunargNode(definition.identifiers[0])
) {
singleUse = ast.isConstantExpression(
/** @type {Node} */ (fixed),
self.scope
);
if (singleUse === "f") {
let scope = /** @type {Scope | null} */ (self.scope);
do {
const { node } = /** @type {Scope} */ (scope);
if (isDefunNode(node) || isFunctionExpression(node)) {
setFlag(node, INLINED);
}
} while ((scope = /** @type {Scope} */ (scope).upper));
}
}
}
if (
singleUse &&
isLambdaNode(fixed) &&
inlineHooks.keepsSloppyLambda !== null &&
inlineHooks.keepsSloppyLambda(/** @type {Node} */ (fixed), compressor)
) {
singleUse = false;
}
if (
singleUse &&
inlineHooks.keepsReassignedDeclaration !== null &&
inlineHooks.keepsReassignedDeclaration(
/** @type {Node} */ (fixed),
definition
)
) {
singleUse = false;
}
if (
singleUse &&
inlineHooks.keepsInferredName !== null &&
inlineHooks.keepsInferredName(/** @type {Node} */ (fixed), compressor)
) {
singleUse = false;
}
if (singleUse && (isLambdaNode(fixed) || isClassNode(fixed))) {
nearestScope = /** @type {Scope} */ (compressor.find_scope());
singleUse =
!scopeEnclosesVariablesInThisScope(
nearestScope,
/** @type {Node} */ (fixed)
) &&
(definition.scope === self.scope ||
(isCallNode(parent) &&
parent.callee === self &&
!(
/** @type {Node} */ (fixed).id &&
/** @type {Node} */ (fixed).id.definition.recursive_refs > 0
)) ||
(inlineHooks.inlinesIntoCallee !== null &&
inlineHooks.inlinesIntoCallee(self, definition, compressor)));
}
if (singleUse && fixed) {
if (isDefClassNode(fixed)) {
setFlag(fixed, SQUEEZED);
fixed = makeNode(ClassExpressionNode, fixed, fixed);
}
if (isDefunNode(fixed)) {
setFlag(fixed, SQUEEZED);
const declaration = fixed;
fixed = makeNode(FunctionNode, fixed, fixed);
// The declaration stays in its body until dropped, so the expression
// takes lists of its own, which no transform copies.
fixed.params = [...declaration.params];
/** @type {Node} */ (fixed.body).body = [
.../** @type {Node[]} */ (/** @type {Node} */ (declaration.body).body)
];
}
if (definition.recursive_refs > 0 && isSymbolDefunNode(fixed.id)) {
const defunDefinition = fixed.id.definition;
let lambdaDefinition = fixed.block_scope.getBinding(fixed.id.name);
let name = lambdaDefinition && lambdaDefinition.identifiers[0];
if (!isSymbolLambdaNode(name)) {
name = makeNode(SymbolLambdaNode, fixed.id, fixed.id);
name.scope = fixed.block_scope;
fixed.id = name;
lambdaDefinition = ast.defineFunction(fixed.block_scope, name);
}
walk(fixed, (/** @type {Node} */ node) => {
if (isSymbolRefNode(node) && node.definition === defunDefinition) {
node.definition = /** @type {Definition} */ (lambdaDefinition);
/** @type {Definition} */ (lambdaDefinition).references.push(node);
}
});
} else if (
isSymbolDefunNode(fixed.id) &&
modules.corrections !== undefined &&
modules.corrections.enabled
) {
// A declaration's name, kept in the expression, would still declare
// it where the declaration stood, which the mangler then reads.
fixed.id = makeNode(SymbolLambdaNode, fixed.id, fixed.id);
}
if (
(isLambdaNode(fixed) || isClassNode(fixed)) &&
fixed.block_scope.upper !== nearestScope
) {
fixed = ast.cloneNode(fixed, true, compressor.get_toplevel());
ast.addChildScope(/** @type {Scope} */ (nearestScope), fixed);
}
return ast.optimize(fixed, compressor);
}
// multiple uses
if (fixed) {
/** @type {Node | undefined} */
let replace;
if (isThisNode(fixed)) {
if (
!isSymbolFunargNode(definition.identifiers[0]) &&
definition.references.every(
(/** @type {Node} */ reference) =>
definition.scope === reference.scope
)
) {
replace = fixed;
}
} else {
const evaluated = ast.evaluate(fixed, compressor);
if (
evaluated !== fixed &&
(compressor.option("unsafe_regexp") || !(evaluated instanceof RegExp))
) {
replace = makeNodeFromConstant(
/** @type {EvaluatedValue} */ (evaluated),
fixed
);
}
}
if (replace) {
const nameLength = ast.nodeSize(self, compressor);
// A boolean written as `!0` or `!1` here, not the word terser sizes and
// leaves for a later pass to shorten; read as an object it stays named,
// as `(!0).p` needs parentheses.
if (
(ast.isTrueNode(replace) || ast.isFalseNode(replace)) &&
modules.improvements !== undefined &&
modules.improvements.enabled
) {
const parent = compressor.parent();
if (isPropAccessNode(parent) && parent.object === self) return self;
replace = ast.optimize(replace, compressor);
}
const replaceSize = ast.nodeSize(replace, compressor);
let overhead = 0;
if (compressor.option("unused") && !compressor.exposed(definition)) {
overhead =
(nameLength + 2 + ast.nodeSize(fixed, compressor)) /
(definition.references.length - definition.assignments);
}
if (replaceSize <= nameLength + overhead) {
return replace;
}
}
}
return self;
};
/**
* What `inlineIntoCall` reads while it decides to flatten a call's function
* into the scope around the call, held once: nothing it reads optimizes, so
* no call's decision runs inside another's.
*/
class InlineState {
constructor() {
/** @type {Node} */
this.self = NO_NODE;
/** @type {Compressor} */
this.compressor = /** @type {Compressor} */ (
/** @type {Compressor | null} */ (null)
);
/** @type {Node} */
this.lambda = NO_NODE;
// The node of the scope the call sits in.
/** @type {Node} */
this.scope = NO_NODE;
/** @type {Definition[] | undefined} */
this.inLoop = undefined;
// The parameters read as the variable their argument names.
/** @type {Node[] | undefined} */
this.aliases = undefined;
// The parameters read as their argument, a constant.
/** @type {Node[] | undefined} */
this.constants = undefined;
// The parameters read as the variable of their name, assigned the argument.
/** @type {Map<Node, Definition> | undefined} */
this.reuses = undefined;
this.level = -1;
}
}
const inlineState = new InlineState();
/**
* Lets go of what a decision read, so none of it outlives the decision.
* @param {InlineState} state the state
* @returns {void}
*/
const releaseInlineState = (state) => {
state.self = NO_NODE;
state.compressor = /** @type {Compressor} */ (
/** @type {Compressor | null} */ (null)
);
state.lambda = NO_NODE;
state.scope = NO_NODE;
state.inLoop = undefined;
state.aliases = undefined;
state.constants = undefined;
state.reuses = undefined;
state.level = -1;
};
/**
* @param {Node[]} args a call's arguments
* @returns {boolean} whether none of them is spread
*/
const hasNoSpread = (args) => {
for (let i = 0; i < args.length; i++) {
if (isExpansionNode(args[i])) return false;
}
return true;
};
/**
* The expression a statement of the body leaves as the call's value,
* not copied, as `flattenFunction` copies what it inlines.
* @param {InlineState} state the decision's state
* @param {Node | false | null | undefined} bodyStatement the statement
* @returns {Node | undefined} the value, or undefined where the statement has none
*/
const returnValue = (state, bodyStatement) => {
if (!bodyStatement) return makeVoid0(state.self);
if (isReturnNode(bodyStatement)) {
if (!bodyStatement.argument) return makeVoid0(state.self);
return bodyStatement.argument;
}
if (isSimpleStatementNode(bodyStatement)) {
return makeNode(UnaryPrefixNode, bodyStatement, {
operator: "void",
argument: bodyStatement.expression
});
}
return undefined;
};
/**
* The value of the body when it is `var`s and one statement at most,
* or a single statement below `inline: 3`.
* @param {InlineState} state the decision's state
* @param {Node | false | undefined} firstStatement the body's first statement
* @returns {Node | false | undefined} the value, or falsy when the body does not flatten
*/
const canFlattenBody = (state, firstStatement) => {
const body = /** @type {Node} */ (state.lambda.body).body;
const length = /** @type {Node[]} */ (body).length;
if (/** @type {number} */ (state.compressor.option("inline")) < 3) {
return length === 1 && returnValue(state, firstStatement);
}
/** @type {Node | null} */
let onlyStatement = null;
for (let i = 0; i < length; i++) {
const line = /** @type {Node[]} */ (body)[i];
if (isVarNode(line)) {
if (
onlyStatement &&
!line.declarations.every((/** @type {Node} */ varDef) => !varDef.init)
) {
return false;
}
} else if (onlyStatement) {
return false;
} else if (!isEmptyStatementNode(line)) {
onlyStatement = line;
}
}
return returnValue(state, onlyStatement);
};
/**
* @param {InlineState} state the decision's state
* @param {Set<string>} blockScoped the names block scopes around the call declare
* @param {boolean} safeToInject whether the scope takes new variables
* @returns {boolean} whether every parameter can become a variable of the scope
*/
const canInjectArgs = (state, blockScoped, safeToInject) => {
const { lambda, scope, inLoop } = state;
const length = lambda.params.length;
for (let i = 0; i < length; i++) {
const arg = lambda.params[i];
if (isDefaultAssignNode(arg)) {
if (hasFlag(arg.left, UNUSED)) continue;
return false;
}
if (isDestructuringNode(arg)) return false;
if (isExpansionNode(arg)) {
if (hasFlag(arg.argument, UNUSED)) continue;
return false;
}
if (hasFlag(arg, UNUSED)) continue;
if (
inlineHooks.readsConstant !== null &&
inlineHooks.readsConstant(state.self, lambda, i)
) {
(state.constants || (state.constants = [])).push(arg);
continue;
}
if (!safeToInject || identifierAtom.has(arg.name)) return false;
if (
blockScoped.has(arg.name) ||
scope.block_scope.conflictingDefinition(arg.name)
) {
// Both re-point the body's names: a function `__INLINE__` flattens
// stays for its other calls, so their copies would read this scope.
if (
state.self.callee !== lambda &&
hasAnnotation(state.self, _INLINE)
) {
return false;
}
if (
inlineHooks.aliasesParameter !== null &&
inlineHooks.aliasesParameter(state.self, lambda, i, state.compressor)
) {
(state.aliases || (state.aliases = [])).push(arg);
} else {
// The improve phase, always installed, sets the hook.
const reusesVariable =
/** @type {NonNullable<InlineHooks["reusesVariable"]>} */
(inlineHooks.reusesVariable);
const variable = reusesVariable(
state.self,
lambda,
i,
scope,
inLoop !== undefined,
state.compressor
);
if (variable === undefined) return false;
(state.reuses || (state.reuses = new Map())).set(arg, variable);
}
} else if (
state.self.callee === lambda &&
inlineHooks.aliasesParameter !== null &&
inlineHooks.aliasesParameter(state.self, lambda, i, state.compressor)
) {
// Read in place, the argument needs no `var` holding a copy.
(state.aliases || (state.aliases = [])).push(arg);
}
if (inLoop) inLoop.push(arg.definition);
}
return true;
};
/**
* @param {InlineState} state the decision's state
* @param {Set<string>} blockScoped the names block scopes around the call declare
* @param {boolean} safeToInject whether the scope takes new variables
* @returns {boolean} whether every `var` of the body can become a variable of the scope
*/
const canInjectVars = (state, blockScoped, safeToInject) => {
const { lambda, scope, inLoop } = state;
const length = /** @type {Node[]} */ (
/** @type {Node} */ (lambda.body).body
).length;
for (let i = 0; i < length; i++) {
const bodyStatement = /** @type {Node[]} */ (
/** @type {Node} */ (lambda.body).body
)[i];
if (!isVarNode(bodyStatement)) continue;
if (!safeToInject) return false;
for (let j = bodyStatement.declarations.length; --j >= 0;) {
const name = bodyStatement.declarations[j].id;
if (
isDestructuringNode(name) ||
blockScoped.has(name.name) ||
identifierAtom.has(name.name) ||
scope.block_scope.conflictingDefinition(name.name)
) {
return false;
}
if (inLoop) inLoop.push(name.definition);
}
}
return true;
};
/**
* Finds the scope the call sits in, and whether the parameters and `var`s
* of the function can move into it.
* @param {InlineState} state the decision's state
* @returns {boolean} true when they can
*/
const canInjectSymbols = (state) => {
const { compressor } = state;
/** @type {Set<string>} */
const blockScoped = new Set();
let scope;
do {
scope = compressor.parent(++state.level);
if (ast.isBlockScope(scope) && scope.block_scope) {
const { variables } = scope.block_scope;
for (let i = 0; i < variables.length; i++) {
blockScoped.add(variables[i].name);
}
}
if (isCatchNode(scope)) {
if (scope.param) {
blockScoped.add(scope.param.name);
}
} else if (isIterationStatementNode(scope)) {
state.inLoop = [];
} else if (isSymbolRefNode(scope) && isScopeNode(ast.fixedValue(scope))) {
state.scope = scope;
return false;
}
} while (!isScopeNode(scope));
state.scope = scope;
const safeToInject = !isToplevelNode(scope) || compressor.toplevel.vars;
const inline = /** @type {number} */ (compressor.option("inline"));
if (!canInjectVars(state, blockScoped, inline >= 3 && safeToInject)) {
return false;
}
if (!canInjectArgs(state, blockScoped, inline >= 2 && safeToInject)) {
return false;
}
const { inLoop } = state;
return !inLoop || inLoop.length === 0 || !isReachable(state.lambda, inLoop);
};
/**
* Declares a name in the scope once, and assigns it the value when there is one.
* @param {InlineState} state the decision's state
* @param {Node[]} declarations the `var` definitions to add
* @param {Node[]} expressions the expressions the call becomes
* @param {Node} name the name
* @param {Node | undefined} value its value
* @returns {void}
*/
const appendVar = (state, declarations, expressions, name, value) => {
const definition = name.definition;
// Only a parameter of the same name has declared it already.
const scopeOfCall = state.scope.block_scope;
const alreadyAppended = scopeOfCall.getBinding(name.name) !== undefined;
if (!alreadyAppended) {
scopeOfCall.bind(definition);
scopeOfCall.enclosed.push(definition);
declarations.push(
makeNode(VarDefNode, name, {
id: name,
init: null
})
);
}
const symbol = makeNode(SymbolRefNode, name, name);
definition.references.push(symbol);
if (value) {
expressions.push(
makeNode(AssignNode, state.self, {
operator: "=",
logical: false,
left: symbol,
right: ast.cloneNode(value)
})
);
}
};
/**
* The call's arguments, each assigned to its parameter or kept for its effects.
* @param {InlineState} state the decision's state
* @param {Node[]} declarations the `var` definitions to add
* @param {Node[]} expressions the expressions the call becomes
* @returns {void}
*/
const flattenArgs = (state, declarations, expressions) => {
const { self, lambda, scope, inLoop } = state;
const length = lambda.params.length;
for (let i = self.arguments.length; --i >= length;) {
expressions.push(self.arguments[i]);
}
const { aliases, constants, reuses } = state;
for (let i = length; --i >= 0;) {
const name = lambda.params[i];
let value = self.arguments[i];
if (constants !== undefined && constants.includes(name)) continue;
const reused = reuses !== undefined ? reuses.get(name) : undefined;
if (reused !== undefined) {
const parameter = name.definition;
walk(/** @type {Node} */ (lambda.body), (/** @type {Node} */ node) => {
if (isSymbolNode(node) && node.definition === parameter) {
node.definition = reused;
// The parameter's value was the argument, not the variable's.
node.fixed = false;
reused.references.push(node);
}
});
// The variable is written now, so no read takes its old value as fixed.
reused.fixed = false;
reused.assignments += parameter.assignments;
// `reduce_vars` counted no pattern write the body makes of it.
const writesVariable =
/** @type {NonNullable<InlineHooks["writesVariable"]>} */
(inlineHooks.writesVariable);
if (writesVariable(/** @type {Node} */ (lambda.body), reused)) {
reused.assignments++;
}
// `reusesVariable` takes no parameter without an argument.
const argument = /** @type {Node} */ (value);
// Passed itself, the variable already holds the argument.
if (!(isSymbolRefNode(argument) && argument.definition === reused)) {
reused.assignments++;
const symbol = makeNode(SymbolRefNode, name, reused.identifiers[0]);
symbol.definition = reused;
reused.references.push(symbol);
expressions.push(
makeNode(AssignNode, self, {
operator: "=",
logical: false,
left: symbol,
right: argument
})
);
}
} else if (aliases !== undefined && aliases.includes(name)) {
const parameter = name.definition;
const variable = value.definition;
walk(/** @type {Node} */ (lambda.body), (/** @type {Node} */ node) => {
if (isSymbolRefNode(node) && node.definition === parameter) {
node.definition = variable;
node.name = variable.name;
variable.references.push(node);
}
});
// Only a `let` or `const` read can throw, in its dead zone.
if (!ast.isSymbolVarNode(variable.identifiers[0])) {
expressions.push(value);
}
} else if (
hasFlag(name, UNUSED) ||
!name.name ||
scope.block_scope.conflictingDefinition(name.name)
) {
if (value) expressions.push(value);
} else {
const symbol = makeNode(SymbolVarNode, name, name);
name.definition.identifiers.push(symbol);
if (!value && inLoop) value = makeVoid0(self);
appendVar(state, declarations, expressions, symbol, value);
}
}
declarations.reverse();
expressions.reverse();
};
/**
* The body's `var`s, each assigned its value, and reset in a loop.
* @param {InlineState} state the decision's state
* @param {Node[]} declarations the `var` definitions to add
* @param {Node[]} expressions the expressions the call becomes
* @returns {void}
*/
const flattenVars = (state, declarations, expressions) => {
const { lambda, inLoop } = state;
let position = expressions.length;
const lines = /** @type {Node[]} */ (/** @type {Node} */ (lambda.body).body)
.length;
for (let i = 0; i < lines; i++) {
const bodyStatement = /** @type {Node[]} */ (
/** @type {Node} */ (lambda.body).body
)[i];
if (!isVarNode(bodyStatement)) continue;
const count = bodyStatement.declarations.length;
for (let j = 0; j < count; j++) {
const varDef = bodyStatement.declarations[j];
const name = varDef.id;
appendVar(state, declarations, expressions, name, varDef.init);
if (
inLoop &&
lambda.params.every(
(/** @type {Node} */ argname) => argname.name !== name.name
)
) {
const variableDefinition = /** @type {Definition} */ (
lambda.block_scope.getBinding(name.name)
);
const symbol = makeNode(SymbolRefNode, name, name);
variableDefinition.references.push(symbol);
expressions.splice(
position++,
0,
makeNode(AssignNode, varDef, {
operator: "=",
logical: false,
left: symbol,
right: makeVoid0(name)
})
);
}
}
}
};
/**
* The expressions the call becomes, its `var` added to the scope.
* @param {InlineState} state the decision's state
* @param {Node} returnedValue what the call evaluates to
* @returns {Node[]} the expressions, each a deep clone
*/
const flattenFunction = (state, returnedValue) => {
/** @type {Node[]} */
const declarations = [];
/** @type {Node[]} */
const expressions = [];
flattenArgs(state, declarations, expressions);
flattenVars(state, declarations, expressions);
expressions.push(returnedValue);
if (declarations.length) {
/** @type {Node[]} */
const statements = /** @type {Node[]} */ (ast.bodyOf(state.scope));
const index =
statements.indexOf(state.compressor.parent(state.level - 1)) + 1;
statements.splice(
index,
0,
makeNode(VarNode, state.lambda, {
declarations
})
);
}
const copies = expressions.map((item) => ast.cloneNode(item, true));
const { constants } = state;
if (constants === undefined) return copies;
const transformer = ast.createTransformer((/** @type {Node} */ node) => {
if (!isSymbolRefNode(node)) return;
for (let i = 0; i < constants.length; i++) {
if (constants[i].definition === node.definition) {
const index = state.lambda.params.indexOf(constants[i]);
return ast.cloneNode(state.self.arguments[index], true);
}
}
});
return copies.map((item) => ast.transformNode(item, transformer));
};
/**
* Parameters have a scope of their own, which a `var` of the body does
* not reach, so nothing inlines into a default value.
* @param {Compressor} compressor the compressor
* @returns {boolean} whether the call sits in a default value of its block
*/
const inDefaultAssign = (compressor) => {
let i = 0;
let parent;
while ((parent = compressor.parent(i++))) {
if (isDefaultAssignNode(parent)) return true;
if (isBlockNode(parent)) break;
}
return false;
};
/**
* terser's `inline_into_call`.
* @param {Node} self a call
* @param {Compressor} compressor the compressor
* @returns {Node} what replaces it
*/
const inlineIntoCall = (self, compressor) => {
if (compressor.in_computed_key()) return self;
const expression = self.callee;
let lambda = expression;
const simpleArgs = hasNoSpread(self.arguments);
if (
compressor.option("reduce_vars") &&
isSymbolRefNode(lambda) &&
!hasAnnotation(self, _NOINLINE)
) {
const fixed = ast.fixedValue(lambda);
if (
retainTopFunction(/** @type {Node} */ (fixed), compressor) ||
(!compressor.toplevel.funcs && expression.definition.global)
) {
return self;
}
lambda = /** @type {Node} */ (fixed);
}
// A call only flattening its function leaves no function in the loop.
const flattensOnly =
mustNotInlineLambdaInLoop(compressor, lambda) &&
!hasAnnotation(self, _INLINE);
if (
flattensOnly &&
!(
inlineHooks.flattensInLoop !== null &&
isSymbolRefNode(expression) &&
inlineHooks.flattensInLoop(lambda, compressor)
)
) {
return self;
}
const isFunction = isLambdaNode(lambda);
const statement =
isFunction &&
/** @type {Node[]} */ (/** @type {Node} */ (lambda.body).body)[0];
const isRegularFunction = isFunction && !lambda.generator && !lambda.async;
const canInline =
isRegularFunction &&
compressor.option("inline") &&
!ast.isCalleePure(self, compressor);
if (canInline && isReturnNode(statement)) {
let returned = /** @type {Node} */ (statement).argument;
if (!returned || ast.isConstantExpression(returned)) {
returned = returned ? ast.cloneNode(returned, true) : makeVoid0(self);
const args = [...self.arguments, returned];
return ast.optimize(makeSequence(self, args), compressor);
}
// optimize identity function
if (
lambda.params.length === 1 &&
isSymbolFunargNode(lambda.params[0]) &&
self.arguments.length < 2 &&
!isExpansionNode(self.arguments[0]) &&
isSymbolRefNode(returned) &&
returned.name === lambda.params[0].name
) {
// terser gives this `void 0` no origin, so it carries no position.
const replacement = ast.optimize(
self.arguments[0] || makeVoid0(),
compressor
);
let parent;
if (
isPropAccessNode(replacement) &&
isCallNode((parent = compressor.parent())) &&
parent.callee === self
) {
// `id(bag.method)(...)` called the method without `this`, which
// the larger `(0, bag.method)(...)` keeps.
return makeSequence(self, [
makeNode(NumberNode, self, { value: 0 }),
replacement
]);
}
return replacement;
}
}
if (canInline) {
const state = inlineState;
state.self = self;
state.compressor = compressor;
state.lambda = lambda;
let expressionDefinition;
let returnedValue;
let nearestScope;
if (
simpleArgs &&
!ast.usesArguments(lambda) &&
!isClassNode(compressor.parent()) &&
!(lambda.id && isFunctionNode(lambda)) &&
(returnedValue = canFlattenBody(state, statement)) &&
(expression === lambda ||
hasAnnotation(self, _INLINE) ||
(compressor.option("unused") &&
(expressionDefinition = expression.definition).references.length ===
1 &&
!isRecursiveRef(compressor, expressionDefinition) &&
ast.isConstantExpression(lambda, expression.scope))) &&
!hasAnnotation(self, _PURE | _NOINLINE) &&
!ast.containsThis(lambda) &&
canInjectSymbols(state) &&
(nearestScope = compressor.find_scope()) &&
!scopeEnclosesVariablesInThisScope(nearestScope, lambda) &&
!inDefaultAssign(compressor) &&
!isClassNode(state.scope)
) {
setFlag(lambda, SQUEEZED);
ast.addChildScope(nearestScope, lambda);
const expressions = flattenFunction(state, returnedValue);
releaseInlineState(state);
return ast.optimize(makeSequence(self, expressions), compressor);
}
releaseInlineState(state);
}
if (flattensOnly) return self;
if (canInline && hasAnnotation(self, _INLINE)) {
setFlag(lambda, SQUEEZED);
lambda = makeNode(
isDefunNode(lambda) ? FunctionNode : ast.factoryOf(lambda),
lambda,
lambda
);
lambda = ast.cloneNode(lambda, true);
ast.figureOutScope(
lambda,
{},
{
parent_scope: compressor.find_scope(),
toplevel: compressor.get_toplevel()
}
);
return ast.optimize(
makeNode(CallNode, self, {
callee: lambda,
arguments: self.arguments
}),
compressor
);
}
const canDropThisCall =
isRegularFunction &&
compressor.option("side_effects") &&
/** @type {Node[]} */ (/** @type {Node} */ (lambda.body).body).every(
isEmpty
);
if (canDropThisCall) {
const args = [...self.arguments, makeVoid0(self)];
return ast.optimize(makeSequence(self, args), compressor);
}
if (
compressor.option("negate_iife") &&
isSimpleStatementNode(compressor.parent()) &&
isIifeCall(self)
) {
return ast.negate(self, compressor, true);
}
let evaluated = ast.evaluate(self, compressor);
if (evaluated !== self) {
evaluated = ast.optimize(
makeNodeFromConstant(/** @type {EvaluatedValue} */ (evaluated), self),
compressor
);
return bestOf(compressor, evaluated, self);
}
return self;
};
return { inlineIntoSymbolRef, inlineIntoCall };
};
/**
* Installs terser's statement optimizers and the `Compressor` and scope methods
* of `compress/index.js` they call, from the compressor's driver to `ImportNode`.
* @param {TerserModules} modules terser's modules
* @param {OptimizerHelpers} helpers what the optimizers share
* @returns {void}
*/
const installStatementOptimizers = (modules, helpers) => {
const { ast } = modules;
const { transformNode, walkNode } = modules.ast;
const A = modules.ast;
const { methods: compressorMethods } = modules.compress;
const { createTransformer, createWalker, walk } = A;
const { base54 } = modules.scope;
const { make_void_0: makeVoid0, remove } = modules.utils;
const {
make_sequence: makeSequence,
best_of_expression: bestOfExpression,
make_node_from_constant: makeNodeFromConstant,
has_break_or_continue: hasBreakOrContinue,
is_empty: isEmpty,
can_be_evicted_from_block: canBeEvictedFromBlock,
as_statement_array: asStatementArray
} = modules.common;
const {
bitwise_binop: bitwiseBinaryOperators,
is_undefined: isUndefined,
is_lhs: isLhs,
aborts
} = modules.inference;
const {
defineOptimizer,
optimizeLambda,
tightenBody,
extractFromUnreachableCode
} = helpers;
/**
* @this {Compressor} the compressor
* @returns {ScopeMangleOptions} the options scopes are figured out with
*/
compressorMethods.mangle_options = function mangle_options() {
const nthIdentifier =
(this._mangle_options && this._mangle_options.nth_identifier) || base54;
const moduleOption =
(this._mangle_options && this._mangle_options.module) ||
this.option("module");
return {
ie8: this.option("ie8"),
nth_identifier: nthIdentifier,
module: moduleOption
};
};
/**
* @template {keyof CompressOptions} K
* @this {Compressor} the compressor
* @param {K} key an option's name
* @returns {CompressOptions[K]} its value
*/
compressorMethods.option = function option(key) {
return this.options[key];
};
/**
* @this {Compressor} the compressor
* @param {Definition} definition a variable
* @returns {boolean} whether code outside the output can read it
*/
compressorMethods.exposed = function exposed(definition) {
if (definition.export) return true;
if (definition.global) {
for (let i = 0, length = definition.identifiers.length; i < length; i++) {
if (
!this.toplevel[
A.isSymbolDefunNode(definition.identifiers[i]) ? "funcs" : "vars"
]
) {
return true;
}
}
}
return false;
};
/**
* @this {Compressor} the compressor
* @returns {boolean | undefined} whether only the truthiness of the node visited is read
*/
compressorMethods.in_boolean_context = function in_boolean_context() {
if (!this.option("booleans")) return false;
let self = this.self();
for (let i = 0, parent; (parent = this.parent(i)); i++) {
if (
A.isSimpleStatementNode(parent) ||
(A.isConditionalNode(parent) && parent.test === self) ||
(A.isDWLoopNode(parent) && parent.test === self) ||
(A.isForNode(parent) && parent.test === self) ||
(A.isIfNode(parent) && parent.test === self) ||
(A.isUnaryPrefixNode(parent) &&
parent.operator === "!" &&
parent.argument === self)
) {
return true;
}
if (
(A.isBinaryNode(parent) &&
(parent.operator === "&&" || parent.operator === "||")) ||
A.isConditionalNode(parent) ||
ast.tailNode(parent) === self
) {
self = parent;
} else {
return false;
}
}
return undefined;
};
/**
* True where the node visited is turned into a 32-bit integer, as in `~x`
* or `(1, x) | 0`.
* @this {Compressor} the compressor
* @param {boolean=} otherOperandMustBeNumber whether the other operand has to be a number too
* @returns {boolean | undefined} whether it is
*/
compressorMethods.in_32_bit_context = function in_32_bit_context(
otherOperandMustBeNumber
) {
if (!this.option("evaluate")) return false;
let self = this.self();
for (let i = 0, parent; (parent = this.parent(i)); i++) {
if (
A.isBinaryNode(parent) &&
bitwiseBinaryOperators.has(parent.operator)
) {
if (otherOperandMustBeNumber) {
return ast.isNumber(
self === parent.left ? parent.right : parent.left,
this
);
}
return true;
}
if (A.isUnaryPrefixNode(parent)) {
return parent.operator === "~";
}
if (
(A.isBinaryNode(parent) &&
// Not the left operand: that can change the branch taken.
((parent.operator === "&&" && parent.right === self) ||
(parent.operator === "||" && parent.right === self) ||
(parent.operator === "??" && parent.right === self))) ||
(A.isConditionalNode(parent) && parent.test !== self) ||
ast.tailNode(parent) === self
) {
self = parent;
} else {
return false;
}
}
return undefined;
};
/**
* @this {Compressor} the compressor
* @returns {Node | undefined} the tree being compressed
*/
compressorMethods.get_toplevel = function get_toplevel() {
return this._toplevel;
};
/**
* @this {Compressor} the compressor
* @param {Node} toplevel the tree
* @returns {Node} the tree compressed
*/
compressorMethods.compress = function compress(toplevel) {
toplevel = ast.resolveDefines(toplevel, this);
/** @type {Compressor} */ (this)._toplevel = toplevel;
/** @type {Compressor} */ (this).tokenTables =
toplevel.tokenTables || NO_TOKEN_TABLES;
if (this.option("expression")) {
ast.processExpression(this._toplevel, true);
}
const passes = Number(this.options.passes) || 1;
let minimumCount = Infinity;
let stopping = false;
const mangle = this.mangle_options();
/** @type {CollapseHooks} */
const collapseHooks = modules.webpackCollapse;
// The improve phase runs the pass after which terser would stop as the last.
let lastPass = false;
for (let pass = 0; pass < passes; pass++) {
collapseHooks.finalPass = lastPass || pass + 1 === passes;
ast.figureOutScope(this._toplevel, mangle);
if (pass === 0 && collapseHooks.probeProgram) {
collapseHooks.probeProgram(this._toplevel);
}
if (pass === 0 && this.option("drop_console")) {
// Before `reduce_vars` and the pass itself.
/** @type {Compressor} */ (this)._toplevel = ast.dropConsole(
this._toplevel,
this.option("drop_console")
);
}
if (pass > 0 || this.option("reduce_vars")) {
ast.resetOptimizerFlags(this._toplevel, this);
}
/** @type {Compressor} */ (this)._toplevel = transformNode(
this._toplevel,
this
);
// Only a pass that another can follow needs the count: terser walks the
// whole tree after the last one too, for a verdict it then drops.
if (lastPass) break;
if (pass + 1 < passes) {
let count = 0;
walk(this._toplevel, (/** @type {Node} */ node) => {
count++;
// terser counted each hole, a node of its own there.
const list = A.isArrayNode(node)
? node.elements
: A.isDestructuringNode(node)
? node.elements || undefined
: undefined;
if (list !== undefined) {
for (const item of list) if (item === null) count++;
}
});
if (count < minimumCount) {
minimumCount = count;
stopping = false;
} else if (!stopping) {
stopping = true;
} else if (
modules.improvements !== undefined &&
modules.improvements.enabled
) {
lastPass = true;
} else {
break;
}
}
}
collapseHooks.finalPass = true;
if (this.option("expression")) {
ast.processExpression(this._toplevel, false);
}
toplevel = this._toplevel;
/** @type {Compressor} */ (this)._toplevel = undefined;
return toplevel;
};
/**
* `is_lhs` of the node visited, which works inside `optimize`.
* @this {Compressor} the compressor
* @returns {Node | undefined} what the node visited's parent writes to, where it writes the node
*/
compressorMethods.is_lhs = function is_lhs() {
const self = this.stack[this.stack.length - 1];
const parent = this.stack[this.stack.length - 2];
return isLhs(self, parent);
};
defineOptimizer("Node", (self) => self);
/**
* Turns each statement that ends the scope into a `return` of it, or back.
* @param {Node} self the node of the scope
* @param {boolean} insert whether to insert the returns rather than remove them
* @param {Compressor=} compressor the compressor, which drops what a removed `return` returns
* @returns {void}
*/
ast.processExpression = function processExpression(self, insert, compressor) {
const transformer = createTransformer((/** @type {Node} */ node) => {
if (insert && A.isSimpleStatementNode(node)) {
return makeNode(A.ReturnNode, node, {
argument: node.expression
});
}
if (!insert && A.isReturnNode(node)) {
if (compressor) {
const value =
node.argument &&
ast.dropSideEffectFree(node.argument, compressor, true);
return value
? makeNode(A.SimpleStatementNode, node, { expression: value })
: makeNode(A.EmptyStatementNode, node);
}
return makeNode(A.SimpleStatementNode, node, {
expression: node.argument || makeVoid0(node)
});
}
if (A.isClassNode(node) || (A.isLambdaNode(node) && node !== self)) {
return node;
}
if (A.isBlockNode(node)) {
const statements = A.bodyOf(node);
const index = /** @type {Node[]} */ (statements).length - 1;
if (index >= 0) {
/** @type {Node[]} */ (statements)[index] = transformNode(
/** @type {Node[]} */ (statements)[index],
transformer
);
}
} else if (A.isIfNode(node)) {
node.consequent = transformNode(
/** @type {Node} */ (node.consequent),
transformer
);
if (node.alternate) {
node.alternate = transformNode(node.alternate, transformer);
}
} else if (A.isWithNode(node)) {
node.body = transformNode(/** @type {Node} */ (node.body), transformer);
}
return node;
});
transformNode(self, transformer);
};
/**
* @param {Node} self a symbol
* @returns {Node | false | null | undefined} the value its variable is known to hold, if any
*/
ast.fixedValue = function fixedValue(self) {
const fixed = self.definition.fixed;
if (!fixed || A.isSyntaxNode(fixed)) return fixed;
return fixed();
};
/**
* @param {Node} self a reference
* @returns {boolean} whether it reads a function expression's own name
*/
ast.isImmutable = function isImmutable(self) {
const orig = self.definition.identifiers;
return orig.length === 1 && A.isSymbolLambdaNode(orig[0]);
};
/**
* @param {Node} self a reference
* @param {Compressor} compressor the compressor
* @returns {boolean} whether reading it cannot throw for want of a declaration
*/
ast.isDeclared = function isDeclared(self, compressor) {
return (
!self.definition.undeclared ||
((compressor.option("unsafe") || compressor.option("builtins_pure")) &&
compressor.pure_access_globals(self.name))
);
};
const directives = new Set(["use asm", "use strict"]);
defineOptimizer("Directive", (self, compressor) => {
if (
compressor.option("directives") &&
(!directives.has(self.directive) ||
compressor.has_directive(self.directive) !== self)
) {
return makeNode(A.EmptyStatementNode, self);
}
return self;
});
defineOptimizer("Debugger", (self, compressor) => {
if (compressor.option("drop_debugger")) {
return makeNode(A.EmptyStatementNode, self);
}
return self;
});
defineOptimizer("LabeledStatement", (self, compressor) => {
if (
A.isBreakNode(self.body) &&
compressor.loopcontrol_target(/** @type {Node} */ (self.body)) ===
self.body
) {
return makeNode(A.EmptyStatementNode, self);
}
return self.label.references.length === 0
? /** @type {Node} */ (self.body)
: self;
});
defineOptimizer("Block", (self, compressor) => {
// A case's or default's statements are its `consequent`.
tightenBody(/** @type {Node[]} */ (A.bodyOf(self)), compressor);
return self;
});
/**
* @param {Node} node a statement
* @returns {boolean} whether it can leave the block of an `if` without changing scope
*/
const canBeExtractedFromIfBlock = (node) =>
!(
A.isConstNode(node) ||
A.isLetNode(node) ||
A.isUsingNode(node) ||
A.isClassNode(node)
);
defineOptimizer("BlockStatement", (self, compressor) => {
tightenBody(/** @type {Node[]} */ (self.body), compressor);
switch (/** @type {Node[]} */ (self.body).length) {
case 1:
if (
(!compressor.has_directive("use strict") &&
A.isIfNode(compressor.parent()) &&
canBeExtractedFromIfBlock(/** @type {Node[]} */ (self.body)[0]) &&
// Annex B lets only a plain function stand as an `if` body;
// terser unwraps an async function or a generator too.
// prettier-ignore
!(
modules.corrections !== undefined &&
modules.corrections.enabled &&
A.isDefunNode(/** @type {Node[]} */ (self.body)[0]) &&
(/** @type {Node[]} */ (self.body)[0].async ||
/** @type {Node[]} */ (self.body)[0].generator)
)) ||
canBeEvictedFromBlock(/** @type {Node[]} */ (self.body)[0])
) {
return /** @type {Node[]} */ (self.body)[0];
}
break;
case 0:
return makeNode(A.EmptyStatementNode, self);
}
return self;
});
defineOptimizer("Lambda", optimizeLambda);
/**
* Hoists functions and `var`s to the top of the scope, as `hoist_funs` and
* `hoist_vars` ask, with its directives first.
* @param {Node} self the node of the scope
* @param {Compressor} compressor the compressor
* @returns {Node} the node, hoisted
*/
ast.hoistDeclarations = function hoistDeclarations(self, compressor) {
if (compressor.has_directive("use asm")) return self;
const hoistFunctions = compressor.option("hoist_funs");
let hoistVars = compressor.option("hoist_vars");
if (hoistFunctions || hoistVars) {
/** @type {Node[]} */
const directiveStatements = [];
/** @type {Node[]} */
const hoisted = [];
/** @type {Map<string, Node>} */
const vars = new Map();
let varsFound = 0;
let varDeclarations = 0;
// Hoisting a single `var` costs more than it saves.
walk(self, (/** @type {Node} */ node) => {
if (A.isScopeNode(node) && node !== self) return true;
if (A.isVarNode(node)) {
++varDeclarations;
return true;
}
return undefined;
});
hoistVars = hoistVars && varDeclarations > 1;
const transformer = createTransformer((/** @type {Node} */ node) => {
if (node !== self) {
if (A.isDirectiveNode(node)) {
directiveStatements.push(node);
return makeNode(A.EmptyStatementNode, node);
}
if (
hoistFunctions &&
A.isDefunNode(node) &&
!A.isExportNode(transformer.parent()) &&
transformer.parent() === self
) {
hoisted.push(node);
return makeNode(A.EmptyStatementNode, node);
}
if (
hoistVars &&
A.isVarNode(node) &&
// terser hoists an exported `var` too, leaving an `export`
// of assignments, which does not parse.
!(
modules.corrections !== undefined &&
modules.corrections.enabled &&
A.isExportNode(transformer.parent())
) &&
!node.declarations.some((/** @type {Node} */ definition) =>
A.isDestructuringNode(definition.id)
)
) {
for (const definition of node.declarations) {
vars.set(definition.id.name, definition);
++varsFound;
}
const sequence = ast.toAssignments(node, compressor);
const parent = transformer.parent();
if (A.isForInNode(parent) && parent.left === node) {
if (sequence === null || sequence === undefined) {
const name = node.declarations[0].id;
return makeNode(A.SymbolRefNode, name, name);
}
return sequence;
}
if (A.isForNode(parent) && parent.init === node) {
return sequence;
}
if (!sequence) return makeNode(A.EmptyStatementNode, node);
return makeNode(A.SimpleStatementNode, node, {
expression: sequence
});
}
// Nested scopes are left as they are.
if (A.isScopeNode(node)) return node;
}
return undefined;
});
self = transformNode(self, transformer);
if (varsFound > 0) {
// Only the vars that are not also the function's parameters.
/** @type {Node[]} */
const definitions = [];
const isLambda = A.isLambdaNode(self);
const argumentNames = isLambda ? ast.argsAsNames(self) : null;
for (let [name, definition] of vars.entries()) {
if (
isLambda &&
/** @type {Node[]} */ (argumentNames).some(
(/** @type {Node} */ argument) =>
argument.name === definition.id.name
)
) {
vars.delete(name);
} else {
definition = ast.cloneNode(definition);
definition.init = /** @type {Node} */ (
/** @type {Node | null} */ (null)
);
definitions.push(definition);
vars.set(name, definition);
}
}
if (definitions.length > 0) {
/** @type {Node[]} */
const statements = /** @type {Node[]} */ (A.bodyOf(self));
// Merges the assignments that follow into the declarations.
for (let i = 0; i < statements.length;) {
if (A.isSimpleStatementNode(statements[i])) {
const expression = statements[i].expression;
let symbol;
let assign;
if (
A.isAssignNode(expression) &&
expression.operator === "=" &&
A.isSymbolNode((symbol = expression.left)) &&
vars.has(symbol.name)
) {
const definition = /** @type {Node} */ (vars.get(symbol.name));
if (definition.init) break;
definition.init = expression.right;
remove(definitions, definition);
definitions.push(definition);
statements.splice(i, 1);
continue;
}
if (
A.isSequenceNode(expression) &&
A.isAssignNode((assign = expression.expressions[0])) &&
assign.operator === "=" &&
A.isSymbolNode((symbol = assign.left)) &&
vars.has(symbol.name)
) {
const definition = /** @type {Node} */ (vars.get(symbol.name));
if (definition.init) break;
definition.init = assign.right;
remove(definitions, definition);
definitions.push(definition);
statements[i].expression = makeSequence(
expression,
expression.expressions.slice(1)
);
continue;
}
}
if (A.isEmptyStatementNode(statements[i])) {
statements.splice(i, 1);
continue;
}
if (A.isBlockStatementNode(statements[i])) {
statements.splice(
i,
1,
.../** @type {Node[]} */ (statements[i].body)
);
continue;
}
break;
}
hoisted.push(
makeNode(A.VarNode, self, {
declarations: definitions
})
);
}
}
// A class has no list, which `concat` appends as `undefined` and terser
// kept as the class's `body`; here the class keeps its members.
// eslint-disable-next-line unicorn/prefer-spread
const body = directiveStatements.concat(
hoisted,
/** @type {Node[]} */ (A.bodyOf(self))
);
if (A.isLambdaNode(self)) {
/** @type {Node} */ (self.body).body = body;
} else if (!A.isClassNode(self)) {
self.body = body;
}
}
return self;
};
defineOptimizer("SimpleStatement", (self, compressor) => {
if (compressor.option("side_effects")) {
const body = self.expression;
const node = ast.dropSideEffectFree(body, compressor, true);
if (!node) {
return makeNode(A.EmptyStatementNode, self);
}
if (node !== body) {
return makeNode(A.SimpleStatementNode, self, { expression: node });
}
}
return self;
});
defineOptimizer("While", (self, compressor) =>
compressor.option("loops")
? ast.optimize(makeNode(A.ForNode, self, self), compressor)
: self
);
defineOptimizer("Do", (self, compressor) => {
if (!compressor.option("loops")) return self;
const condition = ast.evaluate(ast.tailNode(self.test), compressor);
if (!A.isSyntaxNode(condition)) {
if (condition) {
return ast.optimize(
makeNode(A.ForNode, self, {
body: makeNode(
A.BlockStatementNode,
/** @type {Node | undefined} */ (self.body),
{
body: [
/** @type {Node} */ (self.body),
makeNode(A.SimpleStatementNode, self.test, {
expression: self.test
})
]
}
)
}),
compressor
);
}
if (!hasBreakOrContinue(self, compressor.parent())) {
return ast.optimize(
makeNode(
A.BlockStatementNode,
/** @type {Node | undefined} */ (self.body),
{
body: [
/** @type {Node} */ (self.body),
makeNode(A.SimpleStatementNode, self.test, {
expression: self.test
})
]
}
),
compressor
);
}
}
return self;
});
/**
* Whether a test reads a name a loop's block body declares, which outside
* that block is another binding: Annex B's `var` for a function.
* @param {Node} body the loop's body
* @param {Node} test the test of its leading `if`
* @returns {boolean} true when it does, and the correct phase is on
*/
const readsBodyDeclaration = (body, test) => {
if (
modules.corrections === undefined ||
!modules.corrections.enabled ||
!A.isBlockStatementNode(body)
) {
return false;
}
/** @type {Set<string>} */
const names = new Set();
for (const statement of /** @type {Node[]} */ (body.body)) {
if (A.isDefunNode(statement) || A.isDefClassNode(statement)) {
if (statement.id) names.add(statement.id.name);
} else if (A.isLetNode(statement) || A.isConstNode(statement)) {
for (const definition of statement.declarations) {
A.walk(definition.id, (/** @type {Node} */ node) => {
if (A.isSymbolDeclarationNode(node)) names.add(node.name);
});
}
}
}
if (names.size === 0) return false;
let reads = false;
A.walk(test, (/** @type {Node} */ node) => {
if (A.isSymbolRefNode(node) && names.has(node.name)) reads = true;
return reads;
});
return reads;
};
/**
* terser's `if_break_in_loop`: folds a leading `break`, or an `if` whose
* branch is one, into the loop's condition.
* @param {Node} self a `for` loop
* @param {Compressor} compressor the compressor
* @returns {Node} the loop, or the block that replaces it
*/
const ifBreakInLoop = (self, compressor) => {
/**
* @param {Node} node a statement
* @returns {boolean} whether it breaks out of the loop visited
*/
const isBreak = (node) =>
A.isBreakNode(node) &&
compressor.loopcontrol_target(node) === compressor.self();
/**
* Replaces the leading `if` of the body with the branch that does not
* break, then folds what it starts with in turn.
* @param {Node | null} rest the branch that stays
* @returns {void}
*/
const dropIt = (rest) => {
const statements = asStatementArray(rest);
if (A.isBlockStatementNode(self.body)) {
self.body = ast.cloneNode(/** @type {Node} */ (self.body));
self.body.body = [
...statements,
.../** @type {Node[]} */ (self.body.body).slice(1)
];
self.body = transformNode(self.body, compressor);
} else {
self.body = transformNode(
makeNode(
A.BlockStatementNode,
/** @type {Node | undefined} */ (self.body),
{
body: statements
}
),
compressor
);
}
self = ifBreakInLoop(self, compressor);
};
const first = A.isBlockStatementNode(self.body)
? /** @type {Node[]} */ (/** @type {Node} */ (self.body).body)[0]
: self.body;
if (
compressor.option("dead_code") &&
isBreak(/** @type {Node} */ (first))
) {
/** @type {Node[]} */
const body = [];
if (A.isStatementNode(self.init)) {
body.push(self.init);
} else if (self.init) {
body.push(
makeNode(A.SimpleStatementNode, self.init, {
expression: self.init
})
);
}
if (self.test) {
body.push(
makeNode(A.SimpleStatementNode, self.test, {
expression: self.test
})
);
}
extractFromUnreachableCode(
compressor,
/** @type {Node} */ (self.body),
body
);
return makeNode(A.BlockStatementNode, self, {
body
});
}
if (
A.isIfNode(first) &&
!readsBodyDeclaration(
/** @type {Node} */ (self.body),
/** @type {Node} */ (first).test
)
) {
if (
isBreak(/** @type {Node} */ (/** @type {Node} */ (first).consequent))
) {
if (self.test) {
self.test = makeNode(A.BinaryNode, self.test, {
left: self.test,
operator: "&&",
right: ast.negate(/** @type {Node} */ (first).test, compressor)
});
} else {
self.test = ast.negate(/** @type {Node} */ (first).test, compressor);
}
dropIt(/** @type {Node} */ (first).alternate);
} else if (isBreak(/** @type {Node} */ (first).alternate)) {
if (self.test) {
self.test = makeNode(A.BinaryNode, self.test, {
left: self.test,
operator: "&&",
right: /** @type {Node} */ (first).test
});
} else {
self.test = /** @type {Node} */ (first).test;
}
dropIt(/** @type {Node} */ (/** @type {Node} */ (first).consequent));
}
}
return self;
};
defineOptimizer("For", (self, compressor) => {
if (!compressor.option("loops")) return self;
if (compressor.option("side_effects") && self.init) {
self.init = /** @type {Node} */ (
ast.dropSideEffectFree(self.init, compressor)
);
}
if (self.test) {
let condition = ast.evaluate(self.test, compressor);
if (!A.isSyntaxNode(condition)) {
if (condition) {
self.test = NO_NODE;
} else if (!compressor.option("dead_code")) {
const original = self.test;
self.test = makeNodeFromConstant(condition, self.test);
self.test = bestOfExpression(
transformNode(self.test, compressor),
original
);
}
}
if (compressor.option("dead_code")) {
if (A.isSyntaxNode(condition)) {
condition = ast.evaluate(ast.tailNode(self.test), compressor);
}
if (!condition) {
/** @type {Node[]} */
const body = [];
extractFromUnreachableCode(
compressor,
/** @type {Node} */ (self.body),
body
);
if (A.isStatementNode(self.init)) {
body.push(self.init);
} else if (self.init) {
body.push(
makeNode(A.SimpleStatementNode, self.init, {
expression: self.init
})
);
}
body.push(
makeNode(A.SimpleStatementNode, self.test, {
expression: self.test
})
);
return ast.optimize(
makeNode(A.BlockStatementNode, self, { body }),
compressor
);
}
}
}
return ifBreakInLoop(self, compressor);
});
/**
* Annex B reads a function standing alone as an `if`'s branch as in a
* block of its own, which a statement list it is moved into is not.
* @param {Node} branch the branch an `if` of a known test keeps
* @returns {Node} the branch, a function wrapped in its own block
*/
const keptBranch = (branch) =>
A.isDefunNode(branch) &&
modules.corrections !== undefined &&
modules.corrections.enabled
? makeNode(A.BlockStatementNode, branch, { body: [branch] })
: branch;
defineOptimizer("If", (self, compressor) => {
if (isEmpty(self.alternate)) self.alternate = NO_NODE;
if (!compressor.option("conditionals")) return self;
// A condition known statically has no side effects, so one branch
// goes; `x && true` is not known, though it could be.
let condition = ast.evaluate(self.test, compressor);
if (!compressor.option("dead_code") && !A.isSyntaxNode(condition)) {
const original = self.test;
self.test = makeNodeFromConstant(condition, original);
self.test = bestOfExpression(
transformNode(self.test, compressor),
original
);
}
if (compressor.option("dead_code")) {
if (A.isSyntaxNode(condition)) {
condition = ast.evaluate(ast.tailNode(self.test), compressor);
}
if (!condition) {
/** @type {Node[]} */
const body = [];
extractFromUnreachableCode(
compressor,
/** @type {Node} */ (self.consequent),
body
);
body.push(
makeNode(A.SimpleStatementNode, self.test, {
expression: self.test
})
);
if (self.alternate) body.push(keptBranch(self.alternate));
return ast.optimize(
makeNode(A.BlockStatementNode, self, { body }),
compressor
);
} else if (!A.isSyntaxNode(condition)) {
/** @type {Node[]} */
const body = [];
body.push(
makeNode(A.SimpleStatementNode, self.test, {
expression: self.test
})
);
body.push(keptBranch(/** @type {Node} */ (self.consequent)));
if (self.alternate) {
extractFromUnreachableCode(compressor, self.alternate, body);
}
return ast.optimize(
makeNode(A.BlockStatementNode, self, { body }),
compressor
);
}
}
const negated = ast.negate(self.test, compressor);
const conditionLength = ast.nodeSize(self.test);
const negatedLength = ast.nodeSize(negated);
let negatedIsBest = negatedLength < conditionLength;
if (self.alternate && negatedIsBest) {
// The branches are swapped here already; the lengths are only
// compared for equality below, so they stay as they are.
negatedIsBest = false;
self.test = negated;
const body = self.consequent;
self.consequent = self.alternate || makeNode(A.EmptyStatementNode, self);
self.alternate = /** @type {Node} */ (body);
}
if (
isEmpty(/** @type {Node} */ (self.consequent)) &&
isEmpty(self.alternate)
) {
return ast.optimize(
makeNode(A.SimpleStatementNode, self.test, {
expression: ast.cloneNode(self.test)
}),
compressor
);
}
if (
A.isSimpleStatementNode(self.consequent) &&
A.isSimpleStatementNode(self.alternate)
) {
return ast.optimize(
makeNode(A.SimpleStatementNode, self, {
expression: makeNode(A.ConditionalNode, self, {
test: self.test,
consequent: /** @type {Node} */ (self.consequent).expression,
alternate: self.alternate.expression
})
}),
compressor
);
}
if (isEmpty(self.alternate) && A.isSimpleStatementNode(self.consequent)) {
if (
conditionLength === negatedLength &&
!negatedIsBest &&
A.isBinaryNode(self.test) &&
self.test.operator === "||"
) {
// As long, but the negation needs no parentheses around it
// (https://github.com/mishoo/UglifyJS2/issues/979).
negatedIsBest = true;
}
if (negatedIsBest) {
return ast.optimize(
makeNode(A.SimpleStatementNode, self, {
expression: makeNode(A.BinaryNode, self, {
operator: "||",
left: negated,
right: /** @type {Node} */ (self.consequent).expression
})
}),
compressor
);
}
return ast.optimize(
makeNode(A.SimpleStatementNode, self, {
expression: makeNode(A.BinaryNode, self, {
operator: "&&",
left: self.test,
right: /** @type {Node} */ (self.consequent).expression
})
}),
compressor
);
}
if (
A.isEmptyStatementNode(self.consequent) &&
A.isSimpleStatementNode(self.alternate)
) {
return ast.optimize(
makeNode(A.SimpleStatementNode, self, {
expression: makeNode(A.BinaryNode, self, {
operator: "||",
left: self.test,
right: self.alternate.expression
})
}),
compressor
);
}
if (
A.isExitNode(self.consequent) &&
A.isExitNode(self.alternate) &&
ast.kindOf(/** @type {Node} */ (self.consequent)) ===
ast.kindOf(self.alternate)
) {
return ast.optimize(
makeNode(ast.factoryOf(/** @type {Node} */ (self.consequent)), self, {
argument: transformNode(
makeNode(A.ConditionalNode, self, {
test: self.test,
consequent:
/** @type {Node} */ (self.consequent).argument ||
makeVoid0(/** @type {Node} */ (self.consequent)),
alternate: self.alternate.argument || makeVoid0(self.alternate)
}),
compressor
)
}),
compressor
);
}
if (
A.isIfNode(self.consequent) &&
!(/** @type {Node} */ (self.consequent).alternate) &&
!self.alternate
) {
self = makeNode(A.IfNode, self, {
test: makeNode(A.BinaryNode, self.test, {
operator: "&&",
left: self.test,
right: /** @type {Node} */ (self.consequent).test
}),
consequent: /** @type {Node} */ (self.consequent).consequent,
alternate: null
});
}
if (aborts(/** @type {Node} */ (self.consequent)) && self.alternate) {
const alternative = self.alternate;
self.alternate = NO_NODE;
return ast.optimize(
makeNode(A.BlockStatementNode, self, {
body: [self, alternative]
}),
compressor
);
}
if (aborts(self.alternate)) {
const body = /** @type {Node} */ (self.consequent);
self.consequent = self.alternate;
self.test = negatedIsBest ? negated : ast.negate(self.test, compressor);
self.alternate = NO_NODE;
return ast.optimize(
makeNode(A.BlockStatementNode, self, {
body: [self, body]
}),
compressor
);
}
return self;
});
/**
* Whether one case of a `switch` refers to a `let`, `const` or class that
* another declares, which binds across the whole `switch`.
* @param {Node} self a `switch`
* @returns {boolean} true when one does
*/
const readsAnotherCaseBinding = (self) => {
/** @type {Map<Definition, Node>} */
const declaredIn = new Map();
for (const branch of self.cases) {
for (const statement of /** @type {Node[]} */ (branch.consequent)) {
if (canBeExtractedFromIfBlock(statement)) continue;
const names = A.isClassNode(statement)
? [statement.id]
: statement.declarations.map(
(/** @type {Node} */ declaration) => declaration.id
);
for (const name of names) {
if (!name) continue;
walkNode(
name,
createWalker((/** @type {Node} */ node) => {
if (A.isSymbolDeclarationNode(node) && node.definition) {
declaredIn.set(node.definition, branch);
}
return undefined;
})
);
}
}
}
if (declaredIn.size === 0) return false;
for (const branch of self.cases) {
let reads = false;
/**
* @param {Node} part the label or a statement of the case
* @param {Node | undefined} own the case whose own bindings it may read
* @returns {void}
*/
const walkPart = (part, own) => {
walkNode(
part,
createWalker((/** @type {Node} */ node) => {
if (reads) return true;
if (A.isSymbolRefNode(node) && node.definition) {
const declaring = declaredIn.get(node.definition);
if (declaring !== undefined && declaring !== own) reads = true;
}
return undefined;
})
);
};
// `case x: let x` reads `x` in its dead zone as its own label runs.
if (branch.test) walkPart(branch.test, undefined);
for (const statement of /** @type {Node[]} */ (branch.consequent)) {
walkPart(statement, branch);
}
if (reads) return true;
}
return false;
};
defineOptimizer("Switch", (self, compressor) => {
if (!compressor.option("switches")) return self;
let value = ast.evaluate(self.discriminant, compressor);
if (!A.isSyntaxNode(value)) {
const original = self.discriminant;
self.discriminant = makeNodeFromConstant(value, original);
self.discriminant = bestOfExpression(
transformNode(self.discriminant, compressor),
original
);
}
if (!compressor.option("dead_code")) return self;
// A case's `let`, `const` or class binds across the whole `switch`, so a
// case left standing may read it in its dead zone, which terser forgets.
if (
modules.corrections !== undefined &&
modules.corrections.enabled &&
readsAnotherCaseBinding(self)
) {
return self;
}
if (A.isSyntaxNode(value)) {
value = ast.evaluate(ast.tailNode(self.discriminant), compressor);
}
/** @type {Node[]} */
const declarations = [];
/** @type {Node[]} */
const body = [];
/**
* @param {Node | undefined} node a statement
* @param {Walker} stack the walker that visits it
* @returns {boolean} whether it breaks out of this switch
*/
const isBreak = (node, stack) =>
A.isBreakNode(node) &&
stack.loopcontrol_target(/** @type {Node} */ (node)) === self;
/**
* @param {Node} branch a case or the default
* @param {Node=} previous the branch before it
* @returns {void}
*/
const eliminateBranch = (branch, previous) => {
if (previous && !aborts(previous)) {
previous.consequent = [
.../** @type {Node[]} */ (previous.consequent),
.../** @type {Node[]} */ (branch.consequent)
];
} else {
extractFromUnreachableCode(compressor, branch, declarations);
}
};
/**
* @param {Node} branch a branch
* @param {Node} previous another branch
* @param {boolean} insertBreak whether `branch` is read with a `break` after it
* @returns {boolean} whether both run the same statements
*/
const branchesEquivalent = (branch, previous, insertBreak) => {
let branchBody = branch.consequent;
const previousBody = previous.consequent;
if (insertBreak) {
branchBody = [
.../** @type {Node[]} */ (branchBody),
makeNode(A.BreakNode)
];
}
if (
/** @type {Node[]} */ (branchBody).length !==
/** @type {Node[]} */ (previousBody).length
) {
return false;
}
const branchBlock = makeNode(A.BlockStatementNode, branch, {
body: branchBody
});
const previousBlock = makeNode(A.BlockStatementNode, previous, {
body: previousBody
});
return ast.isEquivalent(branchBlock, previousBlock);
};
/**
* @param {Node} expression an expression
* @returns {Node} a statement of it
*/
const statement = (expression) =>
makeNode(A.SimpleStatementNode, expression, { expression });
/**
* @param {Node} root the switch
* @returns {boolean} whether it breaks out other than at the end of a branch
*/
const hasNestedBreak = (root) => {
let hasBreak = false;
const walker = createWalker((/** @type {Node} */ node) => {
if (hasBreak) return true;
if (A.isLambdaNode(node)) return true;
if (A.isSimpleStatementNode(node)) return true;
if (!isBreak(node, walker)) return undefined;
const parent = walker.parent();
if (
A.isSwitchBranchNode(parent) &&
/** @type {Node[]} */ (parent.consequent)[
/** @type {Node[]} */ (parent.consequent).length - 1
] === node
) {
return undefined;
}
hasBreak = true;
return undefined;
});
walkNode(root, walker);
return hasBreak;
};
/**
* @param {Node} branch a branch
* @returns {boolean} whether running its statements does nothing
*/
const isInertBody = (branch) =>
!aborts(branch) &&
!ast.hasSideEffects(
makeNode(A.BlockStatementNode, branch, {
body: branch.consequent
}),
compressor
) &&
!declaresInFunction(branch);
/**
* terser drops a branch it reads as inert along with any `var` or function
* in it, whose name the function's other code then no longer binds.
* @param {Node} branch a branch
* @returns {boolean} whether, in the correct phase, it declares one
*/
const declaresInFunction = (branch) => {
if (modules.corrections === undefined || !modules.corrections.enabled) {
return false;
}
/** @type {Node[]} */
const declared = [];
extractFromUnreachableCode(compressor, branch, declared);
return declared.length > 0;
};
/** @type {Node | null | undefined} */
let defaultBranch;
/** @type {Node | undefined} */
let exactMatch;
// Compresses the branches into `body`, keeps one default, and finds the
// branch whose case is the value, as `case 1234` in `switch (1234)`.
let i = 0;
const length = self.cases.length;
for (; i < length && !exactMatch; i++) {
const branch = self.cases[i];
if (A.isDefaultNode(branch)) {
if (!defaultBranch) {
defaultBranch = branch;
} else {
eliminateBranch(branch, body[body.length - 1]);
}
} else if (!A.isSyntaxNode(value)) {
let expression = ast.evaluate(branch.test, compressor);
if (!A.isSyntaxNode(expression) && expression !== value) {
eliminateBranch(branch, body[body.length - 1]);
continue;
}
if (
A.isSyntaxNode(expression) &&
!ast.hasSideEffects(expression, compressor)
) {
expression = ast.evaluate(ast.tailNode(branch.test), compressor);
}
if (expression === value) {
exactMatch = branch;
if (defaultBranch) {
const defaultIndex = body.indexOf(defaultBranch);
body.splice(defaultIndex, 1);
eliminateBranch(defaultBranch, body[defaultIndex - 1]);
defaultBranch = null;
}
}
}
body.push(branch);
}
// Short of the end only after an exact match: the rest never runs.
while (i < length) eliminateBranch(self.cases[i++], body[body.length - 1]);
self.cases = body;
/** @type {Node | null | undefined} */
let defaultOrExact = defaultBranch || exactMatch;
// Groups equivalent branches, which the loop after merges, where every
// branch is a constant case ending in a jump.
if (
body.every(
(branch, index) =>
// prettier-ignore
(branch === defaultOrExact || A.isConstantNode(branch.test)) &&
(/** @type {Node[]} */ (branch.consequent).length === 0 ||
aborts(branch) ||
body.length - 1 === index)
)
) {
for (let i = 0; i < body.length; i++) {
const branch = body[i];
for (let j = i + 1; j < body.length; j++) {
const next = body[j];
if (/** @type {Node[]} */ (next.consequent).length === 0) continue;
const lastBranch = j === body.length - 1;
const equivalentBranch = branchesEquivalent(next, branch, false);
if (
equivalentBranch ||
(lastBranch && branchesEquivalent(next, branch, true))
) {
if (!equivalentBranch && lastBranch) {
/** @type {Node[]} */ (next.consequent).push(
makeNode(A.BreakNode)
);
}
// The branches before it that fall through to it inertly.
let x = j - 1;
let fallthroughDepth = 0;
while (x > i) {
if (isInertBody(body[x--])) {
fallthroughDepth++;
} else {
break;
}
}
const plucked = body.splice(
j - fallthroughDepth,
1 + fallthroughDepth
);
body.splice(i + 1, 0, ...plucked);
i += plucked.length;
}
}
}
}
// Merges runs of equivalent branches.
for (let i = 0; i < body.length; i++) {
let branch = body[i];
if (/** @type {Node[]} */ (branch.consequent).length === 0) continue;
if (!aborts(branch)) continue;
for (let j = i + 1; j < body.length; i++, j++) {
const next = body[j];
if (/** @type {Node[]} */ (next.consequent).length === 0) continue;
if (
branchesEquivalent(next, branch, false) ||
(j === body.length - 1 && branchesEquivalent(next, branch, true))
) {
branch.consequent = [];
branch = next;
continue;
}
break;
}
}
// Prunes the empty branches at the end.
{
let i = body.length - 1;
for (; i >= 0; i--) {
const branchBody = body[i].consequent;
while (
isBreak(
/** @type {Node[]} */ (branchBody)[
/** @type {Node[]} */ (branchBody).length - 1
],
compressor
)
) {
/** @type {Node[]} */ (branchBody).pop();
}
if (!isInertBody(body[i])) break;
}
// The first of the empty branches.
i++;
if (!defaultOrExact || body.indexOf(defaultOrExact) >= i) {
// Doing nothing is the default, so side-effect-free cases that do
// nothing go, back to the last one with side effects.
for (let j = body.length - 1; j >= i; j--) {
const branch = body[j];
if (branch === defaultOrExact) {
defaultOrExact = null;
eliminateBranch(/** @type {Node} */ (body.pop()));
} else if (!ast.hasSideEffects(branch.test, compressor)) {
eliminateBranch(/** @type {Node} */ (body.pop()));
} else {
break;
}
}
}
}
// Prunes side-effect-free branches that fall into the default.
DEFAULT: if (defaultOrExact) {
const defaultIndex = body.indexOf(defaultOrExact);
let defaultBodyIndex = defaultIndex;
for (; defaultBodyIndex < body.length - 1; defaultBodyIndex++) {
if (!isInertBody(body[defaultBodyIndex])) break;
}
if (defaultBodyIndex < body.length - 1) {
break DEFAULT;
}
let sideEffectIndex = body.length - 1;
for (; sideEffectIndex >= 0; sideEffectIndex--) {
const branch = body[sideEffectIndex];
if (branch === defaultOrExact) continue;
if (ast.hasSideEffects(branch.test, compressor)) break;
}
// Side-effect-free cases fold into the default only where it comes
// after every case with side effects, which they could skip.
if (defaultBodyIndex > sideEffectIndex) {
let previousBodyIndex = defaultIndex - 1;
for (; previousBodyIndex >= 0; previousBodyIndex--) {
if (!isInertBody(body[previousBodyIndex])) break;
}
const before = Math.max(sideEffectIndex, previousBodyIndex) + 1;
let after = defaultIndex;
if (sideEffectIndex > defaultIndex) {
// The case with side effects that the default falls into stays,
// and only the cases after it go.
after = sideEffectIndex;
body[sideEffectIndex].consequent = body[defaultBodyIndex].consequent;
} else {
// The default is the last branch.
defaultOrExact.consequent = body[defaultBodyIndex].consequent;
}
// Everything after the default, or the last case with side
// effects, up to the next case with a body.
body.splice(after + 1, defaultBodyIndex - after);
// Everything before the default that falls into it.
body.splice(before, defaultIndex - before);
}
}
// Drops the switch where every case falls into the one body.
DEFAULT: if (defaultOrExact) {
// -1 where no branch has a body, the last where all fall into it.
const index = body.findIndex(
(/** @type {Node} */ branch) => !isInertBody(branch)
);
let caseBody;
if (index === body.length - 1) {
const branch = body[index];
if (hasNestedBreak(self)) break DEFAULT;
// The last body, its breaks pruned already, so it can be hoisted.
caseBody = makeNode(A.BlockStatementNode, branch, {
body: branch.consequent
});
branch.consequent = [];
} else if (index !== -1) {
// Several bodies.
break DEFAULT;
}
const sideEffect = body.find(
(/** @type {Node} */ branch) =>
branch !== defaultOrExact &&
ast.hasSideEffects(branch.test, compressor)
);
if (!sideEffect) {
return ast.optimize(
makeNode(A.BlockStatementNode, self, {
body: [
...declarations,
statement(self.discriminant),
...(defaultOrExact.test ? [statement(defaultOrExact.test)] : []),
...(caseBody ? [caseBody] : [])
]
}),
compressor
);
}
// With no body left in it, doing nothing is the default, so the
// default goes; a body is hoisted to after the switch.
const defaultIndex = body.indexOf(defaultOrExact);
body.splice(defaultIndex, 1);
defaultOrExact = null;
if (caseBody) {
// Optimized once more, with no default left to recurse on.
return ast.optimize(
makeNode(A.BlockStatementNode, self, {
body: [...declarations, self, caseBody]
}),
compressor
);
}
}
// The `var`s of the branches dropped.
if (body.length > 0) {
body[0].consequent = [
...declarations,
.../** @type {Node[]} */ (body[0].consequent)
];
}
if (body.length === 0) {
return ast.optimize(
makeNode(A.BlockStatementNode, self, {
body: [...declarations, statement(self.discriminant)]
}),
compressor
);
}
if (body.length === 1 && !hasNestedBreak(self)) {
// The last body, its breaks pruned already, so it can be hoisted.
const branch = body[0];
return ast.optimize(
makeNode(A.IfNode, self, {
test: makeNode(A.BinaryNode, self, {
operator: "===",
left: self.discriminant,
right: branch.test
}),
consequent: makeNode(A.BlockStatementNode, branch, {
body: branch.consequent
}),
alternate: null
}),
compressor
);
}
if (body.length === 2 && defaultOrExact && !hasNestedBreak(self)) {
const branch = body[0] === defaultOrExact ? body[1] : body[0];
const exactStatement =
defaultOrExact.test && statement(defaultOrExact.test);
if (aborts(body[0])) {
// Only the first branch can end in a break.
const first = body[0];
if (
isBreak(
/** @type {Node[]} */ (first.consequent)[
/** @type {Node[]} */ (first.consequent).length - 1
],
compressor
)
) {
/** @type {Node[]} */ (first.consequent).pop();
}
return ast.optimize(
makeNode(A.IfNode, self, {
test: makeNode(A.BinaryNode, self, {
operator: "===",
left: self.discriminant,
right: branch.test
}),
consequent: makeNode(A.BlockStatementNode, branch, {
body: branch.consequent
}),
alternate: makeNode(A.BlockStatementNode, defaultOrExact, {
body: [
...(exactStatement ? [exactStatement] : []),
.../** @type {Node[]} */ (defaultOrExact.consequent)
]
})
}),
compressor
);
}
let operator = "===";
let consequent = makeNode(A.BlockStatementNode, branch, {
body: branch.consequent
});
let always = makeNode(A.BlockStatementNode, defaultOrExact, {
body: [
...(exactStatement ? [exactStatement] : []),
.../** @type {Node[]} */ (defaultOrExact.consequent)
]
});
if (body[0] === defaultOrExact) {
operator = "!==";
const swapped = always;
always = consequent;
consequent = swapped;
}
return ast.optimize(
makeNode(A.BlockStatementNode, self, {
body: [
makeNode(A.IfNode, self, {
test: makeNode(A.BinaryNode, self, {
operator,
left: self.discriminant,
right: branch.test
}),
consequent,
alternate: null
}),
always
]
}),
compressor
);
}
return self;
});
defineOptimizer("Try", (self, compressor) => {
if (
self.handler &&
self.finalizer &&
/** @type {Node[]} */ (self.finalizer.body).every(isEmpty)
) {
self.finalizer = /** @type {Node} */ (/** @type {Node | null} */ (null));
}
if (
compressor.option("dead_code") &&
/** @type {Node[]} */ (self.block.body).every(isEmpty)
) {
/** @type {Node[]} */
const body = [];
if (self.handler) {
extractFromUnreachableCode(compressor, self.handler, body);
}
if (self.finalizer) {
body.push(.../** @type {Node[]} */ (self.finalizer.body));
}
return ast.optimize(
makeNode(A.BlockStatementNode, self, {
body
}),
compressor
);
}
return self;
});
/**
* @param {Node} self a declaration
* @param {Compressor} compressor the compressor
* @returns {Node | null} the assignments of its initialized names, if any
*/
ast.toAssignments = function toAssignments(self, compressor) {
const reduceVars = compressor.option("reduce_vars");
/** @type {Node[]} */
const assignments = [];
for (const definition of self.declarations) {
if (definition.init) {
const name = makeNode(A.SymbolRefNode, definition.id, definition.id);
assignments.push(
makeNode(A.AssignNode, definition, {
operator: "=",
logical: false,
left: name,
right: definition.init
})
);
if (reduceVars) name.definition.fixed = false;
}
const variable = definition.id.definition;
variable.eliminated++;
variable.replaced--;
}
if (assignments.length === 0) return null;
return makeSequence(self, assignments);
};
defineOptimizer("Definitions", (self) => {
if (self.declarations.length === 0) {
return makeNode(A.EmptyStatementNode, self);
}
return self;
});
defineOptimizer("VarDef", (self, compressor) => {
if (
A.isSymbolLetNode(self.id) &&
self.init !== null &&
self.init !== undefined &&
isUndefined(self.init, compressor)
) {
self.init = /** @type {Node} */ (/** @type {Node | null} */ (null));
}
return self;
});
defineOptimizer("Import", (self) => self);
};
/**
* Installs terser's optimizers of calls, `new`, sequences and unary and binary
* operations, with the `lift_sequences` and `contains_optional` they call.
* @param {TerserModules} modules terser's modules
* @param {OptimizerHelpers} helpers what the optimizers share
* @returns {void}
*/
const installOperatorOptimizers = (modules, helpers) => {
const { transformNode } = modules.ast;
const { ast, common, flags, inference, utils } = modules;
const A = ast;
const { PRECEDENCE, parse, JS_Parse_Error: JSParseError } = modules.parse;
const { OutputStream } = modules.output;
const { createCompressor } = modules.compress;
const {
make_sequence: makeSequence,
best_of: bestOf,
make_empty_function: makeEmptyFunction,
make_node_from_constant: makeNodeFromConstant,
merge_sequence: mergeSequence,
maintain_this_binding: maintainThisBinding,
is_identifier_atom: isIdentifierAtom
} = common;
const {
is_undeclared_ref: isUndeclaredRef,
bitwise_binop: bitwiseOperators,
lazy_op: lazyOperators,
is_nullish: isNullish,
is_undefined: isUndefined
} = inference;
const { has_flag: hasFlag, set_flag: setFlag, UNUSED, TRUTHY, FALSY } = flags;
const {
make_void_0: makeVoid0,
makePredicate,
regexp_source_fix: regexpSourceFix,
regexp_is_safe: regexpIsSafe
} = utils;
const {
defineOptimizer,
inlineArrayLikeSpread,
unsafeUndefinedRef,
inlineIntoCall
} = helpers;
const firstInStatement =
/** @type {(compressor: Compressor) => boolean | undefined} */ (
createFirstInStatement(modules)
);
const commutativeOperators = makePredicate("== === != !== * & | ^");
const unsafeConstructors = ["Object", "RegExp", "Function", "Error", "Array"];
/**
* terser's `is_object`.
* @param {Node} node a node
* @returns {boolean} whether it is an array, function, object or class literal
*/
const isObject = (node) =>
A.isArrayNode(node) ||
A.isLambdaNode(node) ||
A.isObjectNode(node) ||
A.isClassNode(node);
/**
* @param {Node} node an operand
* @returns {string | undefined} the type of the literal it is, if one: a number, bigint, string or boolean, negated or not
*/
const literalTypeOf = (node) => {
if (A.isUnaryPrefixNode(node)) {
if (node.operator === "!") {
return A.isNumberNode(node.argument) ? "boolean" : undefined;
}
return node.operator === "-" ? numericTypeOf(node.argument) : undefined;
}
if (A.isStringNode(node)) return "string";
if (A.isBooleanNode(node)) return "boolean";
return numericTypeOf(node);
};
/**
* @param {Node} node an operand
* @returns {string | undefined} `number` or `bigint` for a literal of one
*/
const numericTypeOf = (node) =>
A.isNumberNode(node)
? "number"
: A.isBigIntNode(node)
? "bigint"
: undefined;
/**
* What an operand evaluates to where only its truth is read: once improved,
* a string kept unevaluated as longer than the expression gives its truth.
* @param {Node} node an operand
* @param {Compressor} compressor the compressor
* @returns {EvaluatedValue | Node} its value, its truth, or itself
*/
const truthOf = (node, compressor) => {
const value = ast.evaluate(node, compressor);
if (
value !== node ||
modules.improvements === undefined ||
!modules.improvements.enabled
) {
return value;
}
const evaluated = ast.evaluateNode(node, compressor, 1);
return typeof evaluated === "string" ? evaluated !== "" : node;
};
/**
* What a comparison tests against `null` or `undefined`, as terser reads it
* with the constant first, or with either side once improved.
* @param {Node} node a node
* @param {string} operator the comparison, `===` or `!==`
* @param {boolean} improved whether the constant may come second
* @param {Compressor} compressor the compressor
* @returns {{ operand: Node, isNull: boolean } | undefined} the other side, and whether the constant is `null`
*/
const nullishTested = (node, operator, improved, compressor) => {
if (!A.isBinaryNode(node) || node.operator !== operator) return;
for (const [constant, operand] of improved
? [
[node.left, node.right],
[node.right, node.left]
]
: [[node.left, node.right]]) {
if (A.isNullNode(constant)) return { operand, isNull: true };
if (isUndefined(constant, compressor)) return { operand, isNull: false };
}
};
/**
* terser's `[…].join(…)` folding, from inside its `CallNode` optimizer.
* @param {Node} self the call
* @param {Node} expression its callee, a `.join` read of an array literal
* @param {Compressor} compressor the compressor
* @returns {Node | null} what replaces the call, or null to go on
*/
const optimizeJoin = (self, expression, compressor) => {
const { arrayPrototypePatched } = modules.webpackInline;
if (arrayPrototypePatched && arrayPrototypePatched()) return null;
let separator;
if (self.arguments.length > 0) {
separator = ast.evaluate(self.arguments[0], compressor);
if (separator === self.arguments[0]) return null;
}
/** @type {(Node | null)[]} */
const elements = [];
/** @type {EvaluatedArray} */
const constants = [];
for (
let i = 0, length = expression.object.elements.length;
i < length;
i++
) {
const element = expression.object.elements[i];
if (A.isExpansionNode(element)) return null;
// A hole evaluates as terser's hole node did: undefined, or itself.
const value =
element === null
? compressor.option("evaluate")
? undefined
: null
: ast.evaluate(element, compressor);
if (value !== element) {
constants.push(/** @type {EvaluatedValue} */ (value));
} else {
if (constants.length > 0) {
elements.push(
makeNode(A.StringNode, self, {
value: constants.join(
/** @type {string | undefined} */ (separator)
)
})
);
constants.length = 0;
}
elements.push(element);
}
}
if (constants.length > 0) {
elements.push(
makeNode(A.StringNode, self, {
value: constants.join(/** @type {string | undefined} */ (separator))
})
);
}
if (elements.length === 0) {
return makeNode(A.StringNode, self, { value: "" });
}
if (elements.length === 1) {
if (elements[0] !== null && ast.isString(elements[0], compressor)) {
return elements[0];
}
return makeNode(A.BinaryNode, /** @type {Node} */ (elements[0]), {
operator: "+",
left: makeNode(A.StringNode, self, { value: "" }),
right: elements[0]
});
}
// A separator of `0`, `false` or `[]` joins with "" here too.
// eslint-disable-next-line eqeqeq
if (separator == "") {
// The separator evaluated, so each hole did too.
const joined = /** @type {Node[]} */ (elements);
const first =
ast.isString(joined[0], compressor) ||
ast.isString(joined[1], compressor)
? /** @type {Node} */ (joined.shift())
: makeNode(A.StringNode, self, { value: "" });
return ast.optimize(
joined.reduce(
(previous, element) =>
makeNode(A.BinaryNode, element, {
operator: "+",
left: previous,
right: element
}),
first
),
compressor
);
}
// Cloned down to the array so the original stays intact for `best_of`.
const node = ast.cloneNode(self);
node.callee = ast.cloneNode(node.callee);
node.callee.object = ast.cloneNode(node.callee.object);
node.callee.object.elements = elements;
return bestOf(compressor, self, node);
};
defineOptimizer("Call", (self, compressor) => {
const expression = self.callee;
let fn = expression;
inlineArrayLikeSpread(self.arguments);
const simpleArgs = self.arguments.every(
(/** @type {Node} */ argument) => !A.isExpansionNode(argument)
);
if (compressor.option("reduce_vars") && A.isSymbolRefNode(fn)) {
fn = /** @type {Node} */ (ast.fixedValue(fn));
}
const isFunction = A.isLambdaNode(fn);
if (isFunction && ast.isPinned(fn)) return self;
if (
compressor.option("unused") &&
simpleArgs &&
isFunction &&
!ast.usesArguments(fn)
) {
let position = 0;
let last = 0;
for (let i = 0, length = self.arguments.length; i < length; i++) {
if (A.isExpansionNode(fn.params[i])) {
if (hasFlag(fn.params[i].argument, UNUSED)) {
while (i < length) {
const node = ast.dropSideEffectFree(
self.arguments[i++],
compressor
);
if (node) {
self.arguments[position++] = node;
}
}
} else {
while (i < length) {
self.arguments[position++] = self.arguments[i++];
}
}
last = position;
break;
}
const trim = i >= fn.params.length;
if (trim || hasFlag(fn.params[i], UNUSED)) {
const node = ast.dropSideEffectFree(self.arguments[i], compressor);
if (node) {
self.arguments[position++] = node;
} else if (!trim) {
self.arguments[position++] = makeNode(
A.NumberNode,
self.arguments[i],
{
value: 0
}
);
continue;
}
} else {
self.arguments[position++] = self.arguments[i];
}
last = position;
}
self.arguments.length = last;
}
if (
A.isDotNode(expression) &&
A.isSymbolRefNode(expression.object) &&
expression.object.name === "console" &&
expression.object.definition.undeclared &&
expression.property.name === "assert"
) {
const condition = self.arguments[0];
if (condition) {
const value = ast.evaluate(condition, compressor);
if (value === 1 || value === true) {
return ast.optimize(makeVoid0(self), compressor);
}
}
}
if (compressor.option("unsafe") && !ast.containsOptional(expression)) {
if (
A.isDotNode(expression) &&
/** @type {NodeToken} */ (
compressor.tokenTables.of(expression.startToken)
).value === "Array" &&
expression.property.name === "from" &&
self.arguments.length === 1
) {
const [argument] = self.arguments;
if (A.isArrayNode(argument)) {
return ast.optimize(
makeNode(A.ArrayNode, argument, {
elements: argument.elements
}),
compressor
);
}
}
if (isUndeclaredRef(expression)) {
switch (expression.name) {
case "Array":
if (self.arguments.length !== 1) {
return ast.optimize(
makeNode(A.ArrayNode, self, {
elements: self.arguments
}),
compressor
);
} else if (
A.isNumberNode(self.arguments[0]) &&
/** @type {number} */ (self.arguments[0].value) <= 11
) {
const elements = [];
for (
let i = 0;
i < /** @type {number} */ (self.arguments[0].value);
i++
) {
elements.push(null);
}
return A.ArrayNode({ elements });
}
break;
case "Object":
if (self.arguments.length === 0) {
return makeNode(A.ObjectNode, self, {
properties: []
});
}
break;
case "String":
if (self.arguments.length === 0) {
return makeNode(A.StringNode, self, {
value: ""
});
}
if (self.arguments.length <= 1) {
return ast.optimize(
makeNode(A.BinaryNode, self, {
left: self.arguments[0],
operator: "+",
right: makeNode(A.StringNode, self, { value: "" })
}),
compressor
);
}
break;
case "Number":
if (self.arguments.length === 0) {
return makeNode(A.NumberNode, self, {
value: 0
});
}
if (
self.arguments.length === 1 &&
compressor.option("unsafe_math")
) {
return ast.optimize(
makeNode(A.UnaryPrefixNode, self, {
argument: self.arguments[0],
operator: "+"
}),
compressor
);
}
break;
case "Symbol":
if (
self.arguments.length === 1 &&
A.isStringNode(self.arguments[0]) &&
compressor.option("unsafe_symbols")
) {
self.arguments.length = 0;
}
break;
case "Boolean":
if (self.arguments.length === 0) return makeNode(A.FalseNode, self);
if (self.arguments.length === 1) {
return ast.optimize(
makeNode(A.UnaryPrefixNode, self, {
argument: makeNode(A.UnaryPrefixNode, self, {
argument: self.arguments[0],
operator: "!"
}),
operator: "!"
}),
compressor
);
}
break;
case "RegExp": {
/** @type {(EvaluatedValue | Node)[]} */
const params = [];
if (
self.arguments.length >= 1 &&
self.arguments.length <= 2 &&
self.arguments.every((/** @type {Node} */ argument) => {
const value = ast.evaluate(argument, compressor);
params.push(value);
return argument !== value;
}) &&
regexpIsSafe(/** @type {string} */ (params[0]))
) {
let [source] = params;
const regexpFlags = params[1];
source = regexpSourceFix(
new RegExp(/** @type {string} */ (source)).source
);
const regexp = makeNode(A.RegExpNode, self, {
value: { source, flags: /** @type {string} */ (regexpFlags) }
});
if (ast.evaluateNode(regexp, compressor) !== regexp) {
return regexp;
}
}
break;
}
}
} else if (A.isDotNode(expression)) {
switch (expression.property.name) {
case "toString":
if (
self.arguments.length === 0 &&
!ast.mayThrowOnAccess(expression.object, compressor)
) {
return ast.optimize(
makeNode(A.BinaryNode, self, {
left: makeNode(A.StringNode, self, { value: "" }),
operator: "+",
right: expression.object
}),
compressor
);
}
break;
case "join":
if (A.isArrayNode(expression.object)) {
const joined = optimizeJoin(self, expression, compressor);
if (joined) return joined;
}
break;
case "charAt":
if (ast.isString(expression.object, compressor)) {
const argument = self.arguments[0];
const index = argument ? ast.evaluate(argument, compressor) : 0;
if (index !== argument) {
return ast.optimize(
makeNode(A.SubNode, expression, {
object: expression.object,
property: makeNodeFromConstant(
/** @type {number} */ (index) | 0,
argument || expression
),
computed: true
}),
compressor
);
}
}
break;
case "apply":
if (
self.arguments.length === 2 &&
A.isArrayNode(self.arguments[1]) &&
// A hole reads through the array's prototype, so it cannot
// become a missing argument or an unconditional `undefined`.
!self.arguments[1].elements.includes(null)
) {
const args = [...self.arguments[1].elements];
args.unshift(self.arguments[0]);
return ast.optimize(
makeNode(A.CallNode, self, {
callee: makeNode(A.DotNode, expression, {
object: expression.object,
optional: false,
property: A.SymbolPropertyNode({ name: "call" }),
computed: false
}),
arguments: /** @type {Node[]} */ (args)
}),
compressor
);
}
break;
case "call": {
let func = expression.object;
if (A.isSymbolRefNode(func)) {
func = /** @type {Node} */ (ast.fixedValue(func));
}
if (A.isLambdaNode(func) && !ast.containsThis(func)) {
// terser passes its optimizer's `this`, undefined, so no position.
return ast.optimize(
self.arguments.length
? makeSequence(undefined, [
self.arguments[0],
makeNode(A.CallNode, self, {
callee: expression.object,
arguments: self.arguments.slice(1)
})
])
: makeNode(A.CallNode, self, {
callee: expression.object,
arguments: []
}),
compressor
);
}
break;
}
}
}
}
if (
compressor.option("unsafe_Function") &&
isUndeclaredRef(expression) &&
expression.name === "Function"
) {
if (self.arguments.length === 0) {
return ast.optimize(makeEmptyFunction(self), compressor);
}
if (
self.arguments.every((/** @type {Node} */ argument) =>
A.isStringNode(argument)
)
) {
// A constant `new Function` body is minified as a function of its own:
// https://github.com/mishoo/UglifyJS2/issues/203
try {
const code = `n(function(${self.arguments
.slice(0, -1)
.map((/** @type {Node} */ argument) => argument.value)
.join(",")}){${self.arguments[self.arguments.length - 1].value}})`;
let program = parse(code);
const mangle = compressor.mangle_options();
ast.figureOutScope(program, mangle);
const innerCompressor = createCompressor(compressor.options, {
mangle_options: compressor._mangle_options
});
assignNativeLookups(innerCompressor, modules.nativeObjects);
innerCompressor.tokenTables = /** @type {TokenTables} */ (
program.tokenTables
);
program = transformNode(program, innerCompressor);
const functionNode = /** @type {PrintFunction} */ (
modules.mangledFunctionOf(program, mangle)
);
const stream = OutputStream();
modules.printFunctionBody(functionNode, stream);
self.arguments = [
makeNode(A.StringNode, self, {
value: functionNode.params
.map((parameter) => modules.printEstreeToString(parameter))
.join(",")
}),
makeNode(A.StringNode, self.arguments[self.arguments.length - 1], {
value: stream.get().replace(/^\{|\}$/g, "")
})
];
return self;
} catch (error) {
// Any other error is left to throw when the code runs.
if (!(error instanceof JSParseError)) {
throw error;
}
}
}
}
return inlineIntoCall(self, compressor);
});
/**
* @param {Node} self a node
* @returns {boolean} whether it holds an optional property read or call
*/
ast.containsOptional = function containsOptional(self) {
if (A.isPropAccessNode(self) || A.isCallNode(self) || A.isChainNode(self)) {
if (self.optional) {
return true;
}
return ast.containsOptional(
A.isCallNode(self)
? self.callee
: A.isPropAccessNode(self)
? self.object
: self.expression
);
}
return false;
};
defineOptimizer("New", (self, compressor) => {
if (
compressor.option("unsafe") &&
isUndeclaredRef(self.callee) &&
unsafeConstructors.includes(self.callee.name)
) {
return transformNode(makeNode(A.CallNode, self, self), compressor);
}
return self;
});
defineOptimizer("Sequence", (self, compressor) => {
if (!compressor.option("side_effects")) return self;
/** @type {Node[]} */
const expressions = [];
let first = firstInStatement(compressor);
const last = self.expressions.length - 1;
for (let index = 0; index <= last; index++) {
/** @type {Node | null} */ let expression = self.expressions[index];
if (index < last) {
expression = ast.dropSideEffectFree(expression, compressor, first);
}
if (expression) {
mergeSequence(expressions, expression);
first = false;
}
}
let end = expressions.length - 1;
while (end > 0 && isUndefined(expressions[end], compressor)) end--;
if (end < expressions.length - 1) {
expressions[end] = makeNode(A.UnaryPrefixNode, self, {
operator: "void",
argument: expressions[end]
});
expressions.length = end + 1;
}
if (end === 0) {
self = maintainThisBinding(
compressor.parent(),
compressor.self(),
expressions[0]
);
if (!A.isSequenceNode(self)) self = ast.optimize(self, compressor);
return self;
}
self.expressions = expressions;
return self;
});
/**
* @param {Node} self a unary operation
* @param {Compressor} compressor the compressor
* @returns {Node} a sequence ending in the operation, where its operand was one
*/
const liftUnarySequences = (self, compressor) => {
if (compressor.option("sequences") && A.isSequenceNode(self.argument)) {
const expressions = [...self.argument.expressions];
const clone = ast.cloneNode(self);
clone.argument = /** @type {Node} */ (expressions.pop());
expressions.push(clone);
return ast.optimize(makeSequence(self, expressions), compressor);
}
return self;
};
defineOptimizer("UnaryPostfix", (self, compressor) =>
liftUnarySequences(self, compressor)
);
defineOptimizer("UnaryPrefix", (self, compressor) => {
/** @type {Node} */ let expression = self.argument;
if (
self.operator === "delete" &&
!(
A.isSymbolRefNode(expression) ||
A.isPropAccessNode(expression) ||
A.isChainNode(expression) ||
isIdentifierAtom(expression)
)
) {
return ast.optimize(
makeSequence(self, [expression, makeNode(A.TrueNode, self)]),
compressor
);
}
if (
self.operator === "void" &&
A.isNumberNode(expression) &&
expression.value === 0
) {
return unsafeUndefinedRef(self, compressor) || self;
}
const sequence = liftUnarySequences(self, compressor);
if (sequence !== self) {
return sequence;
}
if (compressor.option("side_effects") && self.operator === "void") {
const kept = ast.dropSideEffectFree(expression, compressor);
if (kept) {
self.argument = kept;
return self;
}
return ast.optimize(makeVoid0(self), compressor);
}
if (compressor.in_boolean_context()) {
switch (self.operator) {
case "!":
if (A.isUnaryPrefixNode(expression) && expression.operator === "!") {
return expression.argument;
}
if (A.isBinaryNode(expression)) {
self = bestOf(
compressor,
self,
ast.negate(expression, compressor, firstInStatement(compressor))
);
}
break;
case "typeof":
// `typeof` yields a non-empty string, even of an undeclared name.
return ast.optimize(
A.isSymbolRefNode(expression)
? makeNode(A.TrueNode, self)
: makeSequence(self, [expression, makeNode(A.TrueNode, self)]),
compressor
);
}
} else if (
// Where its value is read, `!(a == b || c == d)` as `a != b && c != d`,
// which yields the same boolean.
self.operator === "!" &&
A.isBinaryNode(expression) &&
(expression.operator === "&&" || expression.operator === "||") &&
modules.improvements !== undefined &&
modules.improvements.enabled
) {
const negated = ast.negate(
expression,
compressor,
firstInStatement(compressor)
);
if (ast.isBoolean(negated)) self = bestOf(compressor, self, negated);
}
if (self.operator === "-" && A.isInfinityNode(expression)) {
expression = transformNode(expression, compressor);
}
if (
A.isBinaryNode(expression) &&
(self.operator === "+" || self.operator === "-") &&
(expression.operator === "*" ||
expression.operator === "/" ||
expression.operator === "%")
) {
return makeNode(A.BinaryNode, self, {
operator: expression.operator,
left: makeNode(A.UnaryPrefixNode, expression.left, {
operator: self.operator,
argument: expression.left
}),
right: expression.right
});
}
if (compressor.option("evaluate")) {
// ~~x => x, where only 32 bits are read or x has no more
if (
self.operator === "~" &&
A.isUnaryPrefixNode(self.argument) &&
self.argument.operator === "~" &&
(compressor.in_32_bit_context(false) ||
ast.is32BitInteger(self.argument.argument, compressor))
) {
return self.argument.argument;
}
// ~(x ^ y) => x ^ ~y, and ~(~x ^ y) => x ^ y
if (
self.operator === "~" &&
A.isBinaryNode(expression) &&
expression.operator === "^"
) {
if (
A.isUnaryPrefixNode(expression.left) &&
expression.left.operator === "~"
) {
expression.left = ast.bitwiseNegate(
expression.left,
compressor,
true
);
} else {
expression.right = ast.bitwiseNegate(
expression.right,
compressor,
true
);
}
return expression;
}
}
if (
self.operator !== "-" ||
// A negative number literal would fold into itself forever.
!(
A.isNumberNode(expression) ||
A.isInfinityNode(expression) ||
A.isBigIntNode(expression)
)
) {
let evaluated = ast.evaluate(self, compressor);
if (evaluated !== self) {
evaluated = ast.optimize(
makeNodeFromConstant(/** @type {EvaluatedValue} */ (evaluated), self),
compressor
);
return bestOf(compressor, evaluated, self);
}
}
return self;
});
/**
* @param {Node} self a binary operation
* @param {Compressor} compressor the compressor
* @returns {Node} a sequence ending in the operation, where an operand was one
*/
ast.liftBinarySequences = (self, compressor) => {
if (compressor.option("sequences")) {
if (A.isSequenceNode(self.left)) {
const expressions = [...self.left.expressions];
const clone = ast.cloneNode(self);
clone.left = /** @type {Node} */ (expressions.pop());
expressions.push(clone);
return ast.optimize(makeSequence(self, expressions), compressor);
}
if (
A.isSequenceNode(self.right) &&
!ast.hasSideEffects(self.left, compressor)
) {
const assign = self.operator === "=" && A.isSymbolRefNode(self.left);
const expressions = self.right.expressions;
const last = expressions.length - 1;
let i = 0;
for (; i < last; i++) {
if (!assign && ast.hasSideEffects(expressions[i], compressor)) break;
}
if (i === last) {
const lifted = [...expressions];
const clone = ast.cloneNode(self);
clone.right = /** @type {Node} */ (lifted.pop());
lifted.push(clone);
return ast.optimize(makeSequence(self, lifted), compressor);
} else if (i > 0) {
const clone = ast.cloneNode(self);
clone.right = makeSequence(self.right, expressions.slice(i));
const lifted = expressions.slice(0, i);
lifted.push(clone);
return ast.optimize(makeSequence(self, lifted), compressor);
}
}
}
return self;
};
/**
* Whether `a + b`, split out of a string concatenation, still concatenates,
* where terser regroups `"s" + true + 2` as though `true + 2` did.
* @param {Node} a the left operand
* @param {Node} b the right operand
* @param {Compressor} compressor the compressor
* @returns {boolean} true when either is a string, or terser's reading holds
*/
const joinsAsStrings = (a, b, compressor) =>
modules.corrections === undefined ||
!modules.corrections.enabled ||
ast.isString(a, compressor) ||
ast.isString(b, compressor);
/**
* Whether a node's value is an object however it runs, as swc and oxc read a
* function, class, object or array literal, or an assignment of one.
* @param {Node} node a node
* @returns {boolean} true for such a node, under the improvements
*/
const isAlwaysObject = (node) => {
if (modules.improvements === undefined || !modules.improvements.enabled) {
return false;
}
for (;;) {
if (
A.isFunctionNode(node) ||
A.isArrowNode(node) ||
A.isClassExpressionNode(node) ||
A.isObjectNode(node) ||
A.isArrayNode(node) ||
A.isRegExpNode(node)
) {
return true;
}
if (A.isAssignNode(node) && node.operator === "=") {
node = node.right;
} else if (A.isSequenceNode(node)) {
node = node.expressions[node.expressions.length - 1];
} else {
return false;
}
}
};
defineOptimizer("Binary", (self, compressor) => {
/**
* @returns {boolean} whether swapping the operands keeps what runs
*/
const reversible = () =>
ast.isConstant(self.left) ||
ast.isConstant(self.right) ||
(!ast.hasSideEffects(self.left, compressor) &&
!ast.hasSideEffects(self.right, compressor));
/**
* @param {string=} operator the operator after the swap
* @returns {void}
*/
const reverse = (operator) => {
if (reversible()) {
if (operator) self.operator = operator;
const left = self.left;
self.left = self.right;
self.right = left;
}
};
if (
compressor.option("lhs_constants") &&
commutativeOperators.has(self.operator) &&
ast.isConstant(self.right) &&
!ast.isConstant(self.left) &&
// A constant right cannot see what the left does, so they may swap.
!(
A.isBinaryNode(self.left) &&
PRECEDENCE[self.left.operator] >= PRECEDENCE[self.operator]
)
) {
reverse();
}
self = ast.liftBinarySequences(self, compressor);
if (compressor.option("comparisons")) {
/** @type {boolean | undefined} */
let isStrictComparison;
switch (self.operator) {
case "===":
case "!==":
isStrictComparison = true;
if (
modules.improvements !== undefined &&
modules.improvements.enabled &&
literalTypeOf(self.left) !== undefined &&
literalTypeOf(self.right) !== undefined &&
literalTypeOf(self.left) !== literalTypeOf(self.right)
) {
// Literals of different types are never strictly equal.
return makeNode(
self.operator === "===" ? A.FalseNode : A.TrueNode,
self
);
}
if (
(ast.isString(self.left, compressor) &&
ast.isString(self.right, compressor)) ||
(ast.isNumber(self.left, compressor) &&
ast.isNumber(self.right, compressor)) ||
(ast.isBigInt(self.left, compressor) &&
ast.isBigInt(self.right, compressor)) ||
(ast.isBoolean(self.left) && ast.isBoolean(self.right)) ||
ast.isEquivalent(self.left, self.right)
) {
self.operator = self.operator.slice(0, 2);
}
// falls through
case "==":
case "!=":
if (!isStrictComparison && isUndefined(self.left, compressor)) {
// void 0 == x => null == x
self.left = makeNode(A.NullNode, self.left);
} else if (
!isStrictComparison &&
isUndefined(self.right, compressor)
) {
self.right = makeNode(A.NullNode, self.right);
} else if (
compressor.option("typeofs") &&
// "undefined" == typeof x => undefined === x
A.isStringNode(self.left) &&
self.left.value === "undefined" &&
A.isUnaryPrefixNode(self.right) &&
self.right.operator === "typeof"
) {
const expression = self.right.argument;
if (
A.isSymbolRefNode(expression)
? ast.isDeclared(expression, compressor)
: !(A.isPropAccessNode(expression) && compressor.option("ie8"))
) {
self.right = expression;
self.left = ast.optimize(makeVoid0(self.left), compressor);
if (self.operator.length === 2) self.operator += "=";
}
} else if (
compressor.option("typeofs") &&
A.isUnaryPrefixNode(self.left) &&
self.left.operator === "typeof" &&
A.isStringNode(self.right) &&
self.right.value === "undefined"
) {
const expression = self.left.argument;
if (
A.isSymbolRefNode(expression)
? ast.isDeclared(expression, compressor)
: !(A.isPropAccessNode(expression) && compressor.option("ie8"))
) {
self.left = expression;
self.right = ast.optimize(makeVoid0(self.right), compressor);
if (self.operator.length === 2) self.operator += "=";
}
} else if (
A.isSymbolRefNode(self.left) &&
// obj !== obj => false
A.isSymbolRefNode(self.right) &&
self.left.definition === self.right.definition &&
isObject(/** @type {Node} */ (ast.fixedValue(self.left)))
) {
return makeNode(
self.operator[0] === "=" ? A.TrueNode : A.FalseNode,
self
);
} else if (
ast.is32BitInteger(self.left, compressor) &&
ast.is32BitInteger(self.right, compressor)
) {
/**
* @param {Node} node an operand
* @returns {Node} its negation
*/
const logicalNot = (node) =>
makeNode(A.UnaryPrefixNode, node, {
operator: "!",
argument: node
});
/**
* @param {Node} node an operand
* @param {boolean} truthy whether to test it for truthiness
* @returns {Node} the test, as a boolean where one is read
*/
const asBooleanValue = (node, truthy) => {
if (truthy) {
return compressor.in_boolean_context()
? node
: logicalNot(logicalNot(node));
}
return logicalNot(node);
};
// The only falsy 32-bit integer is 0
if (A.isNumberNode(self.left) && self.left.value === 0) {
return asBooleanValue(self.right, self.operator[0] === "!");
}
if (A.isNumberNode(self.right) && self.right.value === 0) {
return asBooleanValue(self.left, self.operator[0] === "!");
}
// (x & 0xFF) != 0xFF => !(~x & 0xFF)
const andOperation = A.isBinaryNode(self.left)
? self.left
: A.isBinaryNode(self.right)
? self.right
: null;
if (andOperation) {
const mask = andOperation === self.left ? self.right : self.left;
if (
mask &&
andOperation.operator === "&" &&
A.isNumberNode(mask) &&
ast.is32BitInteger(mask, compressor)
) {
const operand = ast.isEquivalent(andOperation.left, mask)
? andOperation.right
: ast.isEquivalent(andOperation.right, mask)
? andOperation.left
: null;
if (operand) {
const optimized = asBooleanValue(
makeNode(A.BinaryNode, self, {
operator: "&",
left: mask,
right: makeNode(A.UnaryPrefixNode, self, {
operator: "~",
argument: operand
})
}),
self.operator[0] === "!"
);
return bestOf(compressor, optimized, self);
}
}
}
}
break;
case "&&":
case "||": {
let lhs = self.left;
if (lhs.operator === self.operator) {
lhs = lhs.right;
}
const operator = self.operator === "&&" ? "!==" : "===";
const improved =
modules.improvements !== undefined && modules.improvements.enabled;
const first = nullishTested(lhs, operator, improved, compressor);
const second = nullishTested(
self.right,
operator,
improved,
compressor
);
if (
first !== undefined &&
second !== undefined &&
first.isNull !== second.isNull &&
((!ast.hasSideEffects(first.operand, compressor) &&
ast.isEquivalent(first.operand, second.operand)) ||
// `(a = b) === null || a === void 0`, reading back what it wrote.
(improved &&
A.isAssignNode(first.operand) &&
first.operand.operator === "=" &&
A.isSymbolRefNode(second.operand) &&
!ast.hasSideEffects(second.operand, compressor) &&
!compressor.find_parent(A.isWithNode) &&
ast.isEquivalent(first.operand.left, second.operand)))
) {
let combined = makeNode(A.BinaryNode, self, {
operator: operator.slice(0, -1),
left: makeNode(A.NullNode, self),
right: first.operand
});
if (lhs !== self.left) {
combined = makeNode(A.BinaryNode, self, {
operator: self.operator,
left: self.left.left,
right: combined
});
}
return combined;
}
break;
}
}
}
if (
self.operator === "+" &&
compressor.in_boolean_context() &&
// Dropping the conversion runs no `toString` the program may patch.
compressor.option("pure_conversions")
) {
const leftValue = ast.evaluate(self.left, compressor);
const rightValue = ast.evaluate(self.right, compressor);
if (leftValue && typeof leftValue === "string") {
return ast.optimize(
makeSequence(self, [self.right, makeNode(A.TrueNode, self)]),
compressor
);
}
if (rightValue && typeof rightValue === "string") {
return ast.optimize(
makeSequence(self, [self.left, makeNode(A.TrueNode, self)]),
compressor
);
}
}
if (compressor.option("comparisons") && ast.isBoolean(self)) {
if (
!A.isBinaryNode(compressor.parent()) ||
A.isAssignNode(compressor.parent())
) {
const negated = makeNode(A.UnaryPrefixNode, self, {
operator: "!",
argument: ast.negate(self, compressor, firstInStatement(compressor))
});
self = bestOf(compressor, self, negated);
}
if (compressor.option("unsafe_comps")) {
switch (self.operator) {
case "<":
reverse(">");
break;
case "<=":
reverse(">=");
break;
}
}
}
if (self.operator === "+") {
if (
A.isStringNode(self.right) &&
ast.constantValue(self.right) === "" &&
ast.isString(self.left, compressor)
) {
return self.left;
}
if (
A.isStringNode(self.left) &&
ast.constantValue(self.left) === "" &&
ast.isString(self.right, compressor)
) {
return self.right;
}
if (
A.isBinaryNode(self.left) &&
self.left.operator === "+" &&
A.isStringNode(self.left.left) &&
ast.constantValue(self.left.left) === "" &&
ast.isString(self.right, compressor)
) {
self.left = self.left.right;
return self;
}
}
if (compressor.option("evaluate")) {
switch (self.operator) {
case "&&": {
const leftValue =
hasFlag(self.left, TRUTHY) || isAlwaysObject(self.left)
? true
: hasFlag(self.left, FALSY)
? false
: truthOf(self.left, compressor);
if (!leftValue) {
return ast.optimize(
maintainThisBinding(
compressor.parent(),
compressor.self(),
self.left
),
compressor
);
} else if (!A.isSyntaxNode(leftValue)) {
return ast.optimize(
makeSequence(self, [self.left, self.right]),
compressor
);
}
const rightValue = truthOf(self.right, compressor);
if (!rightValue) {
if (compressor.in_boolean_context()) {
return ast.optimize(
makeSequence(self, [self.left, makeNode(A.FalseNode, self)]),
compressor
);
}
setFlag(self, FALSY);
} else if (!A.isSyntaxNode(rightValue)) {
const parent = compressor.parent();
if (
(parent.operator === "&&" && parent.left === compressor.self()) ||
compressor.in_boolean_context()
) {
return ast.optimize(self.left, compressor);
}
}
// x || false && y ---> x ? y : false
if (self.left.operator === "||") {
const leftRightValue = ast.evaluate(self.left.right, compressor);
if (!leftRightValue) {
return ast.optimize(
makeNode(A.ConditionalNode, self, {
test: self.left.left,
consequent: self.right,
alternate: self.left.right
}),
compressor
);
}
}
break;
}
case "||": {
const leftValue =
hasFlag(self.left, TRUTHY) || isAlwaysObject(self.left)
? true
: hasFlag(self.left, FALSY)
? false
: truthOf(self.left, compressor);
if (!leftValue) {
return ast.optimize(
makeSequence(self, [self.left, self.right]),
compressor
);
} else if (!A.isSyntaxNode(leftValue)) {
return ast.optimize(
maintainThisBinding(
compressor.parent(),
compressor.self(),
self.left
),
compressor
);
}
const rightValue = truthOf(self.right, compressor);
if (!rightValue) {
const parent = compressor.parent();
if (
(parent.operator === "||" && parent.left === compressor.self()) ||
compressor.in_boolean_context()
) {
return ast.optimize(self.left, compressor);
}
} else if (!A.isSyntaxNode(rightValue)) {
if (compressor.in_boolean_context()) {
return ast.optimize(
makeSequence(self, [self.left, makeNode(A.TrueNode, self)]),
compressor
);
}
setFlag(self, TRUTHY);
}
if (self.left.operator === "&&") {
const leftRightValue = ast.evaluate(self.left.right, compressor);
if (leftRightValue && !A.isSyntaxNode(leftRightValue)) {
return ast.optimize(
makeNode(A.ConditionalNode, self, {
test: self.left.left,
consequent: self.left.right,
alternate: self.right
}),
compressor
);
}
}
break;
}
case "??": {
// `void x ?? y` runs `x`, then reads `y`, as `void` leaves undefined.
if (
modules.improvements !== undefined &&
modules.improvements.enabled &&
A.isUnaryPrefixNode(self.left) &&
self.left.operator === "void"
) {
return ast.optimize(
makeSequence(self, [self.left.argument, self.right]),
compressor
);
}
if (isNullish(self.left, compressor)) {
return self.right;
}
const leftValue = ast.evaluate(self.left, compressor);
if (!A.isSyntaxNode(leftValue)) {
return leftValue === null || leftValue === undefined
? self.right
: self.left;
}
if (compressor.in_boolean_context()) {
const rightValue = ast.evaluate(self.right, compressor);
if (!A.isSyntaxNode(rightValue) && !rightValue) {
return self.left;
}
}
}
}
let associative = true;
switch (self.operator) {
case "+":
// (x + "foo") + "bar" => x + "foobar"
if (
A.isConstantNode(self.right) &&
A.isBinaryNode(self.left) &&
self.left.operator === "+" &&
ast.isString(self.left, compressor) &&
joinsAsStrings(self.left.right, self.right, compressor)
) {
const binary = makeNode(A.BinaryNode, self, {
operator: "+",
left: self.left.right,
right: self.right
});
const optimized = ast.optimize(binary, compressor);
if (binary !== optimized) {
self = makeNode(A.BinaryNode, self, {
operator: "+",
left: self.left.left,
right: optimized
});
}
}
// (x + "foo") + ("bar" + y) => (x + "foobar") + y
if (
A.isBinaryNode(self.left) &&
self.left.operator === "+" &&
ast.isString(self.left, compressor) &&
A.isBinaryNode(self.right) &&
self.right.operator === "+" &&
ast.isString(self.right, compressor) &&
joinsAsStrings(self.left.right, self.right.left, compressor)
) {
const binary = makeNode(A.BinaryNode, self, {
operator: "+",
left: self.left.right,
right: self.right.left
});
const optimized = ast.optimize(binary, compressor);
if (binary !== optimized) {
self = makeNode(A.BinaryNode, self, {
operator: "+",
left: makeNode(A.BinaryNode, self.left, {
operator: "+",
left: self.left.left,
right: optimized
}),
right: self.right.right
});
}
}
// a + -b => a - b
if (
A.isUnaryPrefixNode(self.right) &&
self.right.operator === "-" &&
ast.isNumberOrBigInt(self.left, compressor)
) {
self = makeNode(A.BinaryNode, self, {
operator: "-",
left: self.left,
right: self.right.argument
});
break;
}
// -a + b => b - a
if (
A.isUnaryPrefixNode(self.left) &&
self.left.operator === "-" &&
reversible() &&
ast.isNumberOrBigInt(self.right, compressor)
) {
self = makeNode(A.BinaryNode, self, {
operator: "-",
left: self.right,
right: self.left.argument
});
break;
}
// `foo${bar}baz` + 1 => `foo${bar}baz1`
if (A.isTemplateStringNode(self.left)) {
const left = self.left;
const right = ast.evaluate(self.right, compressor);
// Loose, as terser: a node reads as its string form.
// eslint-disable-next-line eqeqeq
if (right != self.right) {
left.segments[left.segments.length - 1].value += String(right);
return left;
}
}
// 1 + `foo${bar}baz` => `1foo${bar}baz`
if (A.isTemplateStringNode(self.right)) {
const right = self.right;
const left = ast.evaluate(self.left, compressor);
// eslint-disable-next-line eqeqeq
if (left != self.left) {
right.segments[0].value = String(left) + right.segments[0].value;
return right;
}
}
// `1${bar}2` + `foo${bar}baz` => `1${bar}2foo${bar}baz`
if (
A.isTemplateStringNode(self.left) &&
A.isTemplateStringNode(self.right)
) {
const left = self.left;
const segments = left.segments;
const right = self.right;
/** @type {{ value: string }} */ (
segments[segments.length - 1]
).value += /** @type {string} */ (right.segments[0].value);
for (let i = 1; i < right.segments.length; i++) {
segments.push(right.segments[i]);
}
return left;
}
// falls through
case "*":
associative = compressor.option("unsafe_math");
// falls through
case "&":
case "|":
case "^":
// a + +b => +b + a
if (
ast.isNumberOrBigInt(self.left, compressor) &&
ast.isNumberOrBigInt(self.right, compressor) &&
reversible() &&
!(
A.isBinaryNode(self.left) &&
self.left.operator !== self.operator &&
PRECEDENCE[self.left.operator] >= PRECEDENCE[self.operator]
)
) {
const reversed = makeNode(A.BinaryNode, self, {
operator: self.operator,
left: self.right,
right: self.left
});
self =
A.isConstantNode(self.right) && !A.isConstantNode(self.left)
? bestOf(compressor, reversed, self)
: bestOf(compressor, self, reversed);
}
if (associative && ast.isNumberOrBigInt(self, compressor)) {
// a + (b + c) => (a + b) + c
if (
A.isBinaryNode(self.right) &&
self.right.operator === self.operator
) {
self = makeNode(A.BinaryNode, self, {
operator: self.operator,
left: makeNode(A.BinaryNode, self.left, {
operator: self.operator,
left: self.left,
right: self.right.left,
startToken: self.left.startToken,
endToken: self.right.left.endToken
}),
right: self.right.right
});
}
// (n + 2) + 3 => 5 + n, and (2 * n) * 3 => 6 * n
if (
A.isConstantNode(self.right) &&
A.isBinaryNode(self.left) &&
self.left.operator === self.operator
) {
if (A.isConstantNode(self.left.left)) {
self = makeNode(A.BinaryNode, self, {
operator: self.operator,
left: makeNode(A.BinaryNode, self.left, {
operator: self.operator,
left: self.left.left,
right: self.right,
startToken: self.left.left.startToken,
endToken: self.right.endToken
}),
right: self.left.right
});
} else if (A.isConstantNode(self.left.right)) {
self = makeNode(A.BinaryNode, self, {
operator: self.operator,
left: makeNode(A.BinaryNode, self.left, {
operator: self.operator,
left: self.left.right,
right: self.right,
startToken: self.left.right.startToken,
endToken: self.right.endToken
}),
right: self.left.left
});
}
}
// (a | 1) | (2 | d) => (3 | a) | d
if (
A.isBinaryNode(self.left) &&
self.left.operator === self.operator &&
A.isConstantNode(self.left.right) &&
A.isBinaryNode(self.right) &&
self.right.operator === self.operator &&
A.isConstantNode(self.right.left)
) {
self = makeNode(A.BinaryNode, self, {
operator: self.operator,
left: makeNode(A.BinaryNode, self.left, {
operator: self.operator,
left: makeNode(A.BinaryNode, self.left.left, {
operator: self.operator,
left: self.left.right,
right: self.right.left,
startToken: self.left.right.startToken,
endToken: self.right.left.endToken
}),
right: self.left.left
}),
right: self.right.right
});
}
}
}
if (bitwiseOperators.has(self.operator)) {
// De Morgan: z & (X | y) => z & X where y & z is 0, else z & X | (y & z)
// Each is read only once `typeof` has found it a number.
/** @type {number} */
let yValue;
/** @type {number} */
let zValue;
let xNode;
let yNode;
const zNode = self.left;
if (
self.operator === "&" &&
A.isBinaryNode(self.right) &&
self.right.operator === "|" &&
typeof (zValue = /** @type {number} */ (
ast.evaluate(self.left, compressor)
)) === "number"
) {
if (
typeof (yValue = /** @type {number} */ (
ast.evaluate(self.right.right, compressor)
)) === "number"
) {
// z & (X | y)
xNode = self.right.left;
yNode = self.right.right;
} else if (
typeof (yValue = /** @type {number} */ (
ast.evaluate(self.right.left, compressor)
)) === "number"
) {
// z & (y | X)
xNode = self.right.right;
yNode = self.right.left;
}
if (xNode && yNode) {
if ((yValue & zValue) === 0) {
self = makeNode(A.BinaryNode, self, {
operator: self.operator,
left: zNode,
right: xNode
});
} else {
const reorderedOperations = makeNode(A.BinaryNode, self, {
operator: "|",
left: makeNode(A.BinaryNode, self, {
operator: "&",
left: xNode,
right: zNode
}),
right: makeNodeFromConstant(yValue & zValue, yNode)
});
self = bestOf(compressor, self, reorderedOperations);
}
}
}
// x | x => 0 | x, and x & x => 0 | x
if (
(self.operator === "|" || self.operator === "&") &&
ast.isEquivalent(self.left, self.right) &&
!ast.hasSideEffects(self.left, compressor) &&
compressor.in_32_bit_context(true)
) {
self.left = makeNode(A.NumberNode, self, { value: 0 });
self.operator = "|";
}
// ~x ^ ~y => x ^ y
if (
self.operator === "^" &&
A.isUnaryPrefixNode(self.left) &&
self.left.operator === "~" &&
A.isUnaryPrefixNode(self.right) &&
self.right.operator === "~"
) {
self = makeNode(A.BinaryNode, self, {
operator: "^",
left: self.left.argument,
right: self.right.argument
});
}
// x << 0 => x | 0, and x >> 0 => x | 0
if (
(self.operator === "<<" || self.operator === ">>") &&
A.isNumberNode(self.right) &&
self.right.value === 0
) {
self.operator = "|";
}
// {32 bit integer} | 0 => {32 bit integer}, and ^ 0 likewise
const zeroSide =
A.isNumberNode(self.right) && self.right.value === 0
? self.right
: A.isNumberNode(self.left) && self.left.value === 0
? self.left
: null;
const nonZeroSide = /** @type {Node} */ (
zeroSide && (zeroSide === self.right ? self.left : self.right)
);
if (
zeroSide &&
(self.operator === "|" || self.operator === "^") &&
(ast.is32BitInteger(nonZeroSide, compressor) ||
compressor.in_32_bit_context(true))
) {
return nonZeroSide;
}
// {anything} & 0 => 0
if (
zeroSide &&
self.operator === "&" &&
!ast.hasSideEffects(nonZeroSide, compressor) &&
ast.is32BitInteger(nonZeroSide, compressor)
) {
return zeroSide;
}
/**
* @param {Node} node an operand
* @returns {boolean} whether it is -1, all bits set like ~0
*/
const isFullMask = (node) =>
(A.isNumberNode(node) && node.value === -1) ||
(A.isUnaryPrefixNode(node) &&
node.operator === "-" &&
A.isNumberNode(node.argument) &&
node.argument.value === 1);
const fullMask = isFullMask(self.right)
? self.right
: isFullMask(self.left)
? self.left
: null;
const otherSide = fullMask === self.right ? self.left : self.right;
// {32 bit integer} & -1 => {32 bit integer}
if (
fullMask &&
self.operator === "&" &&
(ast.is32BitInteger(otherSide, compressor) ||
compressor.in_32_bit_context(true))
) {
return otherSide;
}
// {anything} ^ -1 => ~{anything}
if (
fullMask &&
self.operator === "^" &&
(ast.is32BitInteger(otherSide, compressor) ||
compressor.in_32_bit_context(true))
) {
return ast.bitwiseNegate(otherSide, compressor);
}
}
}
// x && (y && z) => x && y && z, and x + ("y" + z) => x + "y" + z
if (
A.isBinaryNode(self.right) &&
self.right.operator === self.operator &&
(lazyOperators.has(self.operator) ||
(self.operator === "+" &&
(ast.isString(self.right.left, compressor) ||
(ast.isString(self.left, compressor) &&
ast.isString(self.right.right, compressor)))))
) {
self.left = makeNode(A.BinaryNode, self.left, {
operator: self.operator,
left: transformNode(self.left, compressor),
right: transformNode(self.right.left, compressor)
});
self.right = transformNode(self.right.right, compressor);
return transformNode(self, compressor);
}
let evaluated = ast.evaluate(self, compressor);
if (evaluated !== self) {
evaluated = ast.optimize(
makeNodeFromConstant(/** @type {EvaluatedValue} */ (evaluated), self),
compressor
);
return bestOf(compressor, evaluated, self);
}
return self;
});
};
// The ancestors, innermost first, of a destructuring an `export` declares.
/**
* Installs terser's optimizers of symbols, constants, assignments, conditionals,
* property reads, literals, functions, classes and destructuring.
* @param {TerserModules} modules terser's modules
* @param {OptimizerHelpers} helpers what the optimizers share
* @returns {void}
*/
const installValueOptimizers = (modules, helpers) => {
const { ast } = modules;
const { transformNode } = modules.ast;
const A = modules.ast;
const { walk, walk_abort: walkAbort, _NOINLINE } = A;
const {
make_void_0: makeVoid0,
makePredicate,
has_annotation: hasAnnotation
} = modules.utils;
const {
make_sequence: makeSequence,
best_of: bestOf,
best_of_expression: bestOfExpression,
make_empty_function: makeEmptyFunction,
make_node_from_constant: makeNodeFromConstant,
maintain_this_binding: maintainThisBinding,
is_reachable: isReachable
} = modules.common;
const {
is_undeclared_ref: isUndeclaredRef,
is_nullish: isNullish,
is_undefined: isUndefined
} = modules.inference;
const { UNUSED, clear_flag: clearFlag } = modules.flags;
const {
defineOptimizer,
findVariable,
optimizeLambda,
isAtomic,
unsafeUndefinedRef,
inlineArrayLikeSpread,
tightenBody,
inlineIntoSymbolRef
} = helpers;
const firstInStatement = createFirstInStatement(modules);
const assignOperators = makePredicate("+ - / * % >> << >>> | ^ &");
const commutativeAssignOperators = makePredicate("* | ^ &");
defineOptimizer("SymbolExport", (self) => self);
/**
* Whether a name reads a property of the global object, which may be an
* accessor: undeclared, or a script's top-level `var` or function.
* @param {Node} node an expression
* @param {Compressor} compressor the compressor
* @returns {boolean} true when it does
*/
const readsGlobalProperty = (node, compressor) => {
if (!A.isSymbolRefNode(node)) return false;
const { definition } = node;
return (
!definition ||
definition.undeclared ||
(definition.global &&
!compressor.option("module") &&
!A.isSymbolBlockDeclarationNode(definition.identifiers[0]))
);
};
defineOptimizer("SymbolRef", (self, compressor) => {
if (
!compressor.option("ie8") &&
isUndeclaredRef(self) &&
!compressor.find_parent(A.isWithNode) &&
!(
modules.corrections !== undefined &&
modules.corrections.enabled &&
isPatternTarget(self, compressor, A)
)
) {
switch (self.name) {
case "undefined":
return ast.optimize(makeNode(A.UndefinedNode, self), compressor);
case "NaN":
return ast.optimize(makeNode(A.NaNNode, self), compressor);
case "Infinity":
return ast.optimize(makeNode(A.InfinityNode, self), compressor);
}
}
if (compressor.option("reduce_vars") && !compressor.is_lhs()) {
return inlineIntoSymbolRef(self, compressor);
}
return self;
});
defineOptimizer("Undefined", (self, compressor) => {
const symbolRef = unsafeUndefinedRef(self, compressor);
if (symbolRef) return symbolRef;
const lhs = compressor.is_lhs();
if (lhs && isAtomic(lhs, self)) return self;
return makeVoid0(self);
});
defineOptimizer("Infinity", (self, compressor) => {
const lhs = compressor.is_lhs();
if (lhs && isAtomic(lhs, self)) return self;
if (
compressor.option("keep_infinity") &&
!(lhs && !isAtomic(lhs, self)) &&
!findVariable(compressor, "Infinity")
) {
return self;
}
return makeNode(A.BinaryNode, self, {
operator: "/",
left: makeNode(A.NumberNode, self, { value: 1 }),
right: makeNode(A.NumberNode, self, { value: 0 })
});
});
defineOptimizer("NaN", (self, compressor) => {
const lhs = compressor.is_lhs();
if ((lhs && !isAtomic(lhs, self)) || findVariable(compressor, "NaN")) {
return makeNode(A.BinaryNode, self, {
operator: "/",
left: makeNode(A.NumberNode, self, { value: 0 }),
right: makeNode(A.NumberNode, self, { value: 0 })
});
}
return self;
});
/**
* terser's `in_try`: whether a `try` between an exit and the assigned
* variable's function could still observe the assignment.
* @param {Node} self the assignment
* @param {Compressor} compressor the compressor
* @param {number} level how far up the exit's parent is
* @param {Node} exit the `return` or `throw` the assignment is in
* @returns {true | undefined} true where one could
*/
const inTry = (self, compressor, level, exit) => {
/**
* @returns {boolean} whether the exit may throw with the value assigned read as `null`
*/
const mayAssignmentThrow = () => {
const right = self.right;
self.right = makeNode(A.NullNode, right);
const mayThrow = ast.mayThrow(exit, compressor);
self.right = right;
return mayThrow;
};
const stopAt = self.left.definition.scope.getDefunScope().node;
let parent;
while ((parent = compressor.parent(level++)) !== stopAt) {
if (A.isTryNode(parent)) {
if (parent.finalizer) return true;
if (parent.handler && mayAssignmentThrow()) return true;
}
}
return undefined;
};
defineOptimizer("Assign", (self, compressor) => {
if (self.logical) {
return ast.liftBinarySequences(self, compressor);
}
// x = x ---> x
if (
self.operator === "=" &&
A.isSymbolRefNode(self.left) &&
self.left.name !== "arguments" &&
!self.left.definition.undeclared &&
ast.isEquivalent(self.right, self.left)
) {
return self.right;
}
let definition;
if (
compressor.option("dead_code") &&
A.isSymbolRefNode(self.left) &&
(definition = self.left.definition).scope.node ===
compressor.find_parent(A.isLambdaNode)
) {
let level = 0;
let node;
let parent = self;
do {
node = parent;
parent = compressor.parent(level++);
if (A.isExitNode(parent)) {
if (inTry(self, compressor, level, parent)) break;
if (isReachable(definition.scope.node, [definition])) break;
if (self.operator === "=") return self.right;
definition.fixed = false;
return ast.optimize(
makeNode(A.BinaryNode, self, {
operator: self.operator.slice(0, -1),
left: self.left,
right: self.right
}),
compressor
);
}
} while (
(A.isBinaryNode(parent) && parent.right === node) ||
(A.isSequenceNode(parent) && ast.tailNode(parent) === node)
);
}
const assign = ast.liftBinarySequences(self, compressor);
if (
assign.operator === "=" &&
A.isSymbolRefNode(assign.left) &&
A.isBinaryNode(assign.right)
) {
if (
A.isSymbolRefNode(assign.right.left) &&
assign.right.left.name === assign.left.name &&
assignOperators.has(assign.right.operator)
) {
// x = x - 2 ---> x -= 2
assign.operator = `${assign.right.operator}=`;
assign.right = assign.right.right;
} else if (
A.isSymbolRefNode(assign.right.right) &&
assign.right.right.name === assign.left.name &&
commutativeAssignOperators.has(assign.right.operator) &&
!ast.hasSideEffects(assign.right.left, compressor)
) {
// x = 2 & x ---> x &= 2
assign.operator = `${assign.right.operator}=`;
assign.right = assign.right.left;
}
}
return assign;
});
defineOptimizer("DefaultAssign", (self, compressor) => {
if (!compressor.option("evaluate")) {
return self;
}
let evaluateRight = ast.evaluate(self.right, compressor);
if (evaluateRight === undefined) {
// `[x = undefined] = foo` ---> `[x] = foo`, and a parameter the same
// way unless `keep_fargs`, or where its function is called at once.
const lambda = compressor.parent();
if (!A.isLambdaNode(lambda)) return self.left;
if (compressor.option("keep_fargs") === false) return self.left;
const iife = compressor.parent(1);
if (A.isCallExpressionNode(iife) && iife.callee === lambda) {
return self.left;
}
} else if (evaluateRight !== self.right) {
evaluateRight = makeNodeFromConstant(
/** @type {EvaluatedValue} */ (evaluateRight),
self.right
);
self.right = bestOfExpression(evaluateRight, self.right);
}
return self;
});
/**
* terser's `is_nullish_check`: whether a condition tests `subject == null`,
* or `subject === null || subject === undefined` either way round.
* @param {Node} check the condition
* @param {Node} subject what it should test
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it does
*/
const isNullishCheck = (check, subject, compressor) => {
if (ast.mayThrow(subject, compressor)) return false;
let nullishSide;
// foo == null
if (
A.isBinaryNode(check) &&
check.operator === "==" &&
((nullishSide = isNullish(check.left, compressor) && check.left) ||
(nullishSide = isNullish(check.right, compressor) && check.right)) &&
ast.isEquivalent(
nullishSide === check.left ? check.right : check.left,
subject
)
) {
return true;
}
// foo === null || foo === undefined
if (A.isBinaryNode(check) && check.operator === "||") {
/** @type {Node | undefined} */
let nullComparison;
/** @type {Node | undefined} */
let undefinedComparison;
/**
* @param {Node} comparison one side of the `||`
* @returns {boolean} whether it compares the subject with one nullish value
*/
const findComparison = (comparison) => {
if (!(
A.isBinaryNode(comparison) &&
(comparison.operator === "===" || comparison.operator === "==")
)) {
return false;
}
let found = 0;
let definedSide;
if (A.isNullNode(comparison.left)) {
found++;
nullComparison = comparison;
definedSide = comparison.right;
}
if (A.isNullNode(comparison.right)) {
found++;
nullComparison = comparison;
definedSide = comparison.left;
}
if (isUndefined(comparison.left, compressor)) {
found++;
undefinedComparison = comparison;
definedSide = comparison.right;
}
if (isUndefined(comparison.right, compressor)) {
found++;
undefinedComparison = comparison;
definedSide = comparison.left;
}
if (found !== 1) {
return false;
}
return Boolean(
ast.isEquivalent(/** @type {Node} */ (definedSide), subject)
);
};
if (!findComparison(check.left)) return false;
if (!findComparison(check.right)) return false;
if (
nullComparison &&
undefinedComparison &&
nullComparison !== undefinedComparison
) {
return true;
}
}
return false;
};
/**
* terser's `single_arg_diff`: the one argument two calls differ in.
* @param {Node} consequent a call
* @param {Node} alternative a call to the same callee with as many arguments
* @returns {number | undefined} its index, or undefined unless exactly one differs
*/
const singleArgumentDifference = (consequent, alternative) => {
const first = consequent.arguments;
const second = alternative.arguments;
for (let i = 0, length = first.length; i < length; i++) {
if (A.isExpansionNode(first[i])) return undefined;
if (!ast.isEquivalent(first[i], second[i])) {
if (A.isExpansionNode(second[i])) return undefined;
for (let j = i + 1; j < length; j++) {
if (A.isExpansionNode(first[j])) return undefined;
if (!ast.isEquivalent(first[j], second[j])) return undefined;
}
return i;
}
}
return undefined;
};
defineOptimizer("Conditional", (self, compressor) => {
if (!compressor.option("conditionals")) return self;
// This looks like lift_sequences(), should probably be under "sequences"
if (A.isSequenceNode(self.test)) {
const expressions = [...self.test.expressions];
self.test = /** @type {Node} */ (expressions.pop());
expressions.push(self);
return makeSequence(self, expressions);
}
const evaluated = ast.evaluate(self.test, compressor);
if (evaluated !== self.test) {
if (evaluated) {
return maintainThisBinding(
compressor.parent(),
compressor.self(),
/** @type {Node} */ (self.consequent)
);
}
return maintainThisBinding(
compressor.parent(),
compressor.self(),
self.alternate
);
}
const negated = ast.negate(
evaluated,
compressor,
firstInStatement(
/** @type {Parameters<typeof firstInStatement>[0]} */ (compressor)
)
);
if (bestOf(compressor, evaluated, negated) === negated) {
self = makeNode(A.ConditionalNode, self, {
test: negated,
consequent: self.alternate,
alternate: /** @type {Node} */ (self.consequent)
});
}
const condition = self.test;
const consequent = /** @type {Node} */ (self.consequent);
const alternative = self.alternate;
// x?x:y --> x||y
if (
A.isSymbolRefNode(condition) &&
A.isSymbolRefNode(consequent) &&
condition.definition === consequent.definition
) {
return makeNode(A.BinaryNode, self, {
operator: "||",
left: condition,
right: alternative
});
}
// if (foo) exp = something; else exp = something_else;
// ---> exp = foo ? something : something_else;
if (
A.isAssignNode(consequent) &&
A.isAssignNode(alternative) &&
consequent.operator === alternative.operator &&
consequent.logical === alternative.logical &&
ast.isEquivalent(consequent.left, alternative.left) &&
((!ast.hasSideEffects(self.test, compressor) &&
// A compound assignment then reads its target before the test, which a
// global's accessor may run code for.
(consequent.operator === "=" ||
modules.corrections === undefined ||
!modules.corrections.enabled ||
!readsGlobalProperty(consequent.left, compressor))) ||
(consequent.operator === "=" &&
!ast.hasSideEffects(consequent.left, compressor)))
) {
return makeNode(A.AssignNode, self, {
operator: consequent.operator,
left: consequent.left,
logical: consequent.logical,
right: makeNode(A.ConditionalNode, self, {
test: self.test,
consequent: consequent.right,
alternate: alternative.right
})
});
}
// x ? y(a) : y(b) --> y(x ? a : b)
let argumentIndex;
if (
A.isCallNode(consequent) &&
ast.kindOf(alternative) === ast.kindOf(consequent) &&
consequent.arguments.length > 0 &&
consequent.arguments.length === alternative.arguments.length &&
ast.isEquivalent(consequent.callee, alternative.callee) &&
!ast.hasSideEffects(self.test, compressor) &&
!ast.hasSideEffects(consequent.callee, compressor) &&
typeof (argumentIndex = singleArgumentDifference(
consequent,
alternative
)) === "number"
) {
const node = ast.cloneNode(consequent);
node.arguments[argumentIndex] = makeNode(A.ConditionalNode, self, {
test: self.test,
consequent: consequent.arguments[argumentIndex],
alternate: alternative.arguments[argumentIndex]
});
return node;
}
// a ? b : c ? b : d --> (a || c) ? b : d
if (
A.isConditionalNode(alternative) &&
ast.isEquivalent(consequent, /** @type {Node} */ (alternative.consequent))
) {
return ast.optimize(
makeNode(A.ConditionalNode, self, {
test: makeNode(A.BinaryNode, self, {
operator: "||",
left: condition,
right: alternative.test
}),
consequent,
alternate: alternative.alternate
}),
compressor
);
}
// a == null ? b : a -> a ?? b
if (
compressor.option("ecma") >= 2020 &&
isNullishCheck(condition, alternative, compressor)
) {
return ast.optimize(
makeNode(A.BinaryNode, self, {
operator: "??",
left: alternative,
right: consequent
}),
compressor
);
}
// a ? b : (c, b) --> (a || c), b
if (
A.isSequenceNode(alternative) &&
ast.isEquivalent(
consequent,
alternative.expressions[alternative.expressions.length - 1]
)
) {
return ast.optimize(
makeSequence(self, [
makeNode(A.BinaryNode, self, {
operator: "||",
left: condition,
right: makeSequence(self, alternative.expressions.slice(0, -1))
}),
consequent
]),
compressor
);
}
// a ? b : (c && b) --> (a || c) && b
if (
A.isBinaryNode(alternative) &&
alternative.operator === "&&" &&
ast.isEquivalent(consequent, alternative.right)
) {
return ast.optimize(
makeNode(A.BinaryNode, self, {
operator: "&&",
left: makeNode(A.BinaryNode, self, {
operator: "||",
left: condition,
right: alternative.left
}),
right: consequent
}),
compressor
);
}
// x?y?z:a:a --> x&&y?z:a
if (
A.isConditionalNode(consequent) &&
ast.isEquivalent(consequent.alternate, alternative)
) {
return makeNode(A.ConditionalNode, self, {
test: makeNode(A.BinaryNode, self, {
left: self.test,
operator: "&&",
right: consequent.test
}),
consequent: consequent.consequent,
alternate: alternative
});
}
// x ? y : y --> x, y
if (ast.isEquivalent(consequent, alternative)) {
return ast.optimize(
makeSequence(self, [self.test, consequent]),
compressor
);
}
// x ? y || z : z --> x && y || z
if (
A.isBinaryNode(consequent) &&
consequent.operator === "||" &&
ast.isEquivalent(consequent.right, alternative)
) {
return ast.optimize(
makeNode(A.BinaryNode, self, {
operator: "||",
left: makeNode(A.BinaryNode, self, {
operator: "&&",
left: self.test,
right: consequent.left
}),
right: alternative
}),
compressor
);
}
const inBoolean = compressor.in_boolean_context();
/**
* @param {Node} node an expression
* @returns {Node} it, or `!!` it where it may not be a boolean
*/
const toBooleanValue = (node) => {
if (ast.isBoolean(node)) return node;
return makeNode(A.UnaryPrefixNode, node, {
operator: "!",
argument: ast.negate(node, compressor)
});
};
/**
* @param {Node} node an expression
* @returns {ReturnType<AstHelpers["constantValue"]> | undefined} truthy where it is `true` or `!0`, or truthy in a boolean context
*/
const isTrue = (node) =>
A.isTrueNode(node) ||
(inBoolean && A.isConstantNode(node) && ast.constantValue(node)) ||
(A.isUnaryPrefixNode(node) &&
node.operator === "!" &&
A.isConstantNode(node.argument) &&
!ast.constantValue(node.argument));
/**
* @param {Node} node an expression
* @returns {ReturnType<AstHelpers["constantValue"]> | undefined} truthy where it is `false` or `!1`, or falsy in a boolean context
*/
const isFalse = (node) =>
A.isFalseNode(node) ||
(inBoolean && A.isConstantNode(node) && !ast.constantValue(node)) ||
(A.isUnaryPrefixNode(node) &&
node.operator === "!" &&
A.isConstantNode(node.argument) &&
ast.constantValue(node.argument));
if (isTrue(/** @type {Node} */ (self.consequent))) {
if (isFalse(self.alternate)) {
// c ? true : false ---> !!c
return toBooleanValue(self.test);
}
// c ? true : x ---> !!c || x
return makeNode(A.BinaryNode, self, {
operator: "||",
left: toBooleanValue(self.test),
right: self.alternate
});
}
if (isFalse(/** @type {Node} */ (self.consequent))) {
if (isTrue(self.alternate)) {
// c ? false : true ---> !c
return toBooleanValue(ast.negate(self.test, compressor));
}
// c ? false : x ---> !c && x
return makeNode(A.BinaryNode, self, {
operator: "&&",
left: toBooleanValue(ast.negate(self.test, compressor)),
right: self.alternate
});
}
if (isTrue(self.alternate)) {
// c ? x : true ---> !c || x
return makeNode(A.BinaryNode, self, {
operator: "||",
left: toBooleanValue(ast.negate(self.test, compressor)),
right: /** @type {Node} */ (self.consequent)
});
}
if (isFalse(self.alternate)) {
// c ? x : false ---> !!c && x
return makeNode(A.BinaryNode, self, {
operator: "&&",
left: toBooleanValue(self.test),
right: /** @type {Node} */ (self.consequent)
});
}
return self;
});
defineOptimizer("Boolean", (self, compressor) => {
if (compressor.in_boolean_context()) {
return makeNode(A.NumberNode, self, { value: Number(self.value) });
}
const parent = compressor.parent();
if (compressor.option("booleans_as_integers")) {
if (
A.isBinaryNode(parent) &&
(parent.operator === "===" || parent.operator === "!==")
) {
parent.operator = parent.operator.replace(/[=]$/, "");
}
return makeNode(A.NumberNode, self, { value: Number(self.value) });
}
if (compressor.option("booleans")) {
if (
A.isBinaryNode(parent) &&
(parent.operator === "==" || parent.operator === "!=")
) {
return makeNode(A.NumberNode, self, { value: Number(self.value) });
}
return makeNode(A.UnaryPrefixNode, self, {
operator: "!",
argument: makeNode(A.NumberNode, self, {
value: 1 - /** @type {number} */ (self.value)
})
});
}
return self;
});
/**
* terser's `safe_to_flatten`: whether a value can be read out of the literal
* holding it without changing what `this` it sees.
* @param {Node} value the value, or a reference to it; undefined past the end
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it can
*/
const safeToFlatten = (value, compressor) => {
const fixed = A.isSymbolRefNode(value) ? ast.fixedValue(value) : value;
if (!fixed) return false;
if (!(A.isLambdaNode(fixed) || A.isClassNode(fixed))) {
return true;
}
if (!(A.isLambdaNode(fixed) && ast.containsThis(fixed))) return true;
// An arrow's `this` is the one around it, not the literal's.
if (
A.isArrowNode(fixed) &&
modules.improvements !== undefined &&
modules.improvements.enabled
) {
return true;
}
return A.isNewNode(compressor.parent());
};
/**
* terser's `flatten_object`: `{a: x, b: y}.b` as `[x, y][1]`.
* @param {Node} self a property read
* @param {string} key the property read
* @param {Compressor} compressor the compressor
* @returns {Node | undefined} the array read, where one fits
*/
ast.flattenObject = function flattenObject(self, key, compressor) {
if (!compressor.option("properties")) return undefined;
if (key === "__proto__") return undefined;
if (A.isDotHashNode(self)) return undefined;
const arrows =
compressor.option("unsafe_arrows") && compressor.option("ecma") >= 2015;
const expression = self.object;
if (A.isObjectNode(expression)) {
const properties = expression.properties;
for (let i = properties.length; --i >= 0;) {
const property = properties[i];
if (
String(
A.isConciseMethodNode(property)
? property.key.name
: A.terserKey(property.key)
) === key
) {
const everyPropertyFlattens = properties.every(
(/** @type {Node} */ item) =>
(A.isObjectKeyValNode(item) ||
(arrows &&
A.isConciseMethodNode(item) &&
!(/** @type {Node} */ (item.value).generator))) &&
!item.computed
);
if (!everyPropertyFlattens) return undefined;
if (
!safeToFlatten(/** @type {Node} */ (property.value), compressor)
) {
return undefined;
}
return makeNode(A.SubNode, self, {
object: makeNode(A.ArrayNode, expression, {
elements: properties.map((/** @type {Node} */ item) => {
let value = /** @type {Node} */ (item.value);
if (A.isAccessorNode(value)) {
value = makeNode(A.FunctionNode, value, value);
}
const itemKey = A.terserKey(item.key);
if (A.isSyntaxNode(itemKey) && !A.isSymbolMethodNode(itemKey)) {
return makeSequence(item, [itemKey, value]);
}
return value;
})
}),
property: makeNode(A.NumberNode, self, { value: i }),
computed: true
});
}
}
}
return undefined;
};
/**
* terser's `arguments[n]` rewrite: the parameter an `arguments` read names,
* adding parameters up to it where none bind it yet.
* @param {Node} self the computed property read
* @param {Compressor} compressor the compressor
* @param {Node} expression what it reads from
* @param {Node} property the property it reads
* @returns {Node | undefined} a reference to the parameter, where one fits
*/
const argumentsParameter = (self, compressor, expression, property) => {
let lambda;
if (!(
compressor.option("arguments") &&
A.isSymbolRefNode(expression) &&
expression.name === "arguments" &&
expression.definition.identifiers.length === 1 &&
A.isLambdaNode((lambda = expression.scope.node)) &&
expression.scope.usesArguments &&
!A.isArrowNode(lambda) &&
A.isNumberNode(property)
)) {
return undefined;
}
// An inlined copy still names the original's scope, which then gets the parameter.
if (
modules.corrections !== undefined &&
modules.corrections.enabled &&
compressor.find_parent(
(node) => A.isLambdaNode(node) && !A.isArrowNode(node)
) !== lambda
) {
return undefined;
}
const lambdaScope = expression.scope;
const index = /** @type {number} */ (ast.constantValue(property));
const names = new Set();
const parameters = lambda.params;
for (let i = 0; i < parameters.length; i++) {
// A destructuring or a repeated parameter leaves `arguments` as it is.
if (!A.isSymbolFunargNode(parameters[i])) return undefined;
const name = parameters[i].name;
if (names.has(name)) return undefined;
names.add(name);
}
/** @type {Node | null} */ let parameter = lambda.params[index];
if (parameter && compressor.has_directive("use strict")) {
const definition = parameter.definition;
if (
!compressor.option("reduce_vars") ||
definition.assignments ||
definition.identifiers.length > 1
) {
parameter = null;
}
} else if (
!parameter &&
!compressor.option("keep_fargs") &&
index < lambda.params.length + 5
) {
while (index >= lambda.params.length) {
parameter = ast.createSymbol(lambdaScope, A.SymbolFunargNode, {
source: lambda,
scope: lambdaScope,
tentative_name: `argument_${lambda.params.length}`
});
lambda.params.push(parameter);
}
}
if (parameter) {
const symbol = makeNode(A.SymbolRefNode, self, parameter);
symbol.scope.addReference(symbol);
clearFlag(parameter, UNUSED);
return symbol;
}
return undefined;
};
/**
* terser's `[a, b, c][1]` rewrite: the element read, after what the others
* do, or a shorter array where the ones after it keep side effects.
* @param {Node} self the computed property read
* @param {Compressor} compressor the compressor
* @param {Node} expression the array it reads from
* @param {Node} property the number it reads
* @returns {Node | undefined} the replacement, where one fits
*/
const flattenArrayRead = (self, compressor, expression, property) => {
let index = /** @type {number} */ (ast.constantValue(property));
const elements = expression.elements;
/** @type {Node | null} */
const returnValue = elements[index];
// A hole, null here, flattens to `void 0` as terser's hole node did.
if (returnValue !== null && !safeToFlatten(returnValue, compressor)) {
return undefined;
}
let flatten = true;
/** @type {Node[]} */
const values = [];
// Shared by both loops: the second resumes where the first stops.
let i = elements.length;
while (--i > index) {
const element = elements[i];
const value =
element === null ? null : ast.dropSideEffectFree(element, compressor);
if (value) {
values.unshift(value);
if (flatten && ast.hasSideEffects(value, compressor)) flatten = false;
}
}
if (A.isExpansionNode(returnValue)) return undefined;
const returned = returnValue === null ? makeVoid0(undefined) : returnValue;
if (!flatten) values.unshift(returned);
while (--i >= 0) {
/** @type {Node | null} */ let value = elements[i];
if (A.isExpansionNode(value)) return undefined;
value = value === null ? null : ast.dropSideEffectFree(value, compressor);
if (value) values.unshift(value);
else index--;
}
if (flatten) {
values.push(returned);
return ast.optimize(makeSequence(self, values), compressor);
}
return makeNode(A.SubNode, self, {
object: makeNode(A.ArrayNode, expression, { elements: values }),
property: makeNode(A.NumberNode, property, { value: index }),
computed: true
});
};
defineOptimizer("Sub", (self, compressor) => {
let expression = self.object;
let property = self.property;
let key;
let name;
if (compressor.option("properties")) {
key = ast.evaluate(property, compressor);
if (key !== property) {
if (typeof key === "string") {
if (key === "undefined") {
key = undefined;
} else {
const value = Number.parseFloat(key);
if (value.toString() === key) {
key = value;
}
}
}
property = self.property = bestOfExpression(
property,
transformNode(
makeNodeFromConstant(/** @type {EvaluatedValue} */ (key), property),
compressor
)
);
name = String(key);
if (
BASIC_IDENTIFIER.test(name) &&
name.length <= ast.nodeSize(property) + 1
) {
return ast.optimize(
makeNode(A.DotNode, self, {
object: expression,
optional: self.optional,
property: A.SymbolPropertyNode({ name }),
quote: property.quote,
computed: false
}),
compressor
);
}
}
}
const parameter = argumentsParameter(
self,
compressor,
expression,
property
);
if (parameter) return parameter;
if (
compressor.is_lhs() ||
// `[[1][0]] = a` writes the access, which `[1] = a` cannot.
(modules.corrections !== undefined &&
modules.corrections.enabled &&
isPatternTarget(self, compressor, A))
) {
return self;
}
// Without `properties` the key is undefined, never the property node.
if (key !== property) {
const sub = ast.flattenObject(
self,
/** @type {string} */ (name),
compressor
);
if (sub) {
expression = self.object = sub.object;
property = self.property = sub.property;
}
}
if (
compressor.option("properties") &&
compressor.option("side_effects") &&
A.isNumberNode(property) &&
A.isArrayNode(expression)
) {
const flattened = flattenArrayRead(
self,
compressor,
expression,
property
);
if (flattened) return flattened;
}
let evaluated = ast.evaluate(self, compressor);
if (evaluated !== self) {
evaluated = ast.optimize(
makeNodeFromConstant(/** @type {EvaluatedValue} */ (evaluated), self),
compressor
);
return bestOf(compressor, evaluated, self);
}
return self;
});
defineOptimizer("Chain", (self, compressor) => {
if (isNullish(self.expression, compressor)) {
const parent = compressor.parent();
// `delete undefined` would read as a syntax error once the delete is
// optimized, where `delete 0` is fine.
if (A.isUnaryPrefixNode(parent) && parent.operator === "delete") {
return makeNodeFromConstant(0, self);
}
return ast.optimize(makeVoid0(self), compressor);
}
if (A.isPropAccessNode(self.expression) || A.isCallNode(self.expression)) {
return self;
}
// Keep the AST valid, in case the child swapped itself.
return self.expression;
});
defineOptimizer("Dot", (self, compressor) => {
const parent = compressor.parent();
if (
compressor.is_lhs() ||
// A pattern writes the access, which a folded value cannot be.
(modules.corrections !== undefined &&
modules.corrections.enabled &&
isPatternTarget(self, compressor, A))
) {
return self;
}
if (
compressor.option("unsafe_proto") &&
A.isDotNode(self.object) &&
self.object.property.name === "prototype"
) {
const expression = self.object.object;
if (isUndeclaredRef(expression)) {
switch (expression.name) {
case "Array":
self.object = makeNode(A.ArrayNode, self.object, {
elements: []
});
break;
case "Function":
self.object = makeEmptyFunction(self.object);
break;
case "Number":
self.object = makeNode(A.NumberNode, self.object, {
value: 0
});
break;
case "Object":
self.object = makeNode(A.ObjectNode, self.object, {
properties: []
});
break;
case "RegExp":
self.object = makeNode(A.RegExpNode, self.object, {
value: { source: "t", flags: "" }
});
break;
case "String":
self.object = makeNode(A.StringNode, self.object, {
value: ""
});
break;
}
}
}
if (!A.isCallNode(parent) || !hasAnnotation(parent, _NOINLINE)) {
const sub = ast.flattenObject(self, self.property.name, compressor);
if (sub) return ast.optimize(sub, compressor);
}
if (A.isPropAccessNode(self.object) && A.isPropAccessNode(parent)) {
return self;
}
let evaluated = ast.evaluate(self, compressor);
if (evaluated !== self) {
evaluated = ast.optimize(
makeNodeFromConstant(/** @type {EvaluatedValue} */ (evaluated), self),
compressor
);
return bestOf(compressor, evaluated, self);
}
return self;
});
/**
* terser's `literals_in_boolean_context`: a literal that is always truthy,
* as the shorter of itself and its side effects followed by `true`.
* @param {Node} self an array, object or regular expression literal
* @param {Compressor} compressor the compressor
* @returns {Node} what replaces it
*/
const literalsInBooleanContext = (self, compressor) => {
if (compressor.in_boolean_context()) {
return bestOf(
compressor,
self,
ast.optimize(
makeSequence(self, [self, makeNode(A.TrueNode, self)]),
compressor
)
);
}
return self;
};
defineOptimizer("Array", (self, compressor) => {
const optimized = literalsInBooleanContext(self, compressor);
if (optimized !== self) {
return optimized;
}
inlineArrayLikeSpread(self.elements);
return self;
});
/**
* terser's `inline_object_prop_spread`: spreads in place each object literal
* of plain properties, and drops each spread constant but a string.
* @param {Node[]} properties an object literal's properties
* @returns {void}
*/
const inlineObjectPropertySpread = (properties) => {
for (let i = 0; i < properties.length; i++) {
const property = properties[i];
if (A.isExpansionNode(property)) {
const expression = property.argument;
if (
A.isObjectNode(expression) &&
expression.properties.every((/** @type {Node} */ item) =>
A.isObjectKeyValNode(item)
)
) {
properties.splice(i, 1, ...expression.properties);
// The property at `i` is a new one.
i--;
} else if (
// `is_constant()` is false for a regular expression, hence both.
(A.isConstantNode(expression) || ast.isConstant(expression)) &&
!A.isStringNode(expression)
) {
properties.splice(i, 1);
i--;
}
}
}
};
defineOptimizer("Object", (self, compressor) => {
const optimized = literalsInBooleanContext(self, compressor);
if (optimized !== self) {
return optimized;
}
inlineObjectPropertySpread(self.properties);
return self;
});
defineOptimizer("RegExp", literalsInBooleanContext);
defineOptimizer("Return", (self, compressor) => {
if (self.argument && isUndefined(self.argument, compressor)) {
self.argument = NO_NODE;
}
return self;
});
defineOptimizer("Arrow", optimizeLambda);
defineOptimizer("Function", (self, compressor) => {
const lambda = optimizeLambda(self, compressor);
if (
compressor.option("unsafe_arrows") &&
compressor.option("ecma") >= 2015 &&
!lambda.id &&
!lambda.generator &&
!ast.usesArguments(lambda) &&
!ast.isPinned(lambda)
) {
const usesThis = walk(lambda, (/** @type {Node} */ node) => {
if (A.isThisNode(node)) return walkAbort;
return undefined;
});
if (!usesThis) {
return ast.optimize(makeNode(A.ArrowNode, lambda, lambda), compressor);
}
}
return lambda;
});
defineOptimizer("Class", (self) => {
for (
let i = 0;
i < /** @type {Node[]} */ (/** @type {Node} */ (self.body).body).length;
i++
) {
const property = /** @type {Node[]} */ (
/** @type {Node} */ (self.body).body
)[i];
if (
A.isClassStaticBlockNode(property) &&
/** @type {Node[]} */ (property.body).length === 0
) {
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body).splice(
i,
1
);
i--;
}
}
return self;
});
defineOptimizer("ClassStaticBlock", (self, compressor) => {
tightenBody(/** @type {Node[]} */ (self.body), compressor);
return self;
});
defineOptimizer("Yield", (self, compressor) => {
if (
self.argument &&
!self.delegate &&
isUndefined(self.argument, compressor)
) {
self.argument = NO_NODE;
}
return self;
});
/**
* The string an array literal of primitives converts to, once improved.
* @param {Node} array an array literal
* @param {Compressor} compressor the compressor
* @returns {string | Node} the string, or the array where an element is a hole or no primitive
*/
const joinedLiterals = (array, compressor) => {
const { arrayPrototypePatched } = modules.webpackInline;
if (
modules.improvements === undefined ||
!modules.improvements.enabled ||
(arrayPrototypePatched && arrayPrototypePatched())
) {
return array;
}
/** @type {string[]} */
const parts = [];
for (const element of array.elements) {
if (element === null) {
// A hole can read an inherited value or invoke an inherited getter.
return array;
}
const value = ast.evaluate(element, compressor);
if (
A.isSyntaxNode(value) ||
(value !== null && typeof value === "object") ||
typeof value === "function"
) {
return array;
}
parts.push(value === null || value === undefined ? "" : String(value));
}
return parts.join(",");
};
defineOptimizer("TemplateString", (self, compressor) => {
if (
!compressor.option("evaluate") ||
A.isPrefixedTemplateStringNode(compressor.parent())
) {
return self;
}
/** @type {Node[]} */
const segments = [];
for (let i = 0; i < self.segments.length; i++) {
let segment = self.segments[i];
if (A.isSyntaxNode(segment)) {
const result = A.isArrayNode(segment)
? joinedLiterals(segment, compressor)
: ast.evaluate(segment, compressor);
// A constant no longer than `${segment}`, the 3 being `${}`; a node
// always has a segment before and after it.
if (
result !== segment &&
String(result).length <= ast.nodeSize(segment) + 3
) {
segments[segments.length - 1].value =
/** @type {string} */ (segments[segments.length - 1].value) +
/** @type {string} */ (result) +
self.segments[++i].value;
continue;
}
// `before ${`inner ${any} after`} after` => `before inner ${any} after after`
if (A.isTemplateStringNode(segment)) {
const inners = segment.segments;
/** @type {{ value: string }} */ (
segments[segments.length - 1]
).value += /** @type {string} */ (inners[0].value);
for (let j = 1; j < inners.length; j++) {
segment = inners[j];
segments.push(segment);
}
continue;
}
}
segments.push(segment);
}
self.segments = segments;
// `foo` => "foo"
if (segments.length === 1) {
return makeNode(A.StringNode, self, segments[0]);
}
if (
segments.length === 3 &&
A.isSyntaxNode(segments[1]) &&
(ast.isString(segments[1], compressor) ||
ast.isNumberOrBigInt(segments[1], compressor) ||
isNullish(segments[1], compressor) ||
compressor.option("unsafe"))
) {
// `foo${bar}` => "foo" + bar
if (segments[2].value === "") {
return makeNode(A.BinaryNode, self, {
operator: "+",
left: makeNode(A.StringNode, self, { value: segments[0].value }),
right: segments[1]
});
}
// `${bar}baz` => bar + "baz"
if (segments[0].value === "") {
return makeNode(A.BinaryNode, self, {
operator: "+",
left: segments[1],
right: makeNode(A.StringNode, self, { value: segments[2].value })
});
}
}
return self;
});
defineOptimizer("PrefixedTemplateString", (self) => self);
/**
* terser's `lift_key`: `["p"]: 1` as `p: 1` and `[42]: 1` as `42: 1`.
* @param {Node} self an object or class property
* @param {Compressor} compressor the compressor
* @returns {Node} the property
*/
const liftKey = (self, compressor) => {
if (!compressor.option("computed_props")) return self;
// Not every constant is a key, so this only saves the next tests.
if (!A.isConstantNode(self.key)) return self;
if (A.isStringNode(self.key) || A.isNumberNode(self.key)) {
const key = /** @type {string | number} */ (self.key.value).toString();
if (key === "__proto__") return self;
if (key === "constructor" && A.isClassNode(compressor.parent())) {
return self;
}
self.computed = false;
if (A.isObjectKeyValNode(self)) {
self.quote = self.key.quote;
self.key = A.SymbolPropertyNode({ name: key });
} else if (A.isClassPropertyNode(self)) {
self.quote = self.key.quote;
self.key = makeNode(A.SymbolClassPropertyNode, self.key, {
name: key
});
} else {
self.quote = self.key.quote;
self.key = makeNode(A.SymbolMethodNode, self.key, { name: key });
}
}
return self;
};
defineOptimizer("ObjectProperty", liftKey);
defineOptimizer("ConciseMethod", (self, compressor) => {
liftKey(self, compressor);
// p(){return x;} ---> p:()=>x
if (
compressor.option("arrows") &&
A.isObjectNode(compressor.parent()) &&
!(/** @type {Node} */ (self.value).generator) &&
!ast.usesArguments(/** @type {Node} */ (self.value)) &&
!ast.isPinned(/** @type {Node} */ (self.value)) &&
/** @type {Node[]} */ (
/** @type {Node} */ (/** @type {Node} */ (self.value).body).body
).length === 1 &&
A.isReturnNode(
/** @type {Node[]} */ (
/** @type {Node} */ (/** @type {Node} */ (self.value).body).body
)[0]
) &&
/** @type {Node[]} */ (
/** @type {Node} */ (/** @type {Node} */ (self.value).body).body
)[0].argument &&
!ast.containsThis(/** @type {Node} */ (self.value))
) {
const arrow = makeNode(
A.ArrowNode,
/** @type {Node} */ (self.value),
/** @type {Node} */ (self.value)
);
arrow.async = /** @type {Node} */ (self.value).async;
arrow.generator = /** @type {Node} */ (self.value).generator;
return makeNode(A.ObjectKeyValNode, self, {
key: A.isSymbolMethodNode(self.key)
? A.SymbolPropertyNode({ name: self.key.name })
: self.key,
value: arrow,
quote: self.quote,
computed: !A.isSymbolMethodNode(self.key),
kind: "init",
method: false,
shorthand: false
});
}
return self;
});
defineOptimizer("ObjectKeyVal", (self, compressor) => {
liftKey(self, compressor);
// p:function(){} ---> p(){}, and the same of a generator, an async
// function, and an arrow with a block body.
const unsafeMethods = compressor.option("unsafe_methods");
if (
unsafeMethods &&
compressor.option("ecma") >= 2015 &&
(!(unsafeMethods instanceof RegExp) ||
unsafeMethods.test(String(A.terserKey(self.key))))
) {
const key = self.key;
const value = self.value;
const isArrowWithBlock =
A.isArrowNode(value) &&
Array.isArray(
/** @type {Node} */ (/** @type {Node} */ (value).body).body
) &&
!ast.containsThis(/** @type {Node} */ (value));
if (
(isArrowWithBlock || A.isFunctionNode(value)) &&
!(/** @type {Node} */ (value).id)
) {
return makeNode(A.ConciseMethodNode, self, {
key: self.computed
? key
: makeNode(A.SymbolMethodNode, self, { name: key.name }),
value: makeNode(
A.AccessorNode,
/** @type {Node} */ (value),
/** @type {Node} */ (value)
),
quote: self.quote,
computed: self.computed,
kind: "init",
method: true,
shorthand: false
});
}
}
return self;
});
// What holds a destructuring an `export` declares, nearest first.
const destructuringExportAncestors = [
A.isVarDefNode,
A.isDefinitionsNode,
A.isExportNode
];
/**
* terser's `is_destructuring_export_decl`: whether the destructuring
* visited is one an `export` declares.
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it is
*/
const isDestructuringExportDeclaration = (compressor) => {
for (
let a = 0, p = 0, length = destructuringExportAncestors.length;
a < length;
p++
) {
const parent = compressor.parent(p);
if (!parent) return false;
if (a === 0 && A.isDestructuringNode(parent)) continue;
if (!destructuringExportAncestors[a](parent)) {
return false;
}
a++;
}
return true;
};
/**
* terser's `should_retain`: whether a name bound by destructuring is kept.
* @param {Compressor} compressor the compressor
* @param {Definition} definition the name's definition
* @returns {boolean} whether it is
*/
const shouldRetain = (compressor, definition) => {
if (definition.references.length) return true;
if (!definition.global) return false;
if (compressor.toplevel.vars) {
if (compressor.top_retain) {
return compressor.top_retain(definition);
}
return false;
}
return true;
};
defineOptimizer("Destructuring", (self, compressor) => {
if (
// `"strict"` is not `true`, where `1` is.
// eslint-disable-next-line eqeqeq
compressor.option("pure_getters") == true &&
compressor.option("unused") &&
!self.is_array &&
Array.isArray(self.properties) &&
!isDestructuringExportDeclaration(compressor) &&
!A.isExpansionNode(self.properties[self.properties.length - 1])
) {
/** @type {Node[]} */
const keep = [];
for (let i = 0; i < self.properties.length; i++) {
const element = self.properties[i];
if (!(
A.isObjectKeyValNode(element) &&
!element.computed &&
A.isSymbolDeclarationNode(element.value) &&
!shouldRetain(
compressor,
/** @type {Node} */ (element.value).definition
)
)) {
keep.push(element);
}
}
if (keep.length !== self.properties.length) {
self.properties = keep;
}
}
return self;
});
};
/** @typedef {"sloppy" | number} NativeCondition `unsafe`, or `builtins_ecma >= N` */
/** @typedef {string | { name: string, when: NativeCondition[] }} NativeName a name and what it is gated on */
/** @typedef {{ when?: NativeCondition[], names: NativeName[] }} NativeGroup a global's names and what they are gated on */
/**
* @typedef {object} NativeObjects
* @property {(compressor: Compressor) => (globalName: string) => boolean} pure_access_globals which globals read without a side effect
* @property {Set<string>} pure_prop_access_globals which globals' properties read without a side effect
* @property {(compressor: Compressor) => (globalName: string) => boolean} is_pure_native_fn which globals call without a side effect
* @property {(compressor: Compressor) => (globalName: string, name: PropertyKey) => boolean} is_pure_native_method which prototype methods call without a side effect
* @property {(compressor: Compressor) => (globalName: string, name: PropertyKey) => boolean} is_pure_native_static_fn which static methods call without a side effect
* @property {(compressor: Compressor) => (globalName: string, name: PropertyKey) => boolean} is_pure_native_static_property which static properties read without a side effect
* @property {(compressor: Compressor, call: Node) => boolean} is_pure_builtin_call whether a call of a builtin is pure
*/
/**
* terser's `sloppy && es >= N && ...` chain: the first falsy condition's value,
* or `true` when every condition holds.
* @param {NativeCondition[]} conditions what the entry is gated on, in order
* @param {CompressOptions["unsafe"]} sloppy the compressor's `unsafe` option, as given
* @param {CompressOptions["builtins_ecma"]} es the compressor's `builtins_ecma` option, as given
* @returns {boolean} `true`, or the falsy value the chain stops at
*/
const evaluateNativeConditions = (conditions, sloppy, es) => {
for (const condition of conditions) {
const value = condition === "sloppy" ? sloppy : es >= condition;
if (!value) return value;
}
return true;
};
/**
* The set terser's `makePredicate(remove_false([...]))` builds from a list.
* @param {NativeName[]} names the list, with its conditions
* @param {CompressOptions["unsafe"]} sloppy the compressor's `unsafe` option, as given
* @param {CompressOptions["builtins_ecma"]} es the compressor's `builtins_ecma` option, as given
* @returns {Set<PropertyKey>} each name whose conditions hold
*/
const selectNativeNames = (names, sloppy, es) => {
/** @type {Set<PropertyKey>} */
const selected = new Set();
for (const entry of names) {
if (typeof entry === "string") {
selected.add(entry);
continue;
}
const value = evaluateNativeConditions(entry.when, sloppy, es);
// A falsy `unsafe` other than `false` survives `remove_false`, as in terser.
if (value === true) selected.add(entry.name);
else if (value !== false) selected.add(value);
}
return selected;
};
/**
* webpack's `native-objects.js`: terser's lookups, built from the tables
* `tooling/generate-js-data.js` reads out of terser's source.
* @param {TerserModules} modules terser's modules, the helpers already installed
* @returns {NativeObjects} the module's exports
*/
const createNativeObjects = (modules) => {
const { ast } = modules;
const {
NATIVE_ARG1_IS_ITERABLE,
NATIVE_ARG1_IS_RANGE_OR_ITERABLE,
NATIVE_LONE_ARG_IS_RANGE,
NATIVE_PURE_ACCESS_GLOBALS,
NATIVE_PURE_FUNCTIONS,
NATIVE_PURE_METHODS,
NATIVE_PURE_PROP_ACCESS_GLOBALS,
NATIVE_PURE_STATIC_FUNCTIONS,
NATIVE_PURE_STATIC_PROPERTIES
} = require("./syntax-printer-data").nativeObjectTables();
const { isArrayNode, isDotNode, isNewNode, isNumberNode, isSymbolRefNode } =
modules.ast;
const { is_undeclared_ref: isUndeclaredRef } = modules.inference;
const argumentIsIterable = new Set(NATIVE_ARG1_IS_ITERABLE);
const argumentIsRangeOrIterable = new Set(NATIVE_ARG1_IS_RANGE_OR_ITERABLE);
const loneArgumentIsRange = new Set(NATIVE_LONE_ARG_IS_RANGE);
const upperCaseFirstLetter = /^[A-Z]/;
/**
* @param {NativeName[]} names the table
* @returns {(compressor: Compressor) => (globalName: string) => boolean} terser's `make_lookup` of it
*/
const makeLookup = (names) => (compressor) => {
const predicate = selectNativeNames(
names,
compressor.option("unsafe"),
compressor.option("builtins_ecma")
);
return (globalName) => predicate.has(globalName);
};
/**
* @param {Record<string, NativeGroup>} groups the table
* @returns {(compressor: Compressor) => (globalName: string, name: PropertyKey) => boolean} terser's `make_nested_lookup` of it
*/
const makeNestedLookup = (groups) => (compressor) => {
const sloppy = compressor.option("unsafe");
const es = compressor.option("builtins_ecma");
/** @type {Map<string, Set<PropertyKey>>} */
const lookup = new Map();
for (const globalName of Object.keys(groups)) {
const group = groups[globalName];
if (
group.when === undefined ||
evaluateNativeConditions(group.when, sloppy, es) === true
) {
lookup.set(globalName, selectNativeNames(group.names, sloppy, es));
}
}
return (globalName, name) => {
const names = lookup.get(globalName);
return names !== undefined && names.has(name);
};
};
/**
* terser's `is_builtin_pure_with_these_args`: some builtins listed pure are
* so only for some arguments.
* @param {string} builtin the builtin's name
* @param {Node[]} args the call's arguments
* @returns {boolean} whether these arguments keep it pure
*/
const isBuiltinPureWithTheseArgs = (builtin, args) => {
// All the builtins dealt with here are fine with no argument.
if (args.length === 0) return true;
let firstArgument = args[0];
if (isSymbolRefNode(firstArgument)) {
firstArgument = /** @type {Node} */ (ast.fixedValue(firstArgument));
}
if (loneArgumentIsRange.has(builtin)) {
// new Array(number)
const argumentValid =
args.length > 1 ||
(isNumberNode(firstArgument) &&
/** @type {number} */ (firstArgument.value) >= 0 &&
/** @type {number} */ (firstArgument.value) <= 0xffffffff);
if (!argumentValid) return false;
}
if (argumentIsRangeOrIterable.has(builtin)) {
// new Float32Array(number | Array)
const argumentValid =
args.length === 0 ||
isArrayNode(firstArgument) ||
(isNumberNode(firstArgument) &&
/** @type {number} */ (firstArgument.value) >= 0 &&
/** @type {number} */ (firstArgument.value) <= 0xffffffff);
if (!argumentValid) return false;
}
if (argumentIsIterable.has(builtin)) {
// new Set(iterable)
const argumentValid = args.length === 0 || isArrayNode(firstArgument);
if (!argumentValid) return false;
}
return true;
};
/**
* terser's `is_pure_builtin_call`, reading the compressor's own lookups.
* @param {Compressor} compressor the compressor
* @param {Node} call a call or `new`
* @returns {boolean} whether it calls a builtin without a side effect
*/
const isPureBuiltinCall = (compressor, call) => {
let builtin = "";
let method = "";
let expression = call.callee;
if (isUndeclaredRef(expression)) {
builtin = expression.name;
} else if (isDotNode(expression)) {
method = expression.property.name;
expression = expression.object;
if (isUndeclaredRef(expression)) {
if (
// globalThis.pureFunc()
expression.name === "globalThis" &&
compressor.option("builtins_ecma") >= 2020
) {
builtin = method;
method = "";
} else {
// SomeBuiltin.pureFunc()
builtin = expression.name;
}
} else if (isDotNode(expression)) {
if (
isUndeclaredRef(expression.object) &&
expression.object.name === "globalThis" &&
compressor.option("builtins_ecma") >= 2020
) {
// globalThis.SomeBuiltin.pureFunc()
builtin = expression.property.name;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
if (!method) {
if (compressor.is_pure_native_fn(builtin)) {
// Some require `new`, others throw when given it.
const isNew = isNewNode(call);
// True of every `is_pure_native_fn` name.
const shouldBeNew = upperCaseFirstLetter.test(builtin);
if (isNew !== shouldBeNew) return false;
if (!isBuiltinPureWithTheseArgs(builtin, call.arguments)) {
return false;
}
return true;
}
return false;
}
return compressor.is_pure_native_static_fn(builtin, method);
};
return {
pure_access_globals: makeLookup(NATIVE_PURE_ACCESS_GLOBALS),
pure_prop_access_globals: new Set(NATIVE_PURE_PROP_ACCESS_GLOBALS),
is_pure_native_fn: makeLookup(NATIVE_PURE_FUNCTIONS),
is_pure_native_method: makeNestedLookup(NATIVE_PURE_METHODS),
is_pure_native_static_fn: makeNestedLookup(NATIVE_PURE_STATIC_FUNCTIONS),
is_pure_native_static_property: makeNestedLookup(
NATIVE_PURE_STATIC_PROPERTIES
),
is_pure_builtin_call: isPureBuiltinCall
};
};
/**
* What terser's `Compressor` constructor sets from its own `native-objects.js`,
* set again from webpack's: called right after each `new Compressor`.
* @param {Compressor} compressor the compressor just constructed
* @param {NativeObjects} nativeObjects `modules.nativeObjects`
* @returns {void}
*/
const assignNativeLookups = (compressor, nativeObjects) => {
compressor.pure_access_globals =
nativeObjects.pure_access_globals(compressor);
compressor.is_pure_native_fn = nativeObjects.is_pure_native_fn(compressor);
compressor.is_pure_native_method =
nativeObjects.is_pure_native_method(compressor);
compressor.is_pure_native_static_fn =
nativeObjects.is_pure_native_static_fn(compressor);
compressor.is_pure_native_static_property =
nativeObjects.is_pure_native_static_property(compressor);
};
// What terser's `regexp_source_fix` writes in place of each line terminator.
const LINE_TERMINATOR_ESCAPES = {
"\0": "0",
"\n": "n",
"\r": "r",
"\u2028": "u2028",
"\u2029": "u2029"
};
// The patterns terser's `regexp_is_safe` lets it build without risking a ReDoS.
const SAFE_REGEXP = /^[\\/|\0\s\w^$.[\]()]*$/;
/**
* The bits of terser's `compressor-flags.js`, kept in `node.flags`, which
* terser's own code reads too.
*/
const COMPRESSOR_FLAGS = {
UNUSED: 0b00000001,
TRUTHY: 0b00000010,
FALSY: 0b00000100,
UNDEFINED: 0b00001000,
INLINED: 0b00010000,
// Nodes ever written to, which `unused: "keep_assign"` reads.
WRITE_ONLY: 0b00100000,
// The bits below hold for one compression pass only.
SQUEEZED: 0b0000000100000000,
OPTIMIZED: 0b0000001000000000,
TOP: 0b0000010000000000
};
/** @typedef {{ parent: (n?: number) => Node }} ParentStack a walker, or anything else answering which node encloses the one visited (undefined past the root) */
/**
* The exports of terser's `compress/common.js`, `inference.js`,
* `compressor-flags.js` and `utils/index.js` the phases read, ported.
* @typedef {object} CompressHelpers
* @property {Record<string, EXPECTED_ANY>} common terser's `compress/common.js`
* @property {Record<string, EXPECTED_ANY>} inference the functions of terser's `compress/inference.js`
* @property {Record<string, EXPECTED_ANY>} flags terser's `compress/compressor-flags.js`
* @property {Record<string, EXPECTED_ANY>} utils terser's `utils/index.js`
*/
/**
* webpack's port of the helpers every compress phase reads, under terser's
* names. `MAP.splice` shares terser's `Splice`, which its own transforms return.
* @param {MinifierModules} modules terser's modules
* @returns {CompressHelpers} the helpers
*/
const createCompressHelpers = (modules) => {
const { walkNode } = /** @type {TerserAst} */ (
/** @type {TerserAst | Ast} */ (modules.ast)
);
const { ast } = /** @type {TerserModules} */ (
/** @type {TerserModules | MinifierModules} */ (modules)
);
const {
BigIntNode,
BlockStatementNode,
FalseNode,
FunctionNode,
InfinityNode,
NaNNode,
NullNode,
NumberNode,
RegExpNode,
SequenceNode,
SimpleStatementNode,
StringNode,
TrueNode,
UnaryPrefixNode
} = ast;
const {
isArrayNode,
isArrowNode,
isAssignNode,
isBlockStatementNode,
isCallNode,
isCallExpressionNode,
isChainNode,
isClassNode,
isConstNode,
isConstantNode,
isDefClassNode,
isDefunNode,
isEmptyStatementNode,
isExportNode,
isForInNode,
isFunctionNode,
isImportNode,
isInfinityNode,
isLabeledStatementNode,
isLambdaNode,
isLetNode,
isLoopControlNode,
isNaNNode,
isNewNode,
isSyntaxNode,
isNullNode,
isObjectNode,
isObjectKeyValNode,
isPropAccessNode,
isScopeNode,
isSequenceNode,
isSimpleStatementNode,
isStatementNode,
isSymbolRefNode,
isUnaryNode,
isUnaryPrefixNode,
isUndefinedNode,
isUsingNode
} = ast;
const {
createWalker,
terserKey,
walk,
walk_abort: walkAbort,
walk_parent: walkParent
} = ast;
const { TOP, UNDEFINED } = COMPRESSOR_FLAGS;
/**
* terser's `regexp_source_fix`: escapes each line terminator, and `\0`, a
* regular expression's source holds, where no backslash escapes it already.
* @param {string} source a regular expression's source
* @returns {string} the source escaped
*/
const regexpSourceFix = (source) =>
source.replace(/[\0\n\r\u2028\u2029]/g, (match, offset) => {
const escaped =
source[offset - 1] === "\\" &&
(source[offset - 2] !== "\\" ||
/(?:^|[^\\])(?:\\{2})*$/.test(source.slice(0, offset - 1)));
return (
(escaped ? "" : "\\") +
LINE_TERMINATOR_ESCAPES[
/** @type {keyof typeof LINE_TERMINATOR_ESCAPES} */ (match)
]
);
});
/**
* terser's `regexp_is_safe`.
* @param {string} source a regular expression's source
* @returns {boolean} whether terser may build it without risking a ReDoS
*/
const regexpIsSafe = (source) => SAFE_REGEXP.test(source);
/**
* terser's `makePredicate`: note that it sorts an array it is given in place.
* @param {string | string[]} words the words, or a string of them separated by spaces
* @returns {Set<string>} the words
*/
const makePredicate = (words) =>
new Set((Array.isArray(words) ? words : words.split(" ")).sort());
/**
* terser's `member`.
* @template T
* @param {T} name an item
* @param {T[]} array a list
* @returns {boolean} whether the list holds the item
*/
const member = (name, array) => array.includes(name);
/**
* terser's `has_annotation`.
* @param {Node} node a node
* @param {number} annotation one of terser's annotation bits
* @returns {number} the bit where the node carries it, otherwise 0
*/
const hasAnnotation = (node, annotation) => node._annotations & annotation;
/**
* terser's `has_flag`.
* @param {Node} node a node
* @param {number} flag one of terser's compressor flags
* @returns {number} the flag where the node carries it, otherwise 0
*/
const hasFlag = (node, flag) => node.flags & flag;
/**
* terser's `set_flag`.
* @param {Node} node a node
* @param {number} flag one of terser's compressor flags
* @returns {void}
*/
const setFlag = (node, flag) => {
node.flags |= flag;
};
/**
* terser's `clear_flag`.
* @param {Node} node a node
* @param {number} flag one of terser's compressor flags
* @returns {void}
*/
const clearFlag = (node, flag) => {
node.flags &= ~flag;
};
const firstInStatement = createFirstInStatement(
/** @type {TerserModules} */ (
/** @type {TerserModules | MinifierModules} */ (modules)
)
);
/**
* terser's `MAP`: each node of a list transformed, dropping what a transform
* skips and spreading what it splices.
* @param {Node[]} array the nodes
* @param {Walker} walker the transformer
* @param {boolean=} allowSplicing whether a transform may splice
* @returns {Node[]} the nodes transformed
*/
const MAP = (array, walker, allowSplicing = true) => {
/** @type {Node[]} */
const result = [];
for (let i = 0; i < array.length; ++i) {
// A transform may return a splice, which `transformNode` passes on.
const transformed = /** @type {Node | Splice<Node>} */ (
ast.transformNode(array[i], walker, allowSplicing)
);
if (isSyntaxNode(transformed)) {
result.push(transformed);
} else if (transformed instanceof Splice) {
result.push(...transformed.v);
}
}
return result;
};
/**
* @param {Node[]} value the nodes
* @returns {Splice<Node>} what puts them in place of the node transformed
*/
MAP.splice = (value) => new Splice(value);
/** @type {SkipMarker} */
MAP.skip = {};
/**
* terser's `make_void_0`, as an `UndefinedNode` may meet a variable of that name.
* @param {Node | undefined} orig where its position comes from
* @returns {Node} `void 0`
*/
const makeVoid0 = (orig) =>
makeNode(UnaryPrefixNode, orig, {
operator: "void",
argument: makeNode(NumberNode, orig, { value: 0 })
});
/**
* terser's `merge_sequence`.
* @param {Node[]} array a list of expressions
* @param {Node} node an expression, whose own expressions join where it is a sequence
* @returns {Node[]} the list
*/
const mergeSequence = (array, node) => {
if (isSequenceNode(node)) {
array.push(...node.expressions);
} else {
array.push(node);
}
return array;
};
/**
* terser's `make_sequence`: the one expression, or a sequence of them flattened.
* @param {Node} orig where its position comes from
* @param {Node[]} expressions at least one expression
* @returns {Node} the expression
*/
const makeSequence = (orig, expressions) => {
if (expressions.length === 1) return expressions[0];
if (expressions.length === 0) {
throw new Error("trying to create a sequence with length zero!");
}
return makeNode(SequenceNode, orig, {
expressions: expressions.reduce(mergeSequence, [])
});
};
/**
* terser's `make_empty_function`.
* @param {Node} self where its position comes from
* @returns {Node} `function(){}`
*/
const makeEmptyFunction = (self) => {
const node = makeNode(FunctionNode, self, {
params: [],
body: makeNode(BlockStatementNode, undefined, { body: [] }),
generator: false,
async: false
});
node.block_scope = createCompressorScope("function", node, null, true);
return node;
};
/**
* terser's `make_node_from_constant`.
* @param {ConstantValue} value a value
* @param {Node} orig where its position comes from
* @returns {Node} the node for the value
*/
const makeNodeFromConstant = (value, orig) => {
switch (typeof value) {
case "string":
return makeNode(StringNode, orig, { value });
case "number":
if (Number.isNaN(value)) return makeNode(NaNNode, orig);
if (Number.isFinite(value)) {
return 1 / value < 0
? makeNode(UnaryPrefixNode, orig, {
operator: "-",
argument: makeNode(NumberNode, orig, { value: -value })
})
: makeNode(NumberNode, orig, { value });
}
return value < 0
? makeNode(UnaryPrefixNode, orig, {
operator: "-",
argument: makeNode(InfinityNode, orig)
})
: makeNode(InfinityNode, orig);
case "bigint":
// A literal holding the sign prints unparenthesized as a `**` base.
return value < 0 &&
modules.corrections !== undefined &&
modules.corrections.enabled
? makeNode(UnaryPrefixNode, orig, {
operator: "-",
argument: makeNode(BigIntNode, orig, {
value: (-value).toString()
})
})
: makeNode(BigIntNode, orig, { value: value.toString() });
case "boolean":
return makeNode(value ? TrueNode : FalseNode, orig);
case "undefined":
return makeVoid0(orig);
default:
if (value === null) {
return makeNode(NullNode, orig, { value: null });
}
if (value instanceof RegExp) {
return makeNode(RegExpNode, orig, {
value: {
source: regexpSourceFix(value.source),
flags: value.flags
}
});
}
throw new Error(
stringTemplate("Can't handle constant of type: {type}", {
type: typeof value
})
);
}
};
/**
* terser's `best_of_expression`: the second only where it is smaller.
* @param {Node} first an expression
* @param {Node} second an equivalent one
* @returns {Node} the smaller
*/
const bestOfExpression = (first, second) =>
ast.nodeSize(first) > ast.nodeSize(second) ? second : first;
/**
* terser's `best_of_statement`, which weighs each as a statement of its own.
* @param {Node} first an expression
* @param {Node} second an equivalent one
* @returns {Node} the smaller
*/
const bestOfStatement = (first, second) =>
bestOfExpression(
makeNode(SimpleStatementNode, first, { expression: first }),
makeNode(SimpleStatementNode, second, { expression: second })
).expression;
/**
* terser's `best_of`.
* @param {Compressor} compressor the compressor, at the node replaced
* @param {Node} first an expression
* @param {Node} second an equivalent one
* @returns {Node} the smaller where it stands
*/
const bestOf = (compressor, first, second) =>
firstInStatement(/** @type {ParentStack} */ (compressor))
? bestOfStatement(first, second)
: bestOfExpression(first, second);
/**
* terser's `get_simple_key`: a property key as a value, where it is a constant.
* @param {Node | string | undefined} key a key
* @returns {Node | string | ReturnType<AstHelpers["constantValue"]> | undefined} its value, or the key itself
*/
const getSimpleKey = (key) => {
const node = /** @type {Node} */ (key);
if (isConstantNode(node)) {
return ast.constantValue(node);
}
if (
isUnaryPrefixNode(node) &&
node.operator === "void" &&
isConstantNode(node.argument)
) {
return undefined;
}
return key;
};
/**
* terser's `read_property`: what a literal holds under a key, where it is known.
* @param {Node} object an array or object literal
* @param {Node | string | undefined} property the key read
* @returns {Node | null | undefined} the value, null for an array's hole
*/
const readProperty = (object, property) => {
let key = getSimpleKey(property);
if (isSyntaxNode(key)) return;
let value;
if (isArrayNode(object)) {
const elements = object.elements;
if (key === "length") {
return makeNodeFromConstant(elements.length, object);
}
if (typeof key === "number" && key in elements) value = elements[key];
} else if (isObjectNode(object)) {
key = `${key}`;
const properties = object.properties;
for (let i = properties.length; --i >= 0;) {
const objectProperty = properties[i];
if (!isObjectKeyValNode(objectProperty)) return;
if (!value && terserKey(objectProperty.key) === key) {
value = /** @type {Node} */ (objectProperty.value);
}
}
}
return (
(isSymbolRefNode(value) && ast.fixedValue(/** @type {Node} */ (value))) ||
value
);
};
/**
* terser's `has_break_or_continue`.
* @param {Node} loop a loop
* @param {Node=} parent the statement around it, read where it is a label
* @returns {boolean} whether a `break` or `continue` in its body targets it
*/
const hasBreakOrContinue = (loop, parent) => {
let found = false;
const walker = createWalker(
/**
* @param {Node} node a node visited
* @returns {boolean | undefined} whether to skip its children
*/
(node) => {
if (found || isScopeNode(node)) return true;
if (
isLoopControlNode(node) &&
walker.loopcontrol_target(node) === loop
) {
found = true;
return true;
}
return undefined;
}
);
if (isLabeledStatementNode(parent)) {
walker.push(/** @type {Node} */ (parent));
}
walker.push(loop);
walkNode(/** @type {Node} */ (loop.body), walker);
return found;
};
/**
* terser's `requires_sequence_to_maintain_binding`: `(0, x.noThis)()`,
* `(0, eval)()` and `delete (0, x)` need their sequence.
* @param {Node} parent the node around the sequence, if any
* @param {Node} orig the sequence
* @param {Node} value its last expression
* @returns {boolean} whether dropping the sequence changes the meaning
*/
const requiresSequenceToMaintainBinding = (parent, orig, value) =>
(isUnaryPrefixNode(parent) && parent.operator === "delete") ||
(isCallNode(parent) &&
parent.callee === orig &&
(isChainNode(value) ||
isPropAccessNode(value) ||
(isSymbolRefNode(value) && value.name === "eval")));
/**
* terser's `maintain_this_binding`: `(1, func)()` calls `func` without its
* object, so it keeps the sequence.
* @param {Node} parent the node around the one replaced, if any
* @param {Node} orig the node replaced
* @param {Node} value what replaces it
* @returns {Node} the replacement
*/
const maintainThisBinding = (parent, orig, value) => {
if (requiresSequenceToMaintainBinding(parent, orig, value)) {
const zero = makeNode(NumberNode, orig, { value: 0 });
return makeSequence(orig, [zero, value]);
}
return value;
};
/**
* terser's `is_func_expr`.
* @param {Node} node a node
* @returns {boolean} whether it is a function or arrow expression
*/
const isFuncExpr = (node) => isArrowNode(node) || isFunctionNode(node);
/**
* terser's `is_iife_call`: an arrow is left out, as negating it needs parentheses.
* @param {Node} node a node
* @returns {boolean} whether it calls a function expression, or such a call's result
*/
const isIifeCall = (node) => {
if (!isCallExpressionNode(node)) return false;
return isFunctionNode(node.callee) || isIifeCall(node.callee);
};
/**
* terser's `is_empty`.
* @param {Node | null} thing a statement
* @returns {boolean} whether it does nothing
*/
const isEmpty = (thing) => {
if (thing === null) return true;
if (isEmptyStatementNode(thing)) return true;
if (isBlockStatementNode(thing)) {
return /** @type {Node[]} */ (thing.body).length === 0;
}
return false;
};
/**
* terser's `is_identifier_atom`.
* @param {Node} node a node
* @returns {boolean} whether it is `Infinity`, `NaN` or `undefined`
*/
const isIdentifierAtom = (node) =>
isInfinityNode(node) || isNaNNode(node) || isUndefinedNode(node);
/**
* terser's `is_ref_of`.
* @param {Node} reference a node
* @param {NodeTest} test what a declaration is
* @returns {boolean | undefined} whether it references a name one of whose declarations passes the test
*/
const isRefOf = (reference, test) => {
if (!isSymbolRefNode(reference)) return false;
const orig = reference.definition.identifiers;
for (let i = orig.length; --i >= 0;) {
if (test(orig[i])) return true;
}
return undefined;
};
/**
* terser's `can_be_evicted_from_block`.
* @param {Node} node a statement
* @returns {boolean} whether it can leave its block without changing scope
*/
const canBeEvictedFromBlock = (node) =>
!(
isDefClassNode(node) ||
isDefunNode(node) ||
isLetNode(node) ||
isConstNode(node) ||
isUsingNode(node) ||
isExportNode(node) ||
isImportNode(node)
);
/**
* terser's `as_statement_array`.
* @param {Node | null} thing a statement
* @returns {Node[]} the statements it stands for
*/
const asStatementArray = (thing) => {
if (thing === null) return [];
if (isBlockStatementNode(thing)) return /** @type {Node[]} */ (thing.body);
if (isEmptyStatementNode(thing)) return [];
if (isStatementNode(thing)) return [thing];
throw new Error("Can't convert thing to statement array");
};
/**
* terser's `is_reachable`: a sync function called in place runs at once, so it is
* searched as part of the scope rather than as a closure.
* @param {Node} scopeNode a scope
* @param {Definition[]} definitions definitions
* @returns {boolean} whether a closure in the scope references one of them
*/
const isReachable = (scopeNode, definitions) => {
/**
* @param {Node} node a node visited
* @returns {symbol | undefined} `walk_abort` at a reference to one of them
*/
const findReference = (node) => {
if (isSymbolRefNode(node) && definitions.includes(node.definition)) {
return walkAbort;
}
return undefined;
};
return walkParent(
scopeNode,
/**
* @param {Node} node a node visited
* @param {ParentStack} info its ancestors
* @returns {symbol | boolean | undefined} whether to stop, or skip its children
*/
(node, info) => {
if (isScopeNode(node) && node !== scopeNode) {
const parent = info.parent();
if (
isCallNode(parent) &&
parent.callee === node &&
!(node.async || node.generator)
) {
return undefined;
}
if (walk(node, findReference)) return walkAbort;
return true;
}
return undefined;
}
);
};
/**
* terser's `is_recursive_ref`.
* @param {ParentStack} walker a walker at a reference
* @param {Definition} definition what it references
* @returns {boolean} whether it names a function or class it is inside
*/
const isRecursiveRef = (walker, definition) => {
let node;
for (let i = 0; (node = walker.parent(i)); i++) {
if (isLambdaNode(node) || isClassNode(node)) {
const name = node.id;
if (name && name.definition === definition) {
return true;
}
}
}
return false;
};
/**
* terser's `retain_top_func`, which reads only a `function` declaration.
* @param {Node} fn a function
* @param {Compressor} compressor the compressor
* @returns {boolean | number | undefined} truthy where `top_retain` keeps it
*/
const retainTopFunc = (fn, compressor) =>
compressor.top_retain &&
isDefunNode(fn) &&
hasFlag(fn, TOP) &&
fn.id &&
compressor.top_retain(fn.id.definition);
const unarySideEffects = makePredicate("delete ++ --");
/**
* terser's `is_undeclared_ref`.
* @param {Node} node a node
* @returns {boolean} whether it references a global never declared
*/
const isUndeclaredRef = (node) =>
isSymbolRefNode(node) && node.definition.undeclared;
/**
* terser's `is_undefined`.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {number | boolean} truthy where it is `undefined`
*/
const isUndefined = (node, compressor) =>
hasFlag(node, UNDEFINED) ||
isUndefinedNode(node) ||
(isUnaryPrefixNode(node) &&
node.operator === "void" &&
!ast.hasSideEffects(node.argument, compressor));
/**
* terser's `is_null_or_undefined`.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {number | boolean} truthy where it is explicitly `null` or `undefined`
*/
const isNullOrUndefined = (node, compressor) => {
if (isNullNode(node)) return true;
const undefinedValue = isUndefined(node, compressor);
if (undefinedValue) return undefinedValue;
if (!isSymbolRefNode(node)) return false;
let fixed = node.definition.fixed;
// terser reads only a value stored as a node, not one read lazily; an
// exposed variable stays declared, so `void 0` saves nothing there.
if (
!isSyntaxNode(fixed) &&
modules.improvements !== undefined &&
modules.improvements.enabled &&
!compressor.exposed(node.definition)
) {
fixed = ast.fixedValue(node);
// A value written again after it was set may no longer be nullish.
if (
isSyntaxNode(fixed) &&
(node.definition.assignments !== 0 ||
node.definition.patternWrites !== 0)
) {
return false;
}
// A `let` whose `void 0` was dropped still reads as `undefined`.
if (
fixed === null &&
ast.isSymbolLetNode(node.definition.identifiers[0]) &&
node.definition.assignments === 0 &&
node.definition.patternWrites === 0
) {
return true;
}
}
return isSyntaxNode(fixed) && isNullish(fixed, compressor);
};
/**
* terser's `is_nullish_shortcircuited`.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {boolean | number} truthy where an optional chain in it short-circuits
*/
const isNullishShortCircuited = (node, compressor) => {
if (isPropAccessNode(node) || isCallNode(node)) {
const inner = isCallNode(node) ? node.callee : node.object;
return (
(node.optional && isNullOrUndefined(inner, compressor)) ||
isNullishShortCircuited(inner, compressor)
);
}
if (isChainNode(node)) {
return isNullishShortCircuited(node.expression, compressor);
}
return false;
};
/**
* terser's `is_nullish`.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {boolean | number} truthy where it is `null` or `undefined`
*/
function isNullish(node, compressor) {
if (isNullOrUndefined(node, compressor)) return true;
return isNullishShortCircuited(node, compressor);
}
/**
* terser's `is_lhs`.
* @param {Node} node a node
* @param {Node} parent the node around it, if any
* @returns {Node | undefined} what the parent writes to, where it writes the node
*/
const isLhs = (node, parent) => {
if (isUnaryNode(parent) && unarySideEffects.has(parent.operator)) {
return parent.argument;
}
if (isAssignNode(parent) && parent.left === node) return node;
if (isForInNode(parent) && parent.left === node) return node;
return undefined;
};
/**
* terser's `aborts`.
* @param {Node | null | undefined} thing a statement
* @returns {Node | false | null | undefined} the statement it ends with that aborts, or something falsy
*/
const aborts = (thing) => thing && ast.aborts(thing);
/**
* Whether a value is a binding another module exports, whose function, as
* swc assumes, reads no `this` it is called with.
* @param {Node | false | null | undefined} value a callee's value
* @returns {boolean} true when it is, in the improve phase
*/
const callsImport = (value) => {
if (
modules.improvements === undefined ||
!modules.improvements.enabled ||
!value ||
!isSymbolRefNode(value)
) {
return false;
}
const definition = value.definition;
return (
definition !== undefined &&
ast.kindOf(definition.identifiers[0]) === "SymbolImport"
);
};
/**
* terser's `is_modified`.
* @param {Compressor} compressor the compressor
* @param {ParentStack} walker a walker at the node
* @param {Node} node a node
* @param {Node | false | null | undefined} value what it holds, null for a hole, false where unknown
* @param {number} level how far up the walker's stack the node's parent is
* @param {boolean=} immutable whether the value cannot be modified through a call
* @returns {Node | boolean | undefined} truthy where the node may be written to or mutated
*/
const isModified = (compressor, walker, node, value, level, immutable) => {
const parent = walker.parent(level);
const lhs = isLhs(node, parent);
if (lhs) return lhs;
if (
!immutable &&
isCallNode(parent) &&
parent.callee === node &&
!isArrowNode(value) &&
!isClassNode(value) &&
!ast.isCalleePure(parent, compressor) &&
!callsImport(value) &&
(!isFunctionNode(value) ||
(!isNewNode(parent) && ast.containsThis(/** @type {Node} */ (value))))
) {
return true;
}
if (isArrayNode(parent)) {
return isModified(compressor, walker, parent, parent, level + 1);
}
if (isObjectKeyValNode(parent) && node === parent.value) {
const object = walker.parent(level + 1);
return isModified(compressor, walker, object, object, level + 2);
}
if (isPropAccessNode(parent) && parent.object === node) {
const property = readProperty(
/** @type {Node} */ (value),
ast.terserProperty(parent)
);
return (
!immutable &&
isModified(compressor, walker, parent, property, level + 1)
);
}
// `(c ? t : u).push()` changes what `?:`, `||`, `&&`, `??` or a sequence picks.
if (
modules.corrections !== undefined &&
modules.corrections.enabled &&
((ast.isConditionalNode(parent) && parent.test !== node) ||
(ast.isBinaryNode(parent) &&
(parent.operator === "||" ||
parent.operator === "&&" ||
parent.operator === "??")) ||
(isSequenceNode(parent) && ast.tailNode(parent) === node))
) {
return isModified(
compressor,
walker,
parent,
value,
level + 1,
immutable
);
}
return undefined;
};
/**
* terser's `is_used_in_expression`: `void (0, node, 1)` does not use it,
* `console.log(0, node)` does.
* @param {ParentStack} walker a walker at the node
* @returns {boolean} whether the expression around the node may use its value
*/
const isUsedInExpression = (walker) => {
for (let p = -1; ; p++) {
const node = walker.parent(p);
const parent = walker.parent(p + 1);
if (!parent) break;
if (isSequenceNode(parent)) {
const nthExpression = parent.expressions.indexOf(node);
if (nthExpression !== parent.expressions.length - 1) {
const grandparent = walker.parent(p + 2);
return !(
parent.expressions.length > 2 ||
parent.expressions.length === 1 ||
!requiresSequenceToMaintainBinding(
grandparent,
parent,
parent.expressions[1]
)
);
}
continue;
}
if (isUnaryNode(parent)) {
const operator = parent.operator;
if (operator === "void") {
return false;
}
if (
operator === "typeof" ||
operator === "+" ||
operator === "-" ||
operator === "!" ||
operator === "~"
) {
continue;
}
}
if (isSimpleStatementNode(parent) || isLabeledStatementNode(parent)) {
return false;
}
if (isScopeNode(parent)) {
return false;
}
return true;
}
return true;
};
return {
common: {
as_statement_array: asStatementArray,
best_of: bestOf,
best_of_expression: bestOfExpression,
can_be_evicted_from_block: canBeEvictedFromBlock,
get_simple_key: getSimpleKey,
has_break_or_continue: hasBreakOrContinue,
identifier_atom: makePredicate("Infinity NaN undefined"),
is_empty: isEmpty,
is_func_expr: isFuncExpr,
is_identifier_atom: isIdentifierAtom,
is_iife_call: isIifeCall,
is_reachable: isReachable,
is_recursive_ref: isRecursiveRef,
is_ref_of: isRefOf,
maintain_this_binding: maintainThisBinding,
make_empty_function: makeEmptyFunction,
make_node_from_constant: makeNodeFromConstant,
make_sequence: makeSequence,
merge_sequence: mergeSequence,
read_property: readProperty,
retain_top_func: retainTopFunc
},
inference: {
aborts,
bitwise_binop: makePredicate("<<< >> << & | ^ ~"),
is_lhs: isLhs,
is_modified: isModified,
is_nullish: isNullish,
is_nullish_shortcircuited: isNullishShortCircuited,
is_undeclared_ref: isUndeclaredRef,
is_undefined: isUndefined,
is_used_in_expression: isUsedInExpression,
lazy_op: makePredicate("&& || ??"),
unary_side_effects: unarySideEffects
},
flags: {
...COMPRESSOR_FLAGS,
CLEAR_BETWEEN_PASSES:
COMPRESSOR_FLAGS.SQUEEZED |
COMPRESSOR_FLAGS.OPTIMIZED |
COMPRESSOR_FLAGS.TOP,
clear_flag: clearFlag,
has_flag: hasFlag,
set_flag: setFlag
},
utils: {
MAP,
has_annotation: hasAnnotation,
makePredicate,
make_node: makeNode,
make_void_0: makeVoid0,
member,
regexp_is_safe: regexpIsSafe,
regexp_source_fix: regexpSourceFix,
remove: removeAll,
return_false: alwaysFalse
}
};
};
/**
* Installs webpack's own helper modules: every phase after this one reads the
* helpers of `compress/common.js`, `inference.js`, `compressor-flags.js`,
* `native-objects.js` and `utils` from webpack instead of terser.
* @param {TerserModules} modules terser's modules, whose helpers it replaces
* @returns {void}
*/
const installHelpers = (modules) => {
const helpers = /** @type {TerserCompressHelpers} */ (
createCompressHelpers(
/** @type {MinifierModules} */ (
/** @type {MinifierModules | TerserModules} */ (modules)
)
)
);
modules.common = helpers.common;
modules.inference = helpers.inference;
modules.flags = helpers.flags;
modules.utils = helpers.utils;
modules.nativeObjects = createNativeObjects(modules);
};
/**
* What the optimizers of `compress/index.js` share, with `tighten-body.js` and
* `inline.js` they call.
* @typedef {object} OptimizerHelpers
* @property {(Type: string, optimizer: (self: Node, compressor: Compressor) => Node) => void} defineOptimizer terser's `def_optimize`
* @property {(compressor: Compressor, name: string) => Definition | undefined} findVariable terser's `find_variable`
* @property {(self: Node, compressor: Compressor) => Node} optimizeLambda the lambda optimizer
* @property {(lhs: Node | boolean, self: Node) => boolean} isAtomic terser's `is_atomic`
* @property {(self: Node, compressor: Compressor) => Node | null} unsafeUndefinedRef terser's `unsafe_undefined_ref`
* @property {(elements: (Node | null)[]) => void} inlineArrayLikeSpread terser's `inline_array_like_spread`
* @property {(statements: Node[], compressor: Compressor) => void} tightenBody terser's `tighten_body`
* @property {(compressor: Compressor, statement: Node, target: Node[], listed?: boolean) => void} extractFromUnreachableCode terser's `extract_from_unreachable_code`
* @property {(self: Node, compressor: Compressor) => Node} inlineIntoSymbolRef terser's `inline_into_symbolref`
* @property {(self: Node, compressor: Compressor) => Node} inlineIntoCall terser's `inline_into_call`
*/
/**
* Installs webpack's optimizers: terser's `optimize` of every node, the methods
* of `compress/index.js` they call, and the statement and call inlining of
* `tighten-body.js` and `inline.js`.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installOptimize = (modules) => {
const { ast, flags } = modules;
const A = ast;
const { OPTIMIZED, UNDEFINED } = flags;
const { make_node: makeNode } = modules.utils;
// Each optimizer by the name of the nodes it optimizes, terser's class: the
// table below picks one per node, and the corrections wrap them by name.
/** @type {Record<string, (self: Node, compressor: Compressor) => Node>} */
const optimizers = {};
/**
* terser's `def_optimize`: an optimizer, run once per node and pass.
* @param {string} name the nodes it optimizes, by terser's class
* @param {(self: Node, compressor: Compressor) => Node} optimizer the optimizer
* @returns {void}
*/
const defineOptimizer = (name, optimizer) => {
optimizers[name] = (self, compressor) => {
if ((self.flags & OPTIMIZED) !== 0) return self;
if (compressor.has_directive("use asm")) return self;
const optimized = optimizer(self, compressor);
optimized.flags |= OPTIMIZED;
return optimized;
};
};
/**
* terser's `find_variable`: a name as the scope around the node visited reads it.
* @param {Compressor} compressor the compressor
* @param {string} name the name
* @returns {Definition | undefined} what it reads
*/
const findVariable = (compressor, name) => {
/** @type {Scope | undefined} */
let scope;
let node;
let i = 0;
while ((node = compressor.parent(i++))) {
if (A.isScopeNode(node)) {
scope = node.block_scope;
break;
}
if (A.isCatchNode(node) && node.param) {
// A pattern declares in the clause's own scope; terser read its
// `definition`, which only a lone name has.
scope = node.param.definition
? node.param.definition.scope
: node.block_scope;
break;
}
}
// The program is always among the parents, so a scope is always found.
return /** @type {Scope} */ (scope).findVariable(name);
};
/**
* @param {Node | boolean} lhs an assignment's target, or whether the node visited is one, as `is_lhs` answers
* @param {Node} self the assignment
* @returns {boolean} whether reading the target again costs nothing
*/
const isAtomic = (lhs, self) =>
A.isSymbolRefNode(lhs) ||
ast.kindOf(/** @type {Node} */ (lhs)) === ast.kindOf(self);
/**
* terser's `unsafe_undefined_ref`: a reference to a variable named `undefined`.
* @param {Node} self the node that reads as `undefined`
* @param {Compressor} compressor the compressor
* @returns {Node | null} the reference, or null where there is none
*/
const unsafeUndefinedRef = (self, compressor) => {
if (compressor.option("unsafe_undefined")) {
const definition = findVariable(compressor, "undefined");
if (definition) {
const reference = makeNode(A.SymbolRefNode, self, {
name: "undefined",
scope: definition.scope,
definition
});
reference.flags |= UNDEFINED;
return reference;
}
}
return null;
};
/**
* Spreads in place each array literal spread among array-like elements.
* @param {(Node | null)[]} elements the elements, null for a hole
* @returns {void}
*/
const inlineArrayLikeSpread = (elements) => {
const { arrayPrototypePatched } = modules.webpackInline;
if (arrayPrototypePatched && arrayPrototypePatched()) return;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (element !== null && A.isExpansionNode(element)) {
const expression = element.argument;
if (A.isArrayNode(expression) && !expression.elements.includes(null)) {
elements.splice(i, 1, ...expression.elements);
// The element at `i` is a new one.
i--;
}
}
}
};
const { tightenBody, extractFromUnreachableCode } =
createTightenBody(modules);
const { inlineIntoSymbolRef, inlineIntoCall } = createInline(modules);
/**
* @param {Node} self a function
* @param {Compressor} compressor the compressor
* @returns {boolean} whether it is called where it is written
*/
const isIifeCallee = (self, compressor) => {
const parent = compressor.parent();
return A.isCallNode(parent) && parent.callee === self;
};
/**
* the lambda optimizer.
* @param {Node} self a function
* @param {Compressor} compressor the compressor
* @returns {Node} the function
*/
const optimizeLambda = (self, compressor) => {
tightenBody(
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body),
compressor
);
const { corrections } = modules;
if (
compressor.option("side_effects") &&
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body).length ===
1 &&
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body)[0] ===
compressor.has_directive("use strict") &&
// A plain function's `caller` and `arguments` throw only while strict,
// which nothing reads of one called in place.
!(
corrections !== undefined &&
corrections.enabled &&
(A.isFunctionNode(self) || A.isDefunNode(self)) &&
!self.generator &&
!self.async &&
!isIifeCallee(self, compressor)
)
) {
/** @type {Node[]} */ (/** @type {Node} */ (self.body).body).length = 0;
}
return self;
};
/** @type {OptimizerHelpers} */
const helpers = {
defineOptimizer,
findVariable,
optimizeLambda,
isAtomic,
unsafeUndefinedRef,
inlineArrayLikeSpread,
tightenBody,
extractFromUnreachableCode,
inlineIntoSymbolRef,
inlineIntoCall
};
installStatementOptimizers(modules, helpers);
installOperatorOptimizers(modules, helpers);
installValueOptimizers(modules, helpers);
// Each kind's optimizer under a key on its prototype: that of the nearest of
// its classes `optimizers` has, as terser's `optimize` was a method of them.
const OPTIMIZER_OF = Symbol("optimizer");
for (const [prototypeKey, prototype] of NODE_KIND_PROTOTYPES) {
const kind = prototypeKey.slice(0, prototypeKey.indexOf("|"));
/** @type {Record<symbol, Optimize>} */ (prototype)[OPTIMIZER_OF] =
optimizers[
/** @type {string} */ (
NODE_KIND_ANCESTRY[kind].find((name) => hasOwn(optimizers, name))
)
];
}
/**
* terser's `optimize`: what replaces a node once optimized.
* @param {Node} node a node
* @param {Compressor} compressor the compressor
* @returns {Node} what replaces it
*/
ast.optimize = (node, compressor) =>
/** @type {Node & { [OPTIMIZER_OF]: AstHelpers["optimize"] }} */ (node)[
OPTIMIZER_OF
](node, compressor);
/**
* Replaces the optimizer named, on its kinds too, with one wrapping it.
* @param {string} name the nodes it optimizes, by terser's class
* @param {(original: (self: Node, compressor: Compressor) => Node) => (self: Node, compressor: Compressor) => Node} wrap builds the replacement
* @returns {boolean} whether an optimizer of that name was there to wrap
*/
ast.wrapOptimizer = (name, wrap) => {
const original = optimizers[name];
if (original === undefined) return false;
const wrapped = wrap(original);
optimizers[name] = wrapped;
for (const prototype of NODE_KIND_PROTOTYPES.values()) {
const slots = /** @type {Record<symbol, Optimize>} */ (prototype);
if (slots[OPTIMIZER_OF] === original) slots[OPTIMIZER_OF] = wrapped;
}
return true;
};
};
// terser's order for a regular expression's flags; any it does not know follow.
const REGEXP_FLAG_ORDER = "dgimsuyv";
// A regular expression's `</script`, which would end an inline script element.
const REGEXP_SLASH_SCRIPT = /(<\s*\/\s*script)/i;
const REGEXP_STARTS_WITH_SCRIPT = /^\s*script/i;
/**
* @param {string} _match the whole match
* @param {string} slashScript the `</script` matched
* @returns {string} the same with its slash escaped
*/
const escapeSlashScript = (_match, slashScript) =>
slashScript.replace("/", "\\/");
/**
* @param {string[]} candidates the spellings
* @returns {string} the first shortest
*/
const bestOf = (candidates) => {
let best = candidates[0];
let length = best.length;
for (let i = 1; i < candidates.length; ++i) {
if (candidates[i].length < length) {
best = candidates[i];
length = best.length;
}
}
return best;
};
/**
* @param {number} num a number
* @returns {string} its shortest spelling
*/
const makeNum = (num) => {
// Every other spelling of an integer below 1000 is as long or longer.
if (num >= 0 && num < 1000 && Math.floor(num) === num) return `${num}`;
const str = num.toString(10).replace(/^0\./, ".").replace("e+", "e");
const candidates = [str];
if (Math.floor(num) === num) {
candidates.push(
num < 0
? `-0x${(-num).toString(16).toLowerCase()}`
: `0x${num.toString(16).toLowerCase()}`
);
}
/** @type {RegExpExecArray | null} */
let match;
if ((match = /^\.0+/.exec(str))) {
const length = match[0].length;
const digits = str.slice(length);
candidates.push(`${digits}e-${digits.length + length - 1}`);
} else if ((match = /0+$/.exec(str))) {
const length = match[0].length;
candidates.push(`${str.slice(0, -length)}e${length}`);
} else if ((match = /^(\d)\.(\d+)e(-?\d+)$/.exec(str))) {
candidates.push(
`${match[1]}${match[2]}e${Number(match[3]) - match[2].length}`
);
}
return bestOf(candidates);
};
// The largest denominator a quotient written for a number may have.
const QUOTIENT_DENOMINATORS = 99;
// WHY: measured on victory's font tables, `.4203125` gzips worse written
// `269/640`, and even `.5126315789473684` as `487/950`; only a full-precision
// literal a small denominator divides out, as `2/3` or `1/7`, wins after gzip.
const QUOTIENT_MIN_WRITTEN = 15;
/**
* A quotient of two integers that IEEE division reads back as the number
* exactly, such as `2/3` for `.6666666666666666`, where it is the shorter.
* @param {number} value a positive number
* @param {string} written the number's own spelling
* @param {number} extra what the quotient costs beside its text, as parentheses
* @returns {string | undefined} the quotient, none where none is shorter
*/
const quotientOf = (value, written, extra) => {
if (
written.length < QUOTIENT_MIN_WRITTEN ||
!(value > 0 && value < 1e6) ||
Math.floor(value) === value
) {
return;
}
for (
let denominator = 2;
denominator <= QUOTIENT_DENOMINATORS;
denominator++
) {
const numerator = Math.round(value * denominator);
if (numerator / denominator !== value) continue;
const quotient = `${makeNum(numerator)}/${denominator}`;
return quotient.length + extra < written.length ? quotient : undefined;
}
};
/**
* @param {string} flags a regular expression's flags
* @returns {string} the same, in terser's order
*/
const sortRegexpFlags = (flags) => {
const existing = new Set(flags);
let out = "";
for (const flag of REGEXP_FLAG_ORDER) {
if (existing.has(flag)) {
out += flag;
existing.delete(flag);
}
}
for (const flag of existing) out += flag;
return out;
};
/**
* terser's `print_property_name`, over the reserved words its parser reads.
* @param {TerserModules} modules terser's modules
* @returns {PrintPropertyName} the printer of a property's name
*/
const createPrintPropertyName = (modules) => {
const { ALL_RESERVED_WORDS } =
/** @type {{ ALL_RESERVED_WORDS: Set<string> }} */ (modules.parse);
const unicode = createUnicode();
/**
* @param {string} key the property name
* @param {string | undefined} quote the quote it was written with
* @param {OutputStream<PrintNode | number>} output the stream printed into
* @returns {boolean} whether it printed as a bare name
*/
const printPropertyName = (key, quote, output) => {
if (output.option("quote_keys")) {
output.print_string(key);
return false;
}
// eslint-disable-next-line eqeqeq
if (`${Number(key)}` == key && Number(key) >= 0) {
if (output.option("keep_numbers")) {
output.print(key);
return false;
}
output.print(
makeNum(/** @type {number} */ (/** @type {number | string} */ (key)))
);
return false;
}
const printString = ALL_RESERVED_WORDS.has(key)
? output.option("ie8")
: output.option("ecma") < 2015 || output.option("safari10")
? !unicode.isBasicIdentifier(key)
: !unicode.isIdentifierString(key, true);
if (printString || (quote && output.option("keep_quoted_props"))) {
output.print_string(key, quote);
return false;
}
output.print_name(key);
return true;
};
return printPropertyName;
};
/**
* terser's `Symbol` printer's name: the one mangling gave its definition, else
* the definition's own, a name nothing defines as written.
* @param {PrintIdentifier | PrintLiteral} node a name, or a quoted module name
* @returns {string} the name it prints
*/
const printedNameOf = (node) => {
const definition = /** @type {PrintIdentifier} */ (node).definition;
return definition ? definition.mangled_name || definition.name : node.name;
};
/** @typedef {(key: string, quote: string | undefined, output: OutputStream<PrintNode | number>) => boolean} PrintPropertyName prints a property's name, answering whether it printed as a bare name */
/** @typedef {(node: PrintNode, output: OutputStream) => void} EstreeCodegen prints one ESTree node's own text */
/** @typedef {(node: PrintNode, output: OutputStream) => boolean | undefined} EstreeParens whether an ESTree node is parenthesized where it is printed */
/**
* @template R
* @typedef {{ [Type in PrintNode["type"]]?: (node: PrintNodeOf<Type>, output: OutputStream) => R }} EstreeTable per ESTree type, what answers for a node of it
*/
/** @typedef {EstreeTable<void>} EstreeGenerators per ESTree type, what prints a node of it */
/** @typedef {EstreeTable<boolean | undefined>} EstreeParensTests per ESTree type, whether a node of it is parenthesized where it is printed */
/**
* A string's spelling made safe in an inline `<script>`, as `encode_string`
* makes a value: each escape added reads as what it replaces.
* @param {string} spelling the text between a string's quotes
* @returns {string} the same string, spelled so no `</script` or `<!--` shows
*/
const inlineScriptSpelling = (spelling) =>
spelling
.replace(/<\u002F(script)([>/\t\n\f\r ])/gi, "<\\/$1$2")
// An escaped `\<` gives its backslash up to the `\x3c` taking its place.
.replace(
/(\\*)\u003C!--/g,
(_match, slashes) =>
`${slashes.length % 2 === 1 ? slashes.slice(1) : slashes}\\x3c!--`
)
.replace(/--\u003E/g, "--\\x3e");
/**
* Installs `printEstree`, the printer over the compressed tree as it is held:
* a code generator and a test for its parentheses per ESTree type, each
* terser's. `minify` prints with it.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installEstreePrint = (modules) => {
/**
* @param {PrintParent} node a node
* @returns {node is PrintFunction} whether it is a function
*/
const isLambda = (node) =>
node.type === "FunctionExpression" ||
node.type === "ArrowFunctionExpression" ||
node.type === "FunctionDeclaration";
/**
* @param {PrintParent} node a node
* @returns {node is PrintMemberExpression} whether it is a member access
*/
const isPropAccess = (node) => node.type === "MemberExpression";
/**
* @param {PrintParent} node a node
* @returns {node is PrintCallExpression | PrintNewExpression} whether it is a call or a `new`
*/
const isCall = (node) =>
node.type === "CallExpression" || node.type === "NewExpression";
/**
* @param {string} type an ESTree type
* @returns {boolean} whether its nodes are terser's Scope: a program, function, class or static block
*/
const isScopeType = (type) =>
type === "Program" ||
type === "FunctionExpression" ||
type === "ArrowFunctionExpression" ||
type === "FunctionDeclaration" ||
type === "ClassDeclaration" ||
type === "ClassExpression" ||
type === "StaticBlock";
// terser's node classes the printer asks of a node, by name without `Node`;
// an Accessor (a method's value) reads as a Function, and a try's blocks as
// BlockStatements, where no site asks.
const kindOf = /** @type {PrintKinds} */ ({
Array: (node) => node.type === "ArrayExpression",
Arrow: (node) => node.type === "ArrowFunctionExpression",
Assign: (node) => node.type === "AssignmentExpression",
Await: (node) => node.type === "AwaitExpression",
Binary: (node) =>
(node.type === "BinaryExpression" &&
node.left.type !== "PrivateIdentifier") ||
node.type === "LogicalExpression" ||
node.type === "AssignmentExpression" ||
node.type === "AssignmentPattern",
BlockStatement: (node) => node.type === "BlockStatement",
Call: isCall,
Chain: (node) => node.type === "ChainExpression",
Class: (node) =>
node.type === "ClassDeclaration" || node.type === "ClassExpression",
ClassExpression: (node) => node.type === "ClassExpression",
Conditional: (node) => node.type === "ConditionalExpression",
DefaultAssign: (node) => node.type === "AssignmentPattern",
Definitions: (node) =>
node.type === "VariableDeclaration" &&
(node.kind === "var" || node.kind === "let" || node.kind === "const"),
DefinitionsLike: (node) => node.type === "VariableDeclaration",
Defun: (node) => node.type === "FunctionDeclaration",
Destructuring: (node) =>
node.type === "ArrayPattern" || node.type === "ObjectPattern",
Directive: (node) =>
node.type === "ExpressionStatement" && typeof node.directive === "string",
Do: (node) => node.type === "DoWhileStatement",
Dot: (node) =>
isPropAccess(node) &&
!node.computed &&
node.property.type === "Identifier",
DotHash: (node) =>
isPropAccess(node) && node.property.type === "PrivateIdentifier",
EmptyStatement: (node) => node.type === "EmptyStatement",
Expansion: (node) =>
node.type === "SpreadElement" || node.type === "RestElement",
Export: (node) =>
node.type === "ExportNamedDeclaration" ||
node.type === "ExportDefaultDeclaration" ||
node.type === "ExportAllDeclaration",
For: (node) => node.type === "ForStatement",
ForIn: (node) =>
node.type === "ForInStatement" || node.type === "ForOfStatement",
ForOf: (node) => node.type === "ForOfStatement",
Function: (node) => node.type === "FunctionExpression",
If: (node) => node.type === "IfStatement",
Import: (node) => node.type === "ImportDeclaration",
Lambda: isLambda,
New: (node) => node.type === "NewExpression",
Object: (node) => node.type === "ObjectExpression",
ObjectProperty: (node) =>
node.type === "Property" ||
node.type === "MethodDefinition" ||
node.type === "PropertyDefinition",
PrefixedTemplateString: (node) => node.type === "TaggedTemplateExpression",
PrivateIn: (node) =>
node.type === "BinaryExpression" &&
node.left.type === "PrivateIdentifier",
PropAccess: isPropAccess,
Return: (node) => node.type === "ReturnStatement",
Scope: (node) => isScopeType(node.type),
Sequence: (node) => node.type === "SequenceExpression",
SimpleStatement: (node) =>
node.type === "ExpressionStatement" && typeof node.directive !== "string",
StatementWithBody: (node) =>
node.type === "LabeledStatement" ||
node.type === "IfStatement" ||
node.type === "WithStatement" ||
node.type === "ForStatement" ||
node.type === "ForInStatement" ||
node.type === "ForOfStatement" ||
node.type === "WhileStatement" ||
node.type === "DoWhileStatement",
Sub: (node) => isPropAccess(node) && node.computed === true,
TemplateSegment: (node) => node.type === "TemplateElement",
Unary: (node) =>
node.type === "UnaryExpression" || node.type === "UpdateExpression",
UnaryPostfix: (node) => node.type === "UpdateExpression" && !node.prefix,
UnaryPrefix: (node) =>
node.type === "UnaryExpression" ||
(node.type === "UpdateExpression" && node.prefix),
Var: (node) => node.type === "VariableDeclaration" && node.kind === "var",
VarDefLike: (node) => node.type === "VariableDeclarator",
Yield: (node) => node.type === "YieldExpression"
});
modules.kindOf = kindOf;
const { ALL_RESERVED_WORDS, PRECEDENCE } =
/** @type {{ ALL_RESERVED_WORDS: Set<string>, PRECEDENCE: Record<string, number> }} */ (
modules.parse
);
const regexpSourceFix = /** @type {(source: string) => string} */ (
modules.utils.regexp_source_fix
);
const { getFullChar, isIdentifierCharacter, isIdentifierString } =
createUnicode();
const printPropertyName = createPrintPropertyName(modules);
/**
* A private member's name, which terser prints as a property's and so quotes
* where it does not take it for an identifier; the correct phase never does.
* @param {string} name the name, without its `#`
* @param {string | undefined} quote the quote its key was written with
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printPrivateName = (name, quote, output) => {
output.print("#");
const { corrections } = modules;
if (corrections !== undefined && corrections.enabled) {
output.print_name(name);
} else {
printPropertyName(name, quote, output);
}
};
// What `parentOf` answers past the root: a node no type test matches.
/** @type {PrintParent} */
const NO_PARENT = { type: "", startToken: null, endToken: null };
/**
* @param {OutputStream} output the stream printed into
* @param {number} level how many levels further out than the parent
* @returns {PrintParent} the node holding the one printed, or `NO_PARENT`
*/
const parentOf = (output, level) => output.parent(level) || NO_PARENT;
/** @type {((node: PrintNode) => void) | undefined} */
let onPrinted;
/** @type {MangleRecording | null} */
let recording = null;
/** @type {MangleOptions | null} */
let recordingOptions = null;
// Whether a kept name is a piece too: a global `keep_*` regexp answers per
// call, and a name cache renames the globals the mangler would keep.
let recordingKept = false;
/**
* Records a piece of text just printed that mangling rewrites.
* @param {OutputStream} output the stream printed into
* @param {number} length how long the piece is
* @param {NamedDefinition} target the definition whose name replaces it
* @param {boolean} counted whether the frequency count leaves its name out
* @returns {void}
*/
const recordPiece = (output, length, target, counted) => {
recordPieceAt(
/** @type {MangleRecording} */ (recording),
output.pos() - length,
counted ? -length : length,
target
);
};
/**
* Records an import's or export's `as` clause, whose final shape mangling
* decides, as a piece.
* @param {number} start where the clause starts in the text
* @param {number} length how long it is, 0 where the names matched
* @param {NamedDefinition} local the local name's definition
* @param {PrintNode} foreign the other module's name
* @param {OutputStream} output the stream printed into
* @param {boolean} isImport whether the clause precedes the local name
* @returns {number} which piece it is
*/
const recordSpecifier = (start, length, local, foreign, output, isImport) => {
const index = recordPieceAt(
/** @type {MangleRecording} */ (recording),
start,
length,
local
);
const name = moduleName(foreign);
/** @type {MangleRecording} */ (recording).specifiers.push({
index,
foreign: name,
text:
foreign.type === "Literal"
? output.encode_string(foreign.name, foreign.quote)
: name,
isImport
});
return index;
};
/**
* An import's or export's `as` clause as the stream would print it, the
* spaces around `as` as `print` decides them.
* @param {OutputStream} output the stream printed into
* @param {string} local the local name, as mangling left it
* @param {MangleSpecifier} specifier the clause recorded
* @returns {string} the clause
*/
const asClause = (output, local, specifier) => {
const { text } = specifier;
if (specifier.isImport) {
return isIdentifierCharacter(text[text.length - 1])
? `${text} as `
: `${text}as `;
}
const printed = output.identifierToUtf8(local, true);
const before = isIdentifierCharacter(printed[printed.length - 1])
? " as"
: "as";
return isIdentifierCharacter(getFullChar(text, 0)) || text[0] === "\\"
? `${before} ${text}`
: `${before}${text}`;
};
/**
* Prints a node with what terser printed around it: its parentheses, the
* parent stack `output.parent(n)` reads, and the scope `use asm` applies to.
* @param {PrintNode} node the node
* @param {OutputStream} output the stream printed into
* @param {boolean=} forceParens whether to parenthesize it whatever its parent
* @returns {void}
*/
const printEstree = (node, output, forceParens) => {
// Read once: nodes come in many shapes, so each read is a megamorphic load.
const { type } = node;
const generate = /** @type {EstreeCodegen | undefined} */ (codegen[type]);
if (generate === undefined) {
throw new Error(`printEstree cannot print a ${type} node yet`);
}
if (isScopeType(type)) {
output.active_scope = node;
} else if (
type === "ExpressionStatement" &&
!output.use_asm &&
node.directive === "use asm"
) {
output.use_asm = output.active_scope;
}
const stack = output.stack;
stack.push(node);
const parens = /** @type {EstreeParens | undefined} */ (needsParens[type]);
const parenthesized =
forceParens === true ||
(parens !== undefined && parens(node, output) === true);
if (parenthesized) output.print("(");
output.prepend_comments(node);
// A mapping adds nothing but through `add_mapping`, which needs a map.
if (output.sourceMap) {
const map = /** @type {EstreeCodegen | undefined} */ (
sourceMapping[type]
);
if (map !== undefined) map(node, output);
}
generate(node, output);
output.append_comments(node);
if (parenthesized) output.print(")");
stack.pop();
if (node === output.use_asm) output.use_asm = null;
if (onPrinted !== undefined) onPrinted(node);
};
/**
* @param {PrintParent} node a binary-like node, as `kindOf.Binary` finds one
* @returns {string} its operator, which an assignment pattern leaves implicit
*/
const operatorOf = (node) =>
node.type === "AssignmentPattern"
? "="
: /** @type {PrintBinary} */ (node).operator;
const { isSymbolNode, isSymbolPropertyNode } = modules.ast;
/**
* @param {PrintNode} key a key not computed, or a private name
* @returns {string} the name terser holds for it
*/
const keyName = (key) =>
/** @type {PrintIdentifier | PrintPrivateIdentifier} */ (key).name;
/**
* The name a property's source mapping carries, as terser's `add_mapping`
* reads it off the key, unless renaming set it.
* @param {PrintProperty | PrintPropertyDefinition} member a property not a method
* @param {TokenTables=} tables the tables its token numbers name
* @returns {string | null} the name, null for none
*/
const mapNameOf = (member, tables = NO_TOKEN_TABLES) => {
const { mapName, key } = member;
if (mapName !== undefined) return mapName;
if (isSymbolPropertyNode(key)) return keyName(key);
if (!isSymbolNode(key)) return null;
const start = tables.of(member.startToken);
return start && start.type === "string" ? start.value : keyName(key);
};
modules.mapNameOf = mapNameOf;
/**
* terser's `add_source_map` of most nodes: the node's start.
* @param {PrintNode} node the node printed
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const mapStart = (node, output) => {
output.add_mapping(node.startToken);
};
/**
* terser's `add_source_map` of a getter, setter or method, whose key maps the name.
* @param {PrintNode} node the method printed
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const mapMethodStart = (node, output) => {
output.add_mapping(node.startToken, false);
};
/**
* terser's `add_source_map` of a property, named by its key (none when computed).
* @param {PrintProperty | PrintPropertyDefinition} node the property printed
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const mapProperty = (node, output) => {
const mapName = mapNameOf(node, output.tokenTables);
output.add_mapping(node.startToken, mapName === null ? undefined : mapName);
};
/**
* terser's `add_source_map` of a method's or private property's name: its end.
* @param {PrintNode} key the name
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const mapNameEnd = (key, output) => {
const end = key.endToken;
const tokenType = end && end.type;
if (tokenType === "name" || tokenType === "privatename") {
output.add_mapping(end, keyName(key));
} else {
output.add_mapping(end);
}
};
/**
* Whether a quotient prints bare as a node's value: whether what holds it
* binds no tighter than `/` and reads no other operand after it.
* @param {PrintNode} node the number
* @param {PrintParent} parent what holds it
* @returns {boolean} true where no parentheses are needed
*/
const quotientPrintsBare = (node, parent) => {
switch (parent.type) {
case "BinaryExpression":
case "LogicalExpression": {
const { operator } = parent;
if (operator === "**") return false;
const outer = PRECEDENCE[operator];
const own = PRECEDENCE["/"];
return outer < own || (outer === own && parent.left === node);
}
case "UnaryExpression":
// `-2/3` divides `-2`, which is the same number negated.
return parent.operator === "-" || parent.operator === "+";
case "CallExpression":
case "NewExpression":
return parent.callee !== node;
case "MemberExpression":
return parent.computed && parent.property === node;
case "VariableDeclarator":
return parent.init === node;
case "AssignmentExpression":
return parent.right === node;
case "Property":
return parent.value === node;
case "ArrayExpression":
case "ConditionalExpression":
case "ExpressionStatement":
case "ReturnStatement":
case "SequenceExpression":
case "SwitchCase":
case "TemplateLiteral":
return true;
default:
return false;
}
};
/**
* Prints a number by its shortest spelling, under the improvements a
* quotient where one reads back as the number exactly.
* @param {PrintNode} node the number's literal
* @param {number} value the number
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printNumber = (node, value, output) => {
const written = makeNum(value);
const { improvements } = modules;
if (improvements === undefined || !improvements.enabled) {
output.print(written);
return;
}
const bare = quotientPrintsBare(node, parentOf(output, 0));
const quotient = quotientOf(value, written, bare ? 0 : 2);
if (quotient === undefined) output.print(written);
else if (bare) output.print(quotient);
else output.print(`(${quotient})`);
};
// Each ESTree type's mapping, as terser's class maps it; a type absent here
// is one terser maps nowhere, its children mapping themselves.
/** @type {EstreeGenerators} */
const sourceMapping = {
ArrayExpression: mapStart,
ArrowFunctionExpression: mapStart,
BlockStatement: (node, output) => {
// terser's try block is no BlockStatement, and maps nowhere.
const parent = parentOf(output, 0);
if (parent.type !== "TryStatement" || parent.block !== node) {
mapStart(node, output);
}
},
BreakStatement: mapStart,
CatchClause: mapStart,
ClassDeclaration: mapStart,
ClassExpression: mapStart,
ContinueStatement: mapStart,
DebuggerStatement: mapStart,
DoWhileStatement: mapStart,
ExpressionStatement: (node, output) => {
// A directive maps; terser's SimpleStatement does not.
if (typeof node.directive === "string") mapStart(node, output);
},
ForInStatement: mapStart,
ForOfStatement: mapStart,
ForStatement: mapStart,
FunctionDeclaration: mapStart,
FunctionExpression: mapStart,
Identifier: mapStart,
IfStatement: mapStart,
Literal: mapStart,
MethodDefinition: mapMethodStart,
NewExpression: mapStart,
ObjectExpression: mapStart,
PrivateIdentifier: mapNameEnd,
Property: (node, output) => {
if (node.kind === "init" && !node.method) mapProperty(node, output);
else mapMethodStart(node, output);
},
PropertyDefinition: mapProperty,
ReturnStatement: mapStart,
Super: mapStart,
SwitchCase: mapStart,
SwitchStatement: mapStart,
TemplateElement: mapStart,
TemplateLiteral: mapStart,
ThisExpression: mapStart,
ThrowStatement: mapStart,
TryStatement: mapStart,
VariableDeclaration: mapStart,
WhileStatement: mapStart,
WithStatement: mapStart
};
/**
* @param {PrintParent} node a node
* @returns {boolean} whether it is terser's `Dot` or `Sub`, not a private access
*/
const isDotOrSub = (node) =>
node.type === "MemberExpression" &&
node.property.type !== "PrivateIdentifier";
/**
* @param {PrintNode} node a node
* @returns {node is PrintLiteral & { value: string }} whether it is a string literal, not a bigint's digits
*/
const isStringLiteral = (node) =>
node.type === "Literal" &&
typeof node.value === "string" &&
node.role !== "bigint";
/**
* @param {PrintParent} node a node
* @returns {node is PrintLiteral & { value: number }} whether it is a number literal
*/
const isNumber = (node) =>
node.type === "Literal" && typeof node.value === "number";
/**
* terser's `first_in_statement`, over the stream's parent stack.
* @param {OutputStream} output the stream printed into
* @param {boolean=} exportDefault whether an `export default` value it starts, but is not, counts
* @returns {boolean | undefined} whether the node printed starts its statement
*/
const firstInStatement = (output, exportDefault) => {
let node = /** @type {PrintParent} */ (output.parent(-1));
/** @type {PrintParent | undefined} */
let parent;
for (let i = 0; (parent = output.parent(i)); i++) {
if (parent.type === "ExpressionStatement" && parent.expression === node) {
return true;
}
if (
exportDefault &&
i > 0 &&
parent.type === "ExportDefaultDeclaration" &&
parent.exported_value === node
) {
return true;
}
if (
(parent.type === "SequenceExpression" &&
parent.expressions[0] === node) ||
(parent.type === "CallExpression" && parent.callee === node) ||
(parent.type === "TaggedTemplateExpression" && parent.tag === node) ||
(isDotOrSub(parent) &&
/** @type {PrintMemberExpression} */ (parent).object === node) ||
(parent.type === "ChainExpression" && parent.expression === node) ||
(parent.type === "ConditionalExpression" && parent.test === node) ||
(kindOf.Binary(parent) &&
/** @type {PrintBinaryLike} */ (parent).left === node) ||
(kindOf.UnaryPostfix(parent) &&
/** @type {PrintUnary} */ (parent).argument === node)
) {
node = parent;
} else {
return false;
}
}
return undefined;
};
/**
* terser's `left_is_object`.
* @param {PrintNode} node an expression
* @returns {boolean} whether its leftmost part is an object literal
*/
const leftIsObject = (node) => {
switch (node.type) {
case "ObjectExpression":
return true;
case "SequenceExpression":
return leftIsObject(node.expressions[0]);
case "CallExpression":
return leftIsObject(node.callee);
case "TaggedTemplateExpression":
return leftIsObject(node.tag);
case "MemberExpression":
return isDotOrSub(node) && leftIsObject(node.object);
case "ChainExpression":
return leftIsObject(node.expression);
case "ConditionalExpression":
return leftIsObject(node.test);
case "UpdateExpression":
return !node.prefix && leftIsObject(node.argument);
}
return (
kindOf.Binary(node) &&
leftIsObject(/** @type {PrintBinaryLike} */ (node).left)
);
};
/**
* terser's `walk` stopped by `walk_abort`, over an ESTree tree.
* @param {PrintNode} node the node walked from
* @param {(node: PrintNode) => boolean | undefined} test true where found, false to skip a node's children
* @returns {boolean} whether `test` found a node
*/
const findNode = (node, test) => {
const found = test(node);
if (found !== undefined) return found;
const keys =
node.type === "CallExpression" || node.type === "NewExpression"
? CALL_WALK_KEYS
: ESTREE_RENAMING_CHILD_KEYS[node.type];
// A name or literal holds none.
if (keys === undefined) return false;
const fields = /** @type {PrintFields} */ (
/** @type {PrintFields | PrintNode} */ (node)
);
for (let i = 0; i < keys.length; i++) {
const value = fields[keys[i]];
if (value === null || value === undefined) continue;
if (!Array.isArray(value)) {
if (findNode(value, test)) return true;
continue;
}
for (let j = 0; j < value.length; j++) {
const child = value[j];
if (child && findNode(child, test)) return true;
}
}
return false;
};
/**
* What terser's walk for `new (a.b())` stops at: a call or `new` outside
* any function or class.
* @param {PrintNode} node a node walked
* @returns {boolean | undefined} true for a call, false for a scope
*/
const findCall = (node) => {
if (kindOf.Scope(node)) return false;
if (kindOf.Call(node)) return true;
};
/**
* What terser's `parenthesize_for_noin` stops at: an `in` outside any
* function or class, arrows aside.
* @param {PrintNode} node a node walked
* @returns {boolean | undefined} true for an `in`, false for a scope
*/
const findIn = (node) => {
if (kindOf.Scope(node) && node.type !== "ArrowFunctionExpression") {
return false;
}
if (node.type === "BinaryExpression" && node.operator === "in") return true;
};
/**
* terser's `parenthesize_for_noin`.
* @param {PrintNode} node the node printed
* @param {OutputStream} output the stream printed into
* @param {boolean} noin whether `in` would be read as a `for` loop's
* @returns {void}
*/
const parenthesizeForNoin = (node, output, noin) => {
printEstree(node, output, noin && findNode(node, findIn));
};
/**
* @param {PrintNode} statement a statement
* @returns {boolean} whether it is a string standing alone, not a directive
*/
const isStringStatement = (statement) =>
statement.type === "ExpressionStatement" &&
typeof statement.directive !== "string" &&
isStringLiteral(statement.expression);
/**
* terser's `display_body`.
* @param {PrintNode[]} statements the statements
* @param {boolean} isToplevel whether they are the program's
* @param {OutputStream} output the stream printed into
* @param {boolean} allowDirectives whether leading strings are directives
* @returns {void}
*/
const displayBody = (statements, isToplevel, output, allowDirectives) => {
const last = statements.length - 1;
output.in_directive = allowDirectives;
for (let i = 0; i <= last; i++) {
const statement = statements[i];
const isEmpty = statement.type === "EmptyStatement";
if (
output.in_directive === true &&
!(
kindOf.Directive(statement) ||
isEmpty ||
isStringStatement(statement)
)
) {
output.in_directive = false;
}
if (!isEmpty) {
output.indent();
printEstree(statement, output);
if (!(i === last && isToplevel)) {
output.newline();
if (isToplevel) output.newline();
}
} else if (
modules.corrections !== undefined &&
modules.corrections.enabled
) {
// terser drops an empty statement's comments with it.
output.prepend_comments(statement);
}
if (output.in_directive === true && isStringStatement(statement)) {
output.in_directive = false;
}
}
output.in_directive = false;
};
/**
* terser's `print_braced`.
* @param {PrintNode} owner the node the braces belong to
* @param {PrintNode[]} statements the body's statements
* @param {OutputStream} output the stream printed into
* @param {boolean} allowDirectives whether leading strings are directives
* @returns {void}
*/
const printBraced = (owner, statements, output, allowDirectives) => {
if (statements.length === 0) {
printBracedEmpty(owner, output);
return;
}
output.print("{");
displayBody(statements, false, output, allowDirectives);
output.add_mapping(owner.endToken);
output.print("}");
};
/**
* terser's `make_block`: a statement in braces.
* @param {PrintNode} statement the statement
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const makeBlock = (statement, output) => {
if (statement.type === "EmptyStatement") {
output.print("{}");
} else if (statement.type === "BlockStatement") {
printEstree(statement, output);
} else {
output.print("{");
output.indent();
printEstree(statement, output);
output.newline();
output.print("}");
}
};
/**
* terser's `force_statement`: a statement's body, braced where it must be;
* its `braces` option is never set, as the stream resets it.
* @param {PrintNode} statement the body
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printMaybeBracedBody = (statement, output) => {
if (statement.type === "EmptyStatement") {
output.force_semicolon();
} else if (
(statement.type === "VariableDeclaration" && statement.kind !== "var") ||
kindOf.Class(statement)
) {
makeBlock(statement, output);
} else {
printEstree(statement, output);
}
};
/**
* terser's `make_then`: an `if`'s consequent before an `else`, braced
* where an `if` without one inside would take that `else`.
* @param {PrintIfStatement} node the `if`
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const makeThen = (node, output) => {
const { consequent } = node;
if (output.option("ie8") && consequent.type === "DoWhileStatement") {
makeBlock(consequent, output);
return;
}
let body = consequent;
for (;;) {
if (body.type === "IfStatement") {
if (!body.alternate) {
makeBlock(consequent, output);
return;
}
body = body.alternate;
} else if (kindOf.StatementWithBody(body)) {
body = body.body;
} else {
break;
}
}
printMaybeBracedBody(consequent, output);
};
/**
* terser's `gc_scope`: drops a printed function's body under `_destroy_ast`.
* @param {PrintFunction} node the function
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const gcScope = (node, output) => {
if (!output.option("_destroy_ast")) return;
node.body.body.length = 0;
node.params.length = 0;
};
/**
* terser's `Exit._do_print`: a `return` or `throw`.
* @param {PrintExitStatement} node the statement
* @param {string} keyword its keyword
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printExit = (node, keyword, output) => {
output.print(keyword);
const { argument } = node;
if (argument) {
output.space();
// Comments not yet printed may hold a line break before the value.
/** @type {Comment[] | undefined} */
const comments = output.tokenTables.commentsOf(
argument.startToken,
false
);
const parenthesized =
comments &&
comments.length > 0 &&
!output.printed_comments.has(comments);
if (parenthesized) output.print("(");
// A string print reads no comment, so it parenthesizes the value
// whose comments were dropped here: what it counts differs.
const diverges =
recording !== null && !parenthesized && comments && comments.length > 0;
const before = diverges ? output.pos() : 0;
printEstree(argument, output);
if (parenthesized) output.print(")");
if (diverges) {
const recorded = /** @type {MangleRecording} */ (recording);
recorded.counted.push("()");
if (output.codeAt(before) === 32) recorded.uncounted.push(" ");
}
}
output.semicolon();
};
/**
* terser's `LoopControl._do_print`: a `break` or `continue`.
* @param {PrintLoopControl} node the statement
* @param {string} keyword its keyword
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printLoopControl = (node, keyword, output) => {
output.print(keyword);
if (node.label) {
output.space();
printEstree(node.label, output);
}
output.semicolon();
};
/**
* A parenthesized expression, as `with_parens` prints it.
* @param {PrintNode} node the expression
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printParenthesized = (node, output) => {
output.print("(");
printEstree(node, output);
output.print(")");
};
/**
* @param {PrintNode} node an import's or export's name, a literal where quoted
* @returns {string} the name
*/
const moduleName = (node) =>
printedNameOf(/** @type {PrintIdentifier | PrintLiteral} */ (node));
/**
* terser's `print_mapped_name`: the local side of an import or export.
* @param {PrintNode} node the name
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printMappedName = (node, output) => {
if (node.type === "Literal") output.print_string(node.name, node.quote);
else printEstree(node, output);
};
/**
* terser's `print_foreign_name`: the other module's side, printed as written.
* @param {PrintNode} node the name
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printForeignName = (node, output) => {
if (node.type === "Literal") output.print_string(node.name, node.quote);
else output.print(printedNameOf(/** @type {PrintIdentifier} */ (node)));
};
/**
* terser's `NameMapping` printer for a named specifier; `export * as "*"`,
* whose two names differ by their quotes alone, prints apart.
* @param {PrintNode} local the local name
* @param {PrintNode} foreign the other module's name
* @param {boolean} isImport whether it is an import's
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printNameMapping = (local, foreign, isImport, output) => {
const same = moduleName(local) === moduleName(foreign);
if (
recording === null ||
local.type !== "Identifier" ||
!isMinifierVariable(local.definition) ||
(!recordingKept &&
isUnmangleable(
local.definition,
/** @type {MangleOptions} */ (recordingOptions),
null
))
) {
if (same) {
printMappedName(local, output);
return;
}
if (isImport) printForeignName(foreign, output);
else printMappedName(local, output);
output.space();
output.print("as");
output.space();
if (isImport) printEstree(local, output);
else printForeignName(foreign, output);
return;
}
// Mangling decides whether the names still match: the clause is a
// piece the patch writes or drops.
const start = output.pos();
if (isImport) {
if (!same) {
printForeignName(foreign, output);
output.space();
output.print("as");
output.space();
}
const index = recordSpecifier(
start,
0,
local.definition,
foreign,
output,
true
);
printEstree(local, output);
// The local name's piece follows; the space before it is the clause's.
const recorded = recording;
recorded.lengths[index] = recorded.starts[index + 1] - start;
return;
}
printEstree(local, output);
const clauseStart = output.pos();
if (!same) {
output.space();
output.print("as");
output.space();
printForeignName(foreign, output);
}
recordSpecifier(
clauseStart,
output.pos() - clauseStart,
local.definition,
foreign,
output,
false
);
};
/**
* Named imports or exports in braces.
* @param {PrintNode[]} specifiers the specifiers
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printSpecifiers = (specifiers, output) => {
output.print("{");
for (let i = 0; i < specifiers.length; i++) {
output.space();
printEstree(specifiers[i], output);
if (i < specifiers.length - 1) output.print(",");
}
output.space();
output.print("}");
};
/**
* An import's or export's source and attributes, past the specifiers.
* @param {PrintModuleDeclaration} node the declaration
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printSource = (node, output) => {
if (node.source) {
output.space();
output.print("from");
output.space();
printEstree(node.source, output);
}
printAttributes(node, output);
output.semicolon();
};
/**
* Import attributes, printed as the object terser holds them in.
* @param {PrintModuleDeclaration} node the import or export
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printAttributes = (node, output) => {
const { attributes } = node;
if (!attributes || attributes.properties.length === 0) return;
output.print("with");
printEstree(attributes, output);
};
/**
* terser's `Class` printer.
* @param {PrintClass} node a class declaration or expression
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printClass = (node, output) => {
output.print("class");
output.space();
if (node.id) {
printEstree(node.id, output);
output.space();
}
const { superClass } = node;
if (superClass) {
const parens =
superClass.type !== "Identifier" &&
superClass.type !== "MemberExpression" &&
superClass.type !== "ClassExpression" &&
superClass.type !== "FunctionExpression";
output.print("extends");
if (parens) output.print("(");
else output.space();
printEstree(superClass, output);
if (parens) output.print(")");
else output.space();
}
const members = node.body.body;
if (members.length === 0) {
output.print("{}");
return;
}
output.print("{");
for (let i = 0; i < members.length; i++) {
if (i) output.newline();
output.indent();
printEstree(members[i], output);
}
output.newline();
output.print("}");
};
/**
* terser's `Lambda` printer: a function not a method.
* @param {PrintFunction} node the function declaration or expression
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printFunction = (node, output) => {
printLambda(node, output, false);
gcScope(node, output);
};
/**
* terser's `ForIn` printer, `for … of` among them.
* @param {PrintForInStatement | PrintForOfStatement} node the loop
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printForIn = (node, output) => {
output.print("for");
if (/** @type {PrintForOfStatement} */ (node).await) {
output.space();
output.print("await");
}
output.space();
output.print("(");
printEstree(node.left, output);
output.space();
output.print(node.type === "ForOfStatement" ? "of" : "in");
output.space();
printEstree(node.right, output);
output.print(")");
output.space();
printMaybeBracedBody(node.body, output);
};
/**
* terser's `print_braced_empty`.
* @param {PrintNode} owner the node the braces belong to
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printBracedEmpty = (owner, output) => {
output.print("{");
output.append_comments(owner, true);
output.add_mapping(owner.endToken);
output.print("}");
};
/**
* terser's `Lambda._do_print`.
* @param {PrintFunction} node the function
* @param {OutputStream} output the stream printed into
* @param {boolean} isMethod whether it is a method's value, printed without `function`
* @returns {void}
*/
const printLambda = (node, output, isMethod) => {
if (!isMethod) {
if (node.async) {
output.print("async");
output.space();
}
output.print("function");
if (node.generator) output.star();
if (node.id) output.space();
}
if (node.id) printEstree(node.id, output);
printArguments(node.params, output);
output.space();
printBraced(node, node.body.body, output, true);
};
/**
* @param {(PrintNode | null)[]} list the arguments or parameters
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printArguments = (list, output) => {
output.print("(");
for (let i = 0; i < list.length; i++) {
if (i) output.comma();
printEstree(/** @type {PrintNode} */ (list[i]), output);
}
output.print(")");
};
/**
* terser's `_print_getter_setter`: a getter, setter or method.
* @param {PrintProperty | PrintMethodDefinition} node the property or method definition
* @param {string | undefined} keyword what precedes its name
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printMethod = (node, keyword, output) => {
if (/** @type {PrintMethodDefinition} */ (node).static) {
output.print("static");
output.space();
}
if (keyword) {
output.print(keyword);
output.space();
}
const { key } = node;
const isPrivate = key.type === "PrivateIdentifier";
if (node.computed && !isPrivate) {
output.print("[");
printEstree(key, output);
output.print("]");
} else {
if (isPrivate) {
printPrivateName(keyName(key), node.quote, output);
} else {
printPropertyName(keyName(key), node.quote, output);
}
mapNameEnd(key, output);
}
printLambda(/** @type {PrintFunction} */ (node.value), output, true);
};
/**
* @param {PrintProperty | PrintMethodDefinition} node a method's or property's
* @returns {string | undefined} the keyword its kind prints before its name
*/
const methodKeyword = (node) => {
if (node.kind === "get" || node.kind === "set") return node.kind;
const value = /** @type {PrintFunction} */ (node.value);
if (value.generator && value.async) return "async*";
if (value.generator) return "*";
if (value.async) return "async";
};
/**
* terser's `ObjectKeyVal` printer: a property not a method.
* @param {PrintProperty} node the property
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printKeyValue = (node, output) => {
const { key, value } = node;
const name = node.computed ? undefined : keyName(key);
const tryShorthand = /** @type {boolean} */ (
output.option("shorthand") && name !== undefined
);
if (
tryShorthand &&
value.type === "Identifier" &&
value.atom !== true &&
printedNameOf(value) === name &&
!ALL_RESERVED_WORDS.has(name)
) {
if (!printPropertyName(name, node.quote, output)) {
output.colon();
printEstree(value, output);
}
} else if (
tryShorthand &&
value.type === "AssignmentPattern" &&
value.left.type === "Identifier" &&
printedNameOf(value.left) === name
) {
if (!printPropertyName(name, node.quote, output)) {
output.colon();
printEstree(value.left, output);
}
output.space();
output.print("=");
output.space();
printEstree(value.right, output);
} else {
if (name !== undefined) {
printPropertyName(name, node.quote, output);
} else {
output.print("[");
printEstree(key, output);
output.print("]");
}
output.colon();
printEstree(value, output);
}
};
/**
* A pattern's or array's elements, a final hole followed by a comma of
* its own lest it read as a trailing comma.
* @param {(PrintNode | null)[]} elements the elements, null for a hole
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printElements = (elements, output) => {
const last = elements.length - 1;
for (let i = 0; i <= last; i++) {
const element = elements[i];
if (i) output.comma();
if (element) printEstree(element, output);
else if (i === last) output.comma();
}
};
/**
* terser's `Binary` printer.
* @param {PrintBinaryLike} node a binary, logical or assignment expression, or an assignment pattern
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printBinary = (node, output) => {
const operator = operatorOf(node);
printEstree(node.left, output);
// `>`, `>=`, `>>` or `>>>` straight after `--` would read as `-->`.
if (operator[0] === ">" && output.last().endsWith("--")) {
output.print(" ");
} else {
output.space();
}
output.print(operator);
output.space();
printEstree(node.right, output);
};
/**
* terser's `Call` printer.
* @param {PrintCallExpression | PrintNewExpression} node a call or `new`
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printCall = (node, output) => {
const { callee } = node;
printEstree(callee, output);
if (node.type === "NewExpression" && node.arguments.length === 0) return;
if (kindOf.Call(callee) || kindOf.Lambda(callee)) {
output.add_mapping(node.startToken);
}
if (/** @type {PrintCallExpression} */ (node).optional) output.print("?.");
printArguments(node.arguments, output);
};
/**
* terser's `UnaryPrefix` and `UnaryPostfix` printers.
* @param {PrintUnary} node a unary or update expression
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printUnary = (node, output) => {
const { argument, operator } = node;
if (!node.prefix) {
printEstree(argument, output);
output.print(operator);
return;
}
// Lest it print `<!--`.
if (operator === "--" && output.last().endsWith("!")) output.print(" ");
output.print(operator);
if (
/^[a-z]/i.test(operator) ||
(/[+-]$/.test(operator) &&
kindOf.UnaryPrefix(argument) &&
/^[+-]/.test(/** @type {PrintUnary} */ (argument).operator))
) {
output.space();
}
printEstree(argument, output);
};
/**
* terser's `RegExp` printer.
* @param {PrintLiteral} node a regular expression literal
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printRegExp = (node, output) => {
const value = /** @type {RegExp} */ (node.value);
let source = regexpSourceFix(value.source);
const flags = value.flags ? sortRegexpFlags(value.flags) : "";
source = source.replace(REGEXP_SLASH_SCRIPT, escapeSlashScript);
if (REGEXP_STARTS_WITH_SCRIPT.test(source) && output.last().endsWith("<")) {
output.print(" ");
}
output.print(output.to_utf8(`/${source}/${flags}`, false, true));
const parent = parentOf(output, 0);
if (
kindOf.Binary(parent) &&
/^\w/.test(operatorOf(parent)) &&
/** @type {PrintBinaryLike} */ (parent).left === node
) {
output.print(" ");
}
};
/**
* A template's text between two of its expressions.
* @param {string} text the text, cooked unless the template is tagged
* @param {boolean} isTagged whether a tag reads it, so it prints raw
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
const printTemplateText = (text, isTagged, output) => {
if (isTagged) output.print(text);
else output.print_template_string_chars(text);
};
/** @type {EstreeGenerators} */
const codegen = {
Program: (node, output) => {
displayBody(node.body, true, output, true);
output.print("");
},
ExpressionStatement: (node, output) => {
if (typeof node.directive === "string") {
const expression = /** @type {PrintLiteral} */ (node.expression);
const value = /** @type {string} */ (expression.value);
// A directive spelled with escapes is printed as spelled: encoding
// its value instead could spell another directive, or none.
if (node.directive !== value) {
// A spelling is only ever read with the quote it opens with.
const quote = /** @type {string} */ (expression.quote);
const spelled = output.option("inline_script")
? inlineScriptSpelling(node.directive)
: node.directive;
output.print(`${quote}${spelled}${quote}`);
} else {
output.print_string(value, expression.quote);
}
} else {
printEstree(node.expression, output);
}
output.semicolon();
},
BlockStatement: (node, output) => {
const parent = parentOf(output, 0);
// terser's `Finally` is the block, so prints its keyword too.
if (parent.type === "TryStatement" && parent.finalizer === node) {
output.print("finally");
output.space();
}
printBraced(node, node.body, output, false);
},
EmptyStatement: (node, output) => {
output.semicolon();
},
DebuggerStatement: (node, output) => {
output.print("debugger");
output.semicolon();
},
LabeledStatement: (node, output) => {
printEstree(node.label, output);
output.colon();
const { corrections } = modules;
// Without `semicolons`, terser defers an empty body's `;` to a line
// break, which leaves the label with no statement.
if (
node.body.type === "EmptyStatement" &&
corrections !== undefined &&
corrections.enabled
) {
output.force_semicolon();
} else {
printEstree(node.body, output);
}
},
DoWhileStatement: (node, output) => {
output.print("do");
output.space();
makeBlock(node.body, output);
output.space();
output.print("while");
output.space();
printParenthesized(node.test, output);
output.semicolon();
},
WhileStatement: (node, output) => {
output.print("while");
output.space();
printParenthesized(node.test, output);
output.space();
printMaybeBracedBody(node.body, output);
},
ForStatement: (node, output) => {
const { init, test, update } = node;
output.print("for");
output.space();
output.print("(");
if (init) {
if (init.type === "VariableDeclaration") printEstree(init, output);
else parenthesizeForNoin(init, output, true);
output.print(";");
output.space();
} else {
output.print(";");
}
if (test) {
printEstree(test, output);
output.print(";");
output.space();
} else {
output.print(";");
}
if (update) printEstree(update, output);
output.print(")");
output.space();
printMaybeBracedBody(node.body, output);
},
ForInStatement: printForIn,
ForOfStatement: printForIn,
WithStatement: (node, output) => {
output.print("with");
output.space();
printParenthesized(node.object, output);
output.space();
printMaybeBracedBody(node.body, output);
},
ReturnStatement: (node, output) => {
printExit(node, "return", output);
},
ThrowStatement: (node, output) => {
printExit(node, "throw", output);
},
BreakStatement: (node, output) => {
printLoopControl(node, "break", output);
},
ContinueStatement: (node, output) => {
printLoopControl(node, "continue", output);
},
IfStatement: (node, output) => {
const { alternate } = node;
output.print("if");
output.space();
printParenthesized(node.test, output);
output.space();
if (!alternate) {
printMaybeBracedBody(node.consequent, output);
return;
}
makeThen(node, output);
output.space();
output.print("else");
output.space();
if (alternate.type === "IfStatement") printEstree(alternate, output);
else printMaybeBracedBody(alternate, output);
},
SwitchStatement: (node, output) => {
const { cases } = node;
output.print("switch");
output.space();
printParenthesized(node.discriminant, output);
output.space();
if (cases.length === 0) {
printBracedEmpty(node, output);
return;
}
const last = cases.length - 1;
output.print("{");
for (let i = 0; i <= last; i++) {
output.indent();
printEstree(cases[i], output);
if (i < last && cases[i].consequent.length > 0) output.newline();
}
output.print("}");
},
SwitchCase: (node, output) => {
if (node.test) {
output.print("case");
output.space();
printEstree(node.test, output);
output.print(":");
} else {
output.print("default:");
}
output.newline();
for (const statement of node.consequent) {
output.indent();
printEstree(statement, output);
output.newline();
}
},
TryStatement: (node, output) => {
output.print("try");
output.space();
printEstree(node.block, output);
if (node.handler) {
output.space();
printEstree(node.handler, output);
}
if (node.finalizer) {
output.space();
printEstree(node.finalizer, output);
}
},
CatchClause: (node, output) => {
output.print("catch");
if (node.param) {
output.space();
printParenthesized(node.param, output);
}
output.space();
printBraced(node, node.body.body, output, false);
},
VariableDeclaration: (node, output) => {
const { declarations } = node;
output.print(
node.kind === "using" && node.await === true ? "await using" : node.kind
);
output.space();
for (let i = 0; i < declarations.length; i++) {
if (i) output.comma();
printEstree(declarations[i], output);
}
const parent = parentOf(output, 0);
const isLoopHead =
parent.type === "ForStatement"
? parent.init === node
: kindOf.ForIn(parent) && parent.left === node;
if (!isLoopHead) output.semicolon();
},
VariableDeclarator: (node, output) => {
printEstree(node.id, output);
const { init } = node;
if (!init) return;
output.space();
output.print("=");
output.space();
const grandparent = parentOf(output, 1);
parenthesizeForNoin(
init,
output,
grandparent.type === "ForStatement" || kindOf.ForIn(grandparent)
);
},
ImportDeclaration: (node, output) => {
const { imported_name: defaultName, imported_names: names } = node;
const named = names ? names.length : 0;
output.print("import");
output.space();
if (node.phase) {
output.print(node.phase);
output.space();
}
if (defaultName) printEstree(defaultName, output);
if (named > 0) {
if (defaultName) {
output.print(",");
output.space();
}
const first = /** @type {PrintNode[]} */ (names)[0];
if (first.type === "ImportNamespaceSpecifier") {
printEstree(first, output);
} else {
printSpecifiers(/** @type {PrintNode[]} */ (names), output);
}
}
if (defaultName || named > 0) {
output.space();
output.print("from");
output.space();
}
printEstree(node.source, output);
printAttributes(node, output);
output.semicolon();
},
ImportSpecifier: (node, output) => {
printNameMapping(
node.local,
/** @type {PrintNode} */ (node.imported),
true,
output
);
},
ImportNamespaceSpecifier: (node, output) => {
output.print("*");
output.space();
output.print("as");
output.space();
printEstree(node.local, output);
},
ExportSpecifier: (node, output) => {
printNameMapping(
node.local,
/** @type {PrintNode} */ (node.exported),
false,
output
);
},
ExportNamedDeclaration: (node, output) => {
output.print("export");
output.space();
const declaration = node.exported_value || node.exported_definition;
if (declaration) {
printEstree(declaration, output);
return;
}
printSpecifiers(
/** @type {PrintNameMapping[]} */ (node.exported_names),
output
);
printSource(node, output);
},
ExportAllDeclaration: (node, output) => {
output.print("export");
output.space();
output.print("*");
// `export *` holds its one name mapping, unquoted `*` for none.
const exported = /** @type {PrintIdentifier | PrintLiteral} */ (
/** @type {PrintNameMapping[]} */ (node.exported_names)[0].exported
);
if (!isUnquotedStar(/** @type {Node} */ (exported))) {
output.space();
output.print("as");
output.space();
printForeignName(exported, output);
}
printSource(node, output);
},
ExportDefaultDeclaration: (node, output) => {
const declaration = /** @type {PrintNode} */ (
node.exported_value || node.exported_definition
);
output.print("export");
output.space();
output.print("default");
output.space();
const { corrections } = modules;
// terser writes a named function or class expression bare, which reads
// back as a declaration binding its name in the module.
if (
node.exported_value &&
(declaration.type === "FunctionExpression" ||
declaration.type === "ClassExpression") &&
declaration.id &&
corrections !== undefined &&
corrections.enabled
) {
output.with_parens(() => printEstree(declaration, output));
output.semicolon();
return;
}
printEstree(declaration, output);
if (
!kindOf.Class(declaration) &&
declaration.type !== "FunctionExpression" &&
declaration.type !== "FunctionDeclaration"
) {
output.semicolon();
}
},
Identifier: (node, output) => {
if (recording === null) {
output.print_name(printedNameOf(node));
return;
}
const { definition } = node;
const printed = output.identifierToUtf8(printedNameOf(node), true);
output.print(printed);
if (!definition) return;
// A label's name is always mangled.
if (
isMinifierVariable(definition) &&
isUnmangleable(
definition,
/** @type {MangleOptions} */ (recordingOptions),
null
)
) {
if (recordingKept) {
recordPiece(output, printed.length, definition, false);
}
} else {
recordPiece(output, printed.length, definition, true);
}
},
PrivateIdentifier: (node, output) => {
output.print(`#${node.name}`);
},
Literal: (node, output) => {
const { value } = node;
if (typeof value === "number") {
const keepNumbers = output.option("keep_numbers");
if ((keepNumbers || output.use_asm) && node.raw) {
output.print(node.raw);
} else if (
keepNumbers &&
modules.corrections !== undefined &&
modules.corrections.enabled
) {
// A number the compressor computed has no source to keep, so it stays
// plain unless only an exponent can write it, where the shortest one wins.
const plain = `${value}`;
output.print(plain.includes("e") ? makeNum(value) : plain);
} else {
printNumber(node, value, output);
}
} else if (typeof value === "string") {
// A bigint holds its digits as a string.
if (node.role === "bigint") {
output.print(
output.option("keep_numbers") && node.raw ? node.raw : `${value}n`
);
} else {
output.print_string(value, node.quote, output.in_directive);
}
} else if (value !== null && typeof value === "object") {
printRegExp(node, output);
} else {
output.print(String(value));
}
},
ThisExpression: (node, output) => {
output.print("this");
},
Super: (node, output) => {
output.print("super");
},
MetaProperty: (node, output) => {
output.print(node.role === "importMeta" ? "import.meta" : "new.target");
},
ArrayExpression: (node, output) => {
const { elements } = node;
output.print("[");
if (elements.length > 0) output.space();
printElements(elements, output);
if (elements.length > 0) output.space();
output.print("]");
},
ArrayPattern: (node, output) => {
output.print("[");
printElements(node.elements, output);
output.print("]");
},
ObjectPattern: (node, output) => {
output.print("{");
printElements(node.properties, output);
output.print("}");
},
ObjectExpression: (node, output) => {
const { properties } = node;
if (properties.length === 0) {
printBracedEmpty(node, output);
return;
}
output.print("{");
for (let i = 0; i < properties.length; i++) {
if (i) {
output.print(",");
output.newline();
}
output.indent();
printEstree(properties[i], output);
}
output.newline();
output.print("}");
},
Property: (node, output) => {
if (node.kind === "init" && !node.method) {
printKeyValue(node, output);
} else {
printMethod(node, methodKeyword(node), output);
}
},
MethodDefinition: (node, output) => {
printMethod(node, methodKeyword(node), output);
},
PropertyDefinition: (node, output) => {
if (node.static) {
output.print("static");
output.space();
}
const { key } = node;
if (key.type === "PrivateIdentifier") {
printPrivateName(key.name, undefined, output);
} else if (node.computed) {
output.print("[");
printEstree(key, output);
output.print("]");
} else {
printPropertyName(keyName(key), node.quote, output);
}
if (node.value) {
output.print("=");
printEstree(node.value, output);
}
output.semicolon();
},
StaticBlock: (node, output) => {
output.print("static");
output.space();
printBraced(node, node.body, output, false);
},
SpreadElement: (node, output) => {
output.print("...");
printEstree(node.argument, output);
},
RestElement: (node, output) => {
output.print("...");
printEstree(node.argument, output);
},
AssignmentPattern: printBinary,
AssignmentExpression: printBinary,
LogicalExpression: printBinary,
BinaryExpression: (node, output) => {
if (node.left.type !== "PrivateIdentifier") {
printBinary(node, output);
return;
}
printEstree(node.left, output);
output.space();
output.print("in");
output.space();
printEstree(node.right, output);
},
UnaryExpression: printUnary,
UpdateExpression: printUnary,
ConditionalExpression: (node, output) => {
printEstree(node.test, output);
output.space();
output.print("?");
output.space();
printEstree(node.consequent, output);
output.space();
output.colon();
printEstree(node.alternate, output);
},
SequenceExpression: (node, output) => {
const { expressions } = node;
for (let i = 0; i < expressions.length; i++) {
if (i > 0) {
output.comma();
if (output.should_break()) {
output.newline();
output.indent();
}
}
printEstree(expressions[i], output);
}
},
CallExpression: printCall,
NewExpression: (node, output) => {
output.print("new");
output.space();
printCall(node, output);
},
ImportExpression: (node, output) => {
output.print(node.phase ? `import.${node.phase}` : "import");
printArguments(node.args, output);
},
ChainExpression: (node, output) => {
printEstree(node.expression, output);
},
MemberExpression: (node, output) => {
const { object, property } = node;
printEstree(object, output);
if (node.computed) {
if (node.optional) output.print("?.");
output.print("[");
printEstree(property, output);
output.print("]");
return;
}
const name = /** @type {PrintIdentifier | PrintPrivateIdentifier} */ (
property
).name;
if (property.type === "PrivateIdentifier") {
if (node.optional) output.print("?");
output.print(".#");
output.add_mapping(node.endToken);
output.print_name(name);
return;
}
const printComputed = /** @type {boolean} */ (
ALL_RESERVED_WORDS.has(name)
? output.option("ie8")
: !isIdentifierString(
name,
output.option("ecma") >= 2015 && !output.option("safari10")
)
);
if (node.optional) output.print("?.");
if (printComputed) {
output.print("[");
output.add_mapping(node.endToken);
output.print_string(name);
output.print("]");
return;
}
// `1..x`: a dot straight after an integer literal is its fraction's.
const { corrections } = modules;
if (
isNumber(object) &&
object.value >= 0 &&
!/[xa-f.)]/i.test(output.last()) &&
// An octal literal kept as written takes no fraction, so terser's
// `0o17..x` and `017..x` do not parse.
!(
corrections !== undefined &&
corrections.enabled &&
/^0(?:o|[0-7]+$)/i.test(output.last())
)
) {
output.print(".");
}
if (!node.optional) output.print(".");
output.add_mapping(node.endToken);
output.print_name(name);
},
TemplateLiteral: (node, output) => {
const parent = parentOf(output, 0);
const isTagged = parent.type === "TaggedTemplateExpression";
const { segments } = node;
output.print("`");
// The compressor may leave two strings side by side: printed as one.
let text = "";
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
if (segment.type === "TemplateElement") {
text += isTagged ? segment.raw : segment.value;
continue;
}
printTemplateText(text, isTagged, output);
text = "";
output.print("${");
printEstree(segment, output);
output.print("}");
}
printTemplateText(text, isTagged, output);
output.print("`");
},
TaggedTemplateExpression: (node, output) => {
const { tag } = node;
const parenthesizeTag =
kindOf.Lambda(tag) ||
kindOf.Binary(tag) ||
tag.type === "ConditionalExpression" ||
tag.type === "SequenceExpression" ||
kindOf.Unary(tag) ||
(kindOf.Dot(tag) &&
/** @type {PrintMemberExpression} */ (tag).object.type ===
"ObjectExpression");
if (parenthesizeTag) output.print("(");
printEstree(tag, output);
if (parenthesizeTag) output.print(")");
printEstree(node.quasi, output);
},
AwaitExpression: (node, output) => {
output.print("await");
output.space();
const { argument } = node;
const parens = !(
kindOf.Call(argument) ||
argument.type === "Identifier" ||
argument.type === "MemberExpression" ||
kindOf.Unary(argument) ||
argument.type === "Literal" ||
argument.type === "AwaitExpression" ||
argument.type === "ObjectExpression"
);
if (parens) output.print("(");
printEstree(argument, output);
if (parens) output.print(")");
},
YieldExpression: (node, output) => {
output.print(node.delegate ? "yield*" : "yield");
if (node.argument) {
output.space();
printEstree(node.argument, output);
}
},
FunctionExpression: printFunction,
FunctionDeclaration: printFunction,
ArrowFunctionExpression: (node, output) => {
const parent = parentOf(output, 0);
const parenthesized =
((parent.type === "BinaryExpression" ||
parent.type === "LogicalExpression") &&
parent.left.type !== "PrivateIdentifier") ||
kindOf.Unary(parent) ||
(kindOf.Call(parent) && parent.callee === node);
if (parenthesized) output.print("(");
if (node.async) {
output.print("async");
output.space();
}
const { params } = node;
if (params.length === 1 && params[0].type === "Identifier") {
printEstree(params[0], output);
} else {
printArguments(params, output);
}
output.space();
output.print("=>");
output.space();
const statements = node.body.body;
if (statements.length === 1 && statements[0].type === "ReturnStatement") {
const returned = statements[0].argument;
if (!returned) {
output.print("{}");
} else if (leftIsObject(returned)) {
output.print("(");
printEstree(returned, output);
output.print(")");
} else {
printEstree(returned, output);
}
} else {
printBraced(node, statements, output, false);
}
if (parenthesized) output.print(")");
gcScope(node, output);
},
ClassExpression: printClass,
ClassDeclaration: printClass
};
/**
* terser's `needs_parens` of a unary, update or `await`, before what only
* one of them asks.
* @param {PrintNode} node the node
* @param {PrintParent} parent its parent
* @returns {boolean} whether it is parenthesized
*/
const isCalledOrAccessed = (node, parent) =>
(parent.type === "MemberExpression" && parent.object === node) ||
(kindOf.Call(parent) && parent.callee === node);
/**
* terser's `needs_parens` of a binary or logical expression or an
* assignment pattern.
* @type {(node: PrintBinaryLike, output: OutputStream) => boolean | undefined}
*/
const binaryNeedsParens = (node, output) => {
const parent = parentOf(output, 0);
if (kindOf.Unary(parent) || isCalledOrAccessed(node, parent)) return true;
const operator = operatorOf(node);
if (kindOf.Binary(parent)) {
const parentOperator = operatorOf(parent);
// `??` mixed with `||` or `&&` is a syntax error without parens.
if (
(operator === "??" &&
(parentOperator === "||" || parentOperator === "&&")) ||
(parentOperator === "??" && (operator === "||" || operator === "&&"))
) {
return true;
}
const parentPrecedence = PRECEDENCE[parentOperator];
const precedence = PRECEDENCE[operator];
if (
parentPrecedence > precedence ||
(parentPrecedence === precedence &&
(node === /** @type {PrintBinaryLike} */ (parent).right ||
parentOperator === "**"))
) {
return true;
}
}
if (kindOf.PrivateIn(parent)) {
const parentPrecedence = PRECEDENCE.in;
const precedence = PRECEDENCE[operator];
if (
parentPrecedence > precedence ||
(parentPrecedence === precedence &&
node === /** @type {PrintBinary} */ (parent).right)
) {
return true;
}
}
};
/**
* terser's `needs_parens` of a private `in`.
* @type {(node: PrintBinary, output: OutputStream) => boolean | undefined}
*/
const privateInNeedsParens = (node, output) => {
const parent = parentOf(output, 0);
if (kindOf.Unary(parent) || isCalledOrAccessed(node, parent)) return true;
if (kindOf.Binary(parent)) {
const parentOperator = operatorOf(parent);
const parentPrecedence = PRECEDENCE[parentOperator];
const precedence = PRECEDENCE.in;
if (
parentPrecedence > precedence ||
(parentPrecedence === precedence &&
(node === /** @type {PrintBinaryLike} */ (parent).right ||
parentOperator === "**"))
) {
return true;
}
}
if (
kindOf.PrivateIn(parent) &&
node === /** @type {PrintBinary} */ (parent).right
) {
return true;
}
};
/**
* terser's `needs_parens` of an assignment or a conditional.
* @type {(node: PrintBinary | PrintConditionalExpression, output: OutputStream) => boolean | undefined}
*/
const assignNeedsParens = (node, output) => {
const parent = parentOf(output, 0);
if (
kindOf.Unary(parent) ||
(kindOf.Binary(parent) && parent.type !== "AssignmentExpression") ||
isCalledOrAccessed(node, parent) ||
(parent.type === "ConditionalExpression" && parent.test === node)
) {
return true;
}
// `({a, b} = c)`, an object destructuring assignment.
if (
node.type === "AssignmentExpression" &&
node.left.type === "ObjectPattern"
) {
return true;
}
};
/**
* terser's `needs_parens` of a unary or update expression.
* @type {(node: PrintUnary, output: OutputStream) => boolean | undefined}
*/
const unaryNeedsParens = (node, output) => {
const parent = parentOf(output, 0);
return (
isCalledOrAccessed(node, parent) ||
(kindOf.Binary(parent) &&
/** @type {PrintBinary} */ (parent).operator === "**" &&
kindOf.UnaryPrefix(node) &&
/** @type {PrintBinary} */ (parent).left === node &&
node.operator !== "++" &&
node.operator !== "--")
);
};
// terser prints `export default (class {}).name` bare, which does not parse:
// `export default` reads a leading `class` or `function` as a declaration.
const correctsExport = () =>
modules.corrections !== undefined && modules.corrections.enabled;
/** @type {EstreeParensTests} */
const needsParens = {
FunctionExpression: (node, output) => {
if (!output.has_parens() && firstInStatement(output, correctsExport())) {
return true;
}
const parent = parentOf(output, 0);
if (
output.option("webkit") &&
parent.type === "MemberExpression" &&
parent.object === node
) {
return true;
}
if (
output.option("wrap_iife") &&
kindOf.Call(parent) &&
parent.callee === node
) {
return true;
}
return (
output.option("wrap_func_args") &&
kindOf.Call(parent) &&
parent.arguments.includes(node)
);
},
ArrowFunctionExpression: (node, output) => {
const parent = parentOf(output, 0);
if (
output.option("wrap_func_args") &&
kindOf.Call(parent) &&
parent.arguments.includes(node)
) {
return true;
}
return (
(parent.type === "MemberExpression" && parent.object === node) ||
(parent.type === "ConditionalExpression" && parent.test === node)
);
},
// Otherwise `{...}` would read as a block.
ObjectExpression: (node, output) =>
!output.has_parens() && firstInStatement(output),
ClassExpression: (node, output) =>
firstInStatement(output, correctsExport()),
UnaryExpression: unaryNeedsParens,
UpdateExpression: unaryNeedsParens,
AwaitExpression: (node, output) => {
const parent = parentOf(output, 0);
return (
isCalledOrAccessed(node, parent) ||
(kindOf.Binary(parent) &&
/** @type {PrintBinary} */ (parent).operator === "**" &&
/** @type {PrintBinary} */ (parent).left === node) ||
(output.option("safari10") && kindOf.UnaryPrefix(parent))
);
},
SequenceExpression: (node, output) => {
const parent = parentOf(output, 0);
return (
kindOf.Call(parent) ||
kindOf.Unary(parent) ||
kindOf.Binary(parent) ||
parent.type === "VariableDeclarator" ||
(parent.type === "MemberExpression" && node !== parent.property) ||
parent.type === "ArrayExpression" ||
kindOf.ObjectProperty(parent) ||
parent.type === "ConditionalExpression" ||
parent.type === "ArrowFunctionExpression" ||
kindOf.Expansion(parent) ||
(parent.type === "ForOfStatement" && node === parent.right) ||
parent.type === "YieldExpression" ||
kindOf.Export(parent)
);
},
BinaryExpression: (node, output) =>
node.left.type === "PrivateIdentifier"
? privateInNeedsParens(node, output)
: binaryNeedsParens(node, output),
LogicalExpression: binaryNeedsParens,
AssignmentPattern: binaryNeedsParens,
AssignmentExpression: assignNeedsParens,
ConditionalExpression: assignNeedsParens,
YieldExpression: (node, output) => {
const parent = parentOf(output, 0);
// `a = yield 3` needs none.
return (
(kindOf.Binary(parent) && operatorOf(parent) !== "=") ||
isCalledOrAccessed(node, parent) ||
(parent.type === "ConditionalExpression" && parent.test === node) ||
kindOf.Unary(parent)
);
},
ChainExpression: (node, output) => {
const parent = parentOf(output, 0);
return isCalledOrAccessed(node, parent);
},
MemberExpression: (node, output) => {
const parent = parentOf(output, 0);
// A call inside `new (a.b().c)` would otherwise take the `new`'s arguments.
return (
parent.type === "NewExpression" &&
parent.callee === node &&
findNode(node, findCall)
);
},
TaggedTemplateExpression: (node, output) => {
const parent = parentOf(output, 0);
return parent.type === "NewExpression" && parent.callee === node;
},
CallExpression: (node, output) => {
const parent = parentOf(output, 0);
const calleeIsFunction = node.callee.type === "FunctionExpression";
if (
(parent.type === "NewExpression" && parent.callee === node) ||
(parent.type === "ExportDefaultDeclaration" && calleeIsFunction)
) {
return true;
}
// Safari bug: https://bugs.webkit.org/show_bug.cgi?id=123506
if (
!calleeIsFunction ||
parent.type !== "MemberExpression" ||
parent.object !== node
) {
return false;
}
const grandparent = parentOf(output, 1);
return (
grandparent.type === "AssignmentExpression" &&
grandparent.left === parent
);
},
NewExpression: (node, output) => {
const parent = parentOf(output, 0);
return (
node.arguments.length === 0 &&
(parent.type === "MemberExpression" ||
(kindOf.Call(parent) && parent.callee === node) ||
(parent.type === "TaggedTemplateExpression" && parent.tag === node))
);
},
Literal: (node, output) => {
const parent = parentOf(output, 0);
if (parent.type !== "MemberExpression" || parent.object !== node) {
return false;
}
if (isNumber(node)) {
return node.value < 0 || makeNum(node.value).startsWith("0");
}
return (
node.role === "bigint" &&
/** @type {string} */ (node.value).startsWith("-")
);
}
};
modules.printEstree = printEstree;
const MinifiedOutput =
/** @type {ReturnType<typeof createMinifiedOutput>} */ (
modules.MinifiedOutput
);
// Shared with `correct`, which wraps an entry.
modules.estreeCodegen = codegen;
modules.estreeNeedsParens = needsParens;
/**
* @param {FormatSettings=} given format options
* @returns {OutputStream} a stream as terser's `print_to_string` makes
*/
const createStringStream = (given) => {
const options = minifiedOptions(
/** @type {FormatSettings} */ (defaults(given, FORMAT_DEFAULTS, true))
);
if (options.shorthand === undefined) options.shorthand = options.ecma > 5;
return new MinifiedOutput(options, !given, ESTREE_NODE_ACCESS);
};
modules.createStringStream = createStringStream;
/**
* terser's `print_to_string`: a node's text.
* @param {Node} node terser's node
* @param {FormatSettings=} given format options
* @param {TokenTables=} tables what a node inside a program reads its tokens through, the program's own by default
* @returns {string} its text
*/
const printToString = (node, given, tables) => {
const output = createStringStream(given);
const own = tables || /** @type {PrintProgram} */ (node).tokenTables;
if (own) output.tokenTables = own;
printEstree(/** @type {PrintNode} */ (node), output);
return output.get();
};
modules.printToString = printToString;
/**
* Prints a program; where nobody keeps it, each node printed drops what only
* the compressor read, as terser's tree is dropped once printed.
* @param {PrintProgram} program the program
* @param {OutputStream} output the stream printed into
* @param {boolean | undefined} releases whether nobody keeps the tree
* @returns {void}
*/
modules.printProgram = (program, output, releases) => {
output.tokenTables = program.tokenTables || NO_TOKEN_TABLES;
onPrinted = releases ? releaseCompressorFields : undefined;
try {
printEstree(program, output);
} finally {
onPrinted = undefined;
}
};
/**
* Prints a program before it is mangled, recording what mangling rewrites
* in the text for `patchRecording` to write once the names are chosen.
* @param {PrintProgram} program the program
* @param {OutputStream} output the stream printed into
* @param {MangleOptions} options the formatted mangle options
* @returns {MangleRecording} what the print recorded
*/
modules.printRecording = (program, output, options) => {
const recorded = createMangleRecording();
output.tokenTables = program.tokenTables || NO_TOKEN_TABLES;
recording = recorded;
recordingOptions = options;
recordingKept =
Boolean(options.cache) ||
answersPerCall(options.keep_fnames) ||
answersPerCall(options.keep_classnames);
try {
printEstree(program, output);
} finally {
recording = null;
recordingOptions = null;
}
return recorded;
};
/**
* The text of a recorded print with each name mangling renamed written as
* it was named and each recorded `const` as `let`; the mappings of the
* stream's source map move with the text.
* @param {OutputStream} output the stream printed into
* @param {string} text what it wrote
* @param {MangleRecording} recorded what the print recorded
* @returns {string} the text rewritten
*/
modules.patchRecording = (output, text, recorded) => {
const { starts, lengths, count, targets, specifiers } = recorded;
if (count === 0) return text;
const map = output.sourceMap;
/** @type {string[]} */
const pieces = [];
/** @type {number[]} */
let shiftedColumns = [];
/** @type {number[]} */
let shiftDeltas = [];
let shiftedLine = 1;
let lineStart = 0;
let lineEnd = map ? text.indexOf("\n") : -1;
let from = 0;
let nextSpecifier = 0;
for (let i = 0; i < count; i++) {
const target = targets[i];
const start = starts[i];
const length = Math.abs(lengths[i]);
let replacement;
let before = false;
if (
nextSpecifier < specifiers.length &&
specifiers[nextSpecifier].index === i
) {
const specifier = specifiers[nextSpecifier++];
const local = target.mangled_name || target.name;
replacement =
local === specifier.foreign ? "" : asClause(output, local, specifier);
before = specifier.isImport;
} else {
const mangled = target.mangled_name;
if (!mangled || mangled === target.name) continue;
replacement = output.identifierToUtf8(mangled, true);
}
pieces.push(text.slice(from, start), replacement);
from = start + length;
if (!map) continue;
const delta = replacement.length - length;
if (delta === 0) continue;
let line = shiftedLine;
while (lineEnd !== -1 && lineEnd < start) {
line++;
lineStart = lineEnd + 1;
lineEnd = text.indexOf("\n", lineStart);
}
if (line !== shiftedLine) {
map.shiftColumns(shiftedLine, shiftedColumns, shiftDeltas);
shiftedColumns = [];
shiftDeltas = [];
shiftedLine = line;
}
// A clause before the name moves the name's own mapping too.
shiftedColumns.push(start - lineStart - (before ? 0.5 : 0));
shiftDeltas.push(delta);
}
if (map && shiftedColumns.length !== 0) {
map.shiftColumns(shiftedLine, shiftedColumns, shiftDeltas);
}
pieces.push(text.slice(from));
return pieces.join("");
};
/**
* terser's `print_braced` of a function's body: its statements in braces.
* @param {PrintFunction} node the function
* @param {OutputStream} output the stream printed into
* @returns {void}
*/
modules.printFunctionBody = (node, output) => {
printBraced(node, node.body.body, output, false);
};
/**
* terser's `print_to_string` of a node read as ESTree.
* @param {PrintNode} node the node
* @returns {string} its text
*/
modules.printEstreeToString = (node) => {
const output = createStringStream();
printEstree(node, output);
return output.get();
};
/**
* @param {PrintNode} node a computed key
* @param {CharacterCounter} identifiers the identifier source counting
* @returns {void}
*/
const skipString = (node, identifiers) => {
if (
node.type === "Literal" &&
typeof node.value === "string" &&
node.role !== "bigint"
) {
identifiers.consider(node.value, -1);
} else if (node.type === "ConditionalExpression") {
skipString(node.consequent, identifiers);
skipString(node.alternate, identifiers);
} else if (node.type === "SequenceExpression") {
skipString(node.expressions[node.expressions.length - 1], identifiers);
}
};
/**
* @param {IdentifierSource} identifiers an identifier source
* @returns {identifiers is Required<IdentifierSource>} whether it orders its names by the characters counted
*/
const countsFrequency = (identifiers) =>
Boolean(identifiers.reset && identifiers.consider && identifiers.sort);
modules.countsFrequency = countsFrequency;
/**
* terser's `compute_char_frequency` over a tree `analyzeManglingScopes`
* read: counts the characters the output prints, less the names mangled.
* @param {PrintNode} printNode the tree
* @param {MangleOptions} options the formatted mangle options
* @param {LetCounts | null=} letCounts what writing the `let` declarations early changed in the text, to count back
* @returns {boolean} whether the tree may name a private member: false only where printing it met none
*/
modules.computeEstreeCharFrequency = (printNode, options, letCounts) => {
/** @type {IdentifierSource} */
const identifiers = options.nth_identifier;
if (!countsFrequency(identifiers)) return true;
identifiers.reset();
const output = createStringStream();
output.consumePart = (/** @type {string} */ part) =>
identifiers.consider(part, 1);
// Whether a node `renameMangledEstree` renames a private name at was
// printed: with none, it need not walk the tree.
let namesPrivate = false;
onPrinted = (node) => {
switch (node.type) {
case "Identifier": {
const { definition } = node;
// A label's name is always mangled.
if (
definition &&
!(
isMinifierVariable(definition) &&
isUnmangleable(definition, options, null)
)
) {
identifiers.consider(node.name, -1);
}
return;
}
case "MemberExpression": {
const { property } = node;
if (property.type === "PrivateIdentifier") namesPrivate = true;
if (!options.properties) return;
if (node.computed) {
skipString(property, identifiers);
} else if (property.type === "PrivateIdentifier") {
identifiers.consider(`#${property.name}`, -1);
} else {
identifiers.consider(
/** @type {PrintIdentifier} */ (property).name,
-1
);
}
return;
}
case "PrivateIdentifier":
namesPrivate = true;
return;
case "MethodDefinition":
case "PropertyDefinition":
if (node.key.type === "PrivateIdentifier") namesPrivate = true;
break;
default:
}
};
try {
printEstree(printNode, output);
} finally {
onPrinted = undefined;
}
identifiers.consider(output.get(), 1);
if (letCounts) countLetsBack(identifiers, letCounts);
identifiers.sort();
return namesPrivate;
};
};
// What the tree printer marks a node with beyond its type: a property of a
// binding pattern, whose key terser quotes its own way, and a number too large
// for a double, which terser reads as the name `Infinity`.
const MARK_BINDING_PROPERTY = 1;
const MARK_INFINITY = 2;
/**
* Where each node of a `SyntaxTree` starts and ends among its parse's tokens,
* and what terser's conversion makes of it, by node.
* @typedef {object} TreeTokens
* @property {Int32Array} starts each node's first token's number, 0 for none
* @property {Int32Array} ends each node's last token's number, 0 for none
* @property {Uint8Array} types each node's `NodeType`, as terser's conversion types it
* @property {Uint8Array} marks each node's `MARK_` bits
* @property {number} first the number of the source's first token
*/
/**
* What mangling gave a source `parse.parseTree` read, for the tree printer.
* @typedef {object} TreeNames
* @property {TreeDefinition[]} definitions each name's definition, by its node
* @property {Map<string, string> | null} privateNames each private name's new name, null for none renamed
* @property {MangleRecording | null} recorded where the result was printed while mangling, what `patchRecording` rewrites
* @property {string} text what that print wrote
*/
/**
* A source `parse.parseTree` read for the tree printer.
* @typedef {object} ParsedTree
* @property {SyntaxTree} tree the tree
* @property {number} program its program
* @property {TokenTables} tables what its tokens' numbers are read through
* @property {TreeTokens} read its nodes' tokens
* @property {boolean} module whether it imports or exports
*/
/**
* Installs `printTree`, which prints a source `parse.parseTree` read into a
* `SyntaxTree` as `printEstree` prints terser's tree of it, `correct` and
* `improve` on; `minify` prints with it where nothing compresses or mangles.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installTreePrint = (modules) => {
const { Field, Flag, NodeType, Part } = require("./syntax-parser");
const { NODE_FIELDS } = require("./data");
const { ALL_RESERVED_WORDS, PRECEDENCE } =
/** @type {{ ALL_RESERVED_WORDS: Set<string>, PRECEDENCE: Record<string, number> }} */ (
modules.parse
);
const regexpSourceFix = /** @type {(source: string) => string} */ (
modules.utils.regexp_source_fix
);
const { isIdentifierString } = createUnicode();
const printPropertyName = createPrintPropertyName(modules);
const fieldsByName = /** @type {Record<string, number>} */ (Field);
const typesByName = /** @type {Record<string, number>} */ (NodeType);
// Each node type's fields holding nodes, in the tree's order.
/** @type {number[][]} */
const CHILD_FIELDS = [];
for (const name of Object.keys(NODE_FIELDS)) {
/** @type {number[]} */
const fields = [];
const spec = NODE_FIELDS[name];
for (const part of spec === "" ? [] : spec.split(" ")) {
const [fieldName, kind] = part.split(":");
if (kind === "node" || kind === "list") {
fields.push(fieldsByName[fieldName]);
}
}
CHILD_FIELDS[typesByName[name]] = fields;
}
// The types after which the stream lets a line comment follow.
const STATEMENT_TYPES = new Uint8Array(CHILD_FIELDS.length);
for (const name of ESTREE_STATEMENT_TYPES) {
STATEMENT_TYPES[typesByName[name]] = 1;
}
/**
* @param {number} type a node's type
* @param {number} field one of its fields
* @param {boolean} binding whether the node is part of a binding pattern
* @returns {boolean} whether what the field holds is
*/
const bindsIn = (type, field, binding) => {
switch (type) {
case NodeType.FunctionDeclaration:
case NodeType.FunctionExpression:
case NodeType.ArrowFunctionExpression:
return field === Field.params;
case NodeType.VariableDeclarator:
return field === Field.id;
case NodeType.CatchClause:
return field === Field.param;
case NodeType.ObjectPattern:
case NodeType.ArrayPattern:
case NodeType.RestElement:
return binding;
case NodeType.AssignmentPattern:
return binding && field === Field.left;
case NodeType.Property:
return binding && field === Field.value;
default:
return false;
}
};
/**
* Reads the tokens each node starts and ends at as terser's conversion gives
* them, binding a parenthesis's comments as it does: the parenthesized
* expression takes the parentheses' tokens.
* @param {SyntaxTree} tree the tree
* @param {number} program its program
* @param {(pos: number, ending: boolean, forward?: number) => number | undefined} at the token at an offset
* @param {TokenTable} table the table the tokens' numbers are read through
* @returns {TreeTokens} what was read
*/
const readTreeTokens = (tree, program, at, table) => {
let capacity = 1024;
let starts = new Int32Array(capacity);
let ends = new Int32Array(capacity);
let types = new Uint8Array(capacity);
let marks = new Uint8Array(capacity);
/**
* @param {number} pos an offset
* @param {boolean} ending true for the last token ending by it, else the first starting from it
* @param {number=} forward how many tokens further on
* @returns {number} the token's number, 0 for none
*/
const tokenAt = (pos, ending, forward) => {
const token = at(pos, ending, forward);
return token === undefined ? 0 : token;
};
/**
* @param {number} number a token's number, 0 for none
* @returns {SourceToken | undefined} the token, undefined for none
*/
const tokenOf = (number) =>
number === 0 ? undefined : table.token(number - table.base);
/**
* @param {number} node a node
* @returns {void}
*/
const grow = (node) => {
while (capacity <= node) capacity *= 2;
const widerStarts = new Int32Array(capacity);
widerStarts.set(starts);
starts = widerStarts;
const widerEnds = new Int32Array(capacity);
widerEnds.set(ends);
ends = widerEnds;
const widerTypes = new Uint8Array(capacity);
widerTypes.set(types);
types = widerTypes;
const widerMarks = new Uint8Array(capacity);
widerMarks.set(marks);
marks = widerMarks;
};
/**
* @param {number} node a node, a parenthesis among them
* @param {boolean} binding whether it is part of a binding pattern
* @returns {void}
*/
const readChild = (node, binding) => {
if (tree.type(node) === NodeType.ParenthesizedExpression) {
readParenthesized(node);
} else {
read(node, binding);
}
};
/**
* @param {number} node a node not a parenthesis
* @param {boolean} binding whether it is part of a binding pattern
* @returns {void}
*/
const read = (node, binding) => {
if (node >= capacity) grow(node);
const type = tree.type(node);
let printedType = type;
starts[node] = tokenAt(tree.start(node), false);
ends[node] = tokenAt(tree.end(node), true);
switch (type) {
case NodeType.SequenceExpression:
// terser reads a sequence's end off `peek()`, past the token after it.
ends[node] = tokenAt(tree.end(node), true, 2);
break;
case NodeType.ArrowFunctionExpression:
ends[node] = 0;
break;
case NodeType.Property: {
// terser's parser opens a computed property at its `[`, but a
// binding pattern's at its key, as its conversion does.
const key = tree.field(0, Field.key, node);
const opensAt =
!binding && tree.flag(Flag.computed, node) ? node : key;
starts[node] = tokenAt(tree.start(opensAt), false);
ends[node] = tokenAt(
tree.end(tree.field(0, Field.value, node)),
true
);
if (binding) marks[node] = MARK_BINDING_PROPERTY;
break;
}
case NodeType.FunctionDeclaration:
if (tree.field(0, Field.id, node) === 0) {
printedType = NodeType.FunctionExpression;
}
break;
case NodeType.ClassDeclaration:
if (tree.field(0, Field.id, node) === 0) {
printedType = NodeType.ClassExpression;
}
break;
case NodeType.Literal:
if (tree.value(node) === Infinity) {
printedType = NodeType.Identifier;
marks[node] = MARK_INFINITY;
}
break;
case NodeType.ImportAttribute:
printedType = NodeType.Property;
break;
default:
}
types[node] = printedType;
const fields = CHILD_FIELDS[type];
for (let f = 0; f < fields.length; f++) {
const field = fields[f];
const childBinding = bindsIn(type, field, binding);
const count = tree.fieldCount(field, node);
for (let i = 0; i < count; i++) {
const child = tree.field(i, field, node);
if (child !== 0) readChild(child, childBinding);
}
}
};
/**
* @param {number} paren a parenthesized expression
* @returns {number} the expression it holds, past any other parentheses
*/
const readParenthesized = (paren) => {
const inner = tree.field(0, Field.expression, paren);
let expression = inner;
if (tree.type(inner) === NodeType.ParenthesizedExpression) {
expression = readParenthesized(inner);
} else {
read(inner, false);
// terser reads a sequence's end off `peek()` once the parenthesis
// holding it is read, so it ends two tokens past the close.
if (tree.type(inner) === NodeType.SequenceExpression) {
ends[inner] = tokenAt(tree.end(paren), true, 2);
}
}
const open = tokenAt(tree.start(paren), false);
const close = tokenAt(tree.end(paren), true);
if (open !== 0 && close !== 0) {
bindParenthesisComments(
/** @type {SourceToken} */ (tokenOf(open)),
/** @type {SourceToken} */ (tokenOf(close)),
tokenOf(starts[expression]),
tokenOf(ends[expression])
);
}
starts[expression] = open;
ends[expression] = close;
return expression;
};
read(program, false);
return { starts, ends, types, marks, first: table.base };
};
modules.readTreeTokens = readTreeTokens;
// Off, `minify` prints terser's tree of every source, as a test compares.
modules.treePrinting = { enabled: true };
// The tree printed, its tokens, the stream it is printed into and the
// nodes holding the one printed, innermost last.
let tree = /** @type {SyntaxTree} */ (
/** @type {SyntaxTree | null} */ (null)
);
let tokens = /** @type {TreeTokens} */ (
/** @type {TreeTokens | null} */ (null)
);
let output = /** @type {OutputStream<number>} */ (
/** @type {OutputStream<number> | null} */ (null)
);
/** @type {number[]} */
let stack = [];
// The source the tree was read from, and where the last number or regular
// expression printed ended, which an `in` after it is spaced from.
let source = "";
let literalEnd = -1;
// Where names are mangled, each name's definition by its node and each
// private name's new name; while characters are counted, the options
// whose identifier source counts them.
/** @type {TreeDefinition[] | null} */
let definitions = null;
/** @type {Map<string, string> | null} */
let privateNames = null;
/** @type {MangleOptions | null} */
let countOptions = null;
// While a print is recorded for mangling to rewrite, what it records and
// the mangle options deciding which names are pieces.
/** @type {MangleRecording | null} */
let recording = null;
/** @type {MangleOptions | null} */
let recordingOptions = null;
/**
* @param {number} node a node, or an import's attributes as the negated import
* @returns {number} its type, as terser's conversion types it
*/
const typeOf = (node) =>
node < 0 ? NodeType.ObjectExpression : tokens.types[node];
/**
* @param {number} node a node, 0 for none
* @returns {number} the node parentheses around it hold, which terser's tree has instead
*/
const unwrap = (node) => {
while (node !== 0 && tree.type(node) === NodeType.ParenthesizedExpression) {
node = tree.field(0, Field.expression, node);
}
return node;
};
/**
* @param {number} field the `Field`
* @param {number} node a node
* @returns {number} the node the field holds, past parentheses, 0 for none
*/
const childOf = (field, node) => unwrap(tree.field(0, field, node));
/**
* @param {number} i the item's index
* @param {number} field the `Field`, a list
* @param {number} node a node
* @returns {number} the list's `i`-th node, past parentheses, 0 for a hole
*/
const itemOf = (i, field, node) => unwrap(tree.field(i, field, node));
/**
* @param {number} level how many levels further out than the parent
* @returns {number} the node holding the one printed, 0 past the root
*/
const parentAt = (level) => {
const index = stack.length - 2 - level;
return index >= 0 ? stack[index] : 0;
};
/**
* @param {number} node a binary-like node, as `isBinary` finds one
* @returns {string} its operator, which an assignment pattern leaves implicit
*/
const operatorOf = (node) =>
typeOf(node) === NodeType.AssignmentPattern
? "="
: /** @type {string} */ (tree.textOf(Part.operator, node));
/**
* @param {number} node an identifier
* @returns {string} its name
*/
const nameOf = (node) =>
(tokens.marks[node] & MARK_INFINITY) !== 0 ? "Infinity" : tree.name(node);
/**
* terser's `print_name` of a symbol: its definition's name, as mangled.
* @param {number} node an identifier
* @returns {string} the name printed
*/
const printedName = (node) => {
const definition = definitions === null ? null : definitions[node];
return definition
? definition.mangled_name || definition.name
: nameOf(node);
};
/**
* @param {number} node a private name
* @returns {string} the name printed, without its `#`
*/
const privateNameOf = (node) => {
const name = tree.name(node);
const renamed = privateNames === null ? undefined : privateNames.get(name);
return renamed === undefined ? name : renamed;
};
/**
* @param {number} key a key not computed: a name or a literal
* @returns {string} the name terser holds for it
*/
const keyName = (key) =>
tree.type(key) === NodeType.Literal
? String(tree.value(key))
: tree.name(key);
/**
* @param {number} node a string literal
* @returns {string} the quote it was written with
*/
const quoteOf = (node) =>
/** @type {string} */ (tree.textOf(Part.raw, node))[0];
/**
* @param {number} node an import's or export's name, a literal where quoted
* @returns {string} the name
*/
const moduleName = (node) =>
tree.type(node) === NodeType.Literal
? String(tree.value(node))
: tree.name(node);
/**
* @param {number} node an import's or export's local name, a literal where quoted
* @returns {string} the name, as mangled
*/
const localName = (node) =>
tree.type(node) === NodeType.Literal
? String(tree.value(node))
: printedName(node);
/**
* The quote a member's key keeps, as terser's conversion reads it.
* @param {number} member a property, method or class field
* @returns {string | undefined} the quote, "" for none
*/
const keyQuoteOf = (member) => {
if (tree.type(member) === NodeType.ImportAttribute) return undefined;
if (tree.flag(Flag.computed, member)) return "";
const key = tree.field(0, Field.key, member);
if (
tree.type(key) !== NodeType.Literal ||
typeof tree.value(key) !== "string"
) {
return "";
}
return (tokens.marks[member] & MARK_BINDING_PROPERTY) !== 0
? '"'
: quoteOf(key);
};
/**
* @param {number} type a node's type
* @returns {boolean} whether it is a function
*/
const isLambdaType = (type) =>
type === NodeType.FunctionExpression ||
type === NodeType.ArrowFunctionExpression ||
type === NodeType.FunctionDeclaration;
/**
* @param {number} type a node's type
* @returns {boolean} whether it is a call or a `new`
*/
const isCallType = (type) =>
type === NodeType.CallExpression || type === NodeType.NewExpression;
/**
* @param {number} type a node's type
* @returns {boolean} whether it is a class
*/
const isClassType = (type) =>
type === NodeType.ClassDeclaration || type === NodeType.ClassExpression;
/**
* @param {number} type a node's type
* @returns {boolean} whether it is terser's `Scope`
*/
const isScopeType = (type) =>
type === NodeType.Program ||
isLambdaType(type) ||
isClassType(type) ||
type === NodeType.StaticBlock;
/**
* @param {number} type a node's type
* @returns {boolean} whether it is terser's `Unary`
*/
const isUnaryType = (type) =>
type === NodeType.UnaryExpression || type === NodeType.UpdateExpression;
/**
* @param {number} type a node's type
* @returns {boolean} whether it is terser's `ForIn`, `for … of` among them
*/
const isForInType = (type) =>
type === NodeType.ForInStatement || type === NodeType.ForOfStatement;
/**
* @param {number} type a node's type
* @returns {boolean} whether it is terser's `ObjectProperty`
*/
const isObjectPropertyType = (type) =>
type === NodeType.Property ||
type === NodeType.MethodDefinition ||
type === NodeType.PropertyDefinition;
/**
* @param {number} node a node
* @returns {boolean} whether it is `#x in y`, which terser holds apart from binary expressions
*/
const isPrivateIn = (node) =>
typeOf(node) === NodeType.BinaryExpression &&
tree.type(tree.field(0, Field.left, node)) === NodeType.PrivateIdentifier;
/**
* @param {number} node a node
* @returns {boolean} whether it is terser's `Binary`
*/
const isBinary = (node) => {
const type = typeOf(node);
return (
(type === NodeType.BinaryExpression && !isPrivateIn(node)) ||
type === NodeType.LogicalExpression ||
type === NodeType.AssignmentExpression ||
type === NodeType.AssignmentPattern
);
};
/**
* @param {number} node a node
* @returns {boolean} whether it is terser's `UnaryPrefix`
*/
const isUnaryPrefix = (node) => {
const type = typeOf(node);
return (
type === NodeType.UnaryExpression ||
(type === NodeType.UpdateExpression && tree.flag(Flag.prefix, node))
);
};
/**
* @param {number} node a node
* @returns {boolean} whether it is terser's `UnaryPostfix`
*/
const isUnaryPostfix = (node) =>
typeOf(node) === NodeType.UpdateExpression && !tree.flag(Flag.prefix, node);
/**
* @param {number} node a node
* @returns {boolean} whether it is terser's `Dot` or `Sub`, not a private access
*/
const isDotOrSub = (node) =>
typeOf(node) === NodeType.MemberExpression &&
tree.type(tree.field(0, Field.property, node)) !==
NodeType.PrivateIdentifier;
/**
* @param {number} node a node
* @returns {boolean} whether it is a string literal, not a directive's
*/
const isStringLiteral = (node) =>
typeOf(node) === NodeType.Literal && typeof tree.value(node) === "string";
/**
* @param {number} node a node
* @returns {boolean} whether it is a number literal
*/
const isNumber = (node) =>
typeOf(node) === NodeType.Literal && typeof tree.value(node) === "number";
/**
* @param {number} node a statement
* @returns {boolean} whether it is a directive
*/
const isDirective = (node) =>
typeOf(node) === NodeType.ExpressionStatement &&
tree.textOf(Part.directive, node) !== null;
/**
* @param {number} node a statement
* @returns {boolean} whether it is a string standing alone, not a directive
*/
const isStringStatement = (node) =>
typeOf(node) === NodeType.ExpressionStatement &&
tree.textOf(Part.directive, node) === null &&
isStringLiteral(childOf(Field.expression, node));
/**
* @param {number} node an `export default`
* @returns {boolean} whether it exports a value, which terser's conversion holds apart from a named declaration
*/
const exportsValue = (node) => {
const declaration = tree.field(0, Field.declaration, node);
const type = tree.type(declaration);
return (
(type !== NodeType.FunctionDeclaration &&
type !== NodeType.ClassDeclaration) ||
tree.field(0, Field.id, declaration) === 0
);
};
/**
* @param {number} node a call or `new`
* @param {number} argument a node
* @returns {number} where the node is among the arguments, -1 for nowhere
*/
const argumentIndex = (node, argument) => {
const count = tree.fieldCount(Field.arguments, node);
for (let i = 0; i < count; i++) {
if (itemOf(i, Field.arguments, node) === argument) return i;
}
return -1;
};
/**
* @param {number} node a node
* @returns {number} the token it starts at, 0 for none
*/
const startTokenOf = (node) =>
node < 0 ? tokens.first : tokens.starts[node];
/**
* @param {number} node a node
* @returns {number} the token it ends at, 0 for none
*/
const endTokenOf = (node) => (node < 0 ? 0 : tokens.ends[node]);
// What the stream reads off the printed nodes for their comments, each
// answer `ESTREE_NODE_ACCESS`'s for terser's node.
/** @type {NodeAccess<number>} */
const treeNodeAccess = /** @type {NodeAccess<number>} */ ({
startToken: startTokenOf,
endToken: endTokenOf,
keywordValue: (/** @type {number} */ node) => {
switch (typeOf(node)) {
case NodeType.ReturnStatement:
case NodeType.ThrowStatement:
case NodeType.AwaitExpression:
case NodeType.YieldExpression:
return childOf(Field.argument, node) || undefined;
default:
return undefined;
}
},
leftEdgeChild: (/** @type {number} */ parent) => {
switch (typeOf(parent)) {
case NodeType.AwaitExpression:
case NodeType.YieldExpression:
return childOf(Field.argument, parent);
case NodeType.UpdateExpression:
return tree.flag(Flag.prefix, parent)
? undefined
: childOf(Field.argument, parent);
case NodeType.BinaryExpression:
return isPrivateIn(parent) ? undefined : childOf(Field.left, parent);
case NodeType.LogicalExpression:
case NodeType.AssignmentExpression:
return childOf(Field.left, parent);
case NodeType.CallExpression:
return childOf(Field.callee, parent);
case NodeType.ConditionalExpression:
return childOf(Field.test, parent);
case NodeType.MemberExpression:
return isDotOrSub(parent) ? childOf(Field.object, parent) : undefined;
case NodeType.SequenceExpression:
return itemOf(0, Field.expressions, parent);
default:
return undefined;
}
},
isStatement: (/** @type {number} */ node) =>
STATEMENT_TYPES[typeOf(node)] === 1
});
/**
* terser's `walk` stopped by `walk_abort`, over the tree.
* @param {number} node the node walked from
* @param {(node: number) => boolean | undefined} test true where found, false to skip a node's children
* @returns {boolean} whether `test` found a node
*/
const findNode = (node, test) => {
const found = test(node);
if (found !== undefined) return found;
const fields = CHILD_FIELDS[tree.type(node)];
for (let f = 0; f < fields.length; f++) {
const count = tree.fieldCount(fields[f], node);
for (let i = 0; i < count; i++) {
const child = unwrap(tree.field(i, fields[f], node));
if (child !== 0 && findNode(child, test)) return true;
}
}
return false;
};
/**
* What terser's walk for `new (a.b())` stops at: a call or `new` outside
* any function or class.
* @param {number} node a node walked
* @returns {boolean | undefined} true for a call, false for a scope
*/
const findCall = (node) => {
const type = typeOf(node);
if (isScopeType(type)) return false;
if (isCallType(type)) return true;
};
/**
* What terser's `parenthesize_for_noin` stops at: an `in` outside any
* function or class, arrows aside.
* @param {number} node a node walked
* @returns {boolean | undefined} true for an `in`, false for a scope
*/
const findIn = (node) => {
const type = typeOf(node);
if (isScopeType(type) && type !== NodeType.ArrowFunctionExpression) {
return false;
}
if (
type === NodeType.BinaryExpression &&
tree.textOf(Part.operator, node) === "in"
) {
return true;
}
};
/**
* terser's `first_in_statement`, over the parent stack.
* @param {boolean=} exportDefault whether an `export default` value it starts, but is not, counts
* @returns {boolean | undefined} whether the node printed starts its statement
*/
const firstInStatement = (exportDefault) => {
let node = stack[stack.length - 1];
let parent;
for (let i = 0; (parent = parentAt(i)); i++) {
const type = typeOf(parent);
if (
type === NodeType.ExpressionStatement &&
childOf(Field.expression, parent) === node
) {
return true;
}
if (
exportDefault &&
i > 0 &&
type === NodeType.ExportDefaultDeclaration &&
exportsValue(parent) &&
childOf(Field.declaration, parent) === node
) {
return true;
}
if (startsParent(parent, node)) {
node = parent;
} else {
return false;
}
}
return undefined;
};
/**
* @param {number} parent a node
* @param {number} child one of its children
* @returns {boolean} whether the child's first token is the parent's
*/
const startsParent = (parent, child) => {
switch (typeOf(parent)) {
case NodeType.SequenceExpression:
return itemOf(0, Field.expressions, parent) === child;
case NodeType.CallExpression:
return childOf(Field.callee, parent) === child;
case NodeType.TaggedTemplateExpression:
return childOf(Field.tag, parent) === child;
case NodeType.MemberExpression:
return isDotOrSub(parent) && childOf(Field.object, parent) === child;
case NodeType.ChainExpression:
return childOf(Field.expression, parent) === child;
case NodeType.ConditionalExpression:
return childOf(Field.test, parent) === child;
case NodeType.UpdateExpression:
return (
isUnaryPostfix(parent) && childOf(Field.argument, parent) === child
);
default:
return isBinary(parent) && childOf(Field.left, parent) === child;
}
};
/**
* terser's `left_is_object`.
* @param {number} node an expression
* @returns {boolean} whether its leftmost part is an object literal
*/
const leftIsObject = (node) => {
switch (typeOf(node)) {
case NodeType.ObjectExpression:
return true;
case NodeType.SequenceExpression:
return leftIsObject(itemOf(0, Field.expressions, node));
case NodeType.CallExpression:
return leftIsObject(childOf(Field.callee, node));
case NodeType.TaggedTemplateExpression:
return leftIsObject(childOf(Field.tag, node));
case NodeType.MemberExpression:
return isDotOrSub(node) && leftIsObject(childOf(Field.object, node));
case NodeType.ChainExpression:
return leftIsObject(childOf(Field.expression, node));
case NodeType.ConditionalExpression:
return leftIsObject(childOf(Field.test, node));
case NodeType.UpdateExpression:
return (
!tree.flag(Flag.prefix, node) &&
leftIsObject(childOf(Field.argument, node))
);
}
return isBinary(node) && leftIsObject(childOf(Field.left, node));
};
// Each printed type's code generator and parentheses test, `codegen`'s and
// `needsParens`'s with the `correct` and `improve` phases' wrappers folded in.
/** @type {((node: number) => void)[]} */
const generators = [];
/** @type {((node: number) => boolean | undefined)[]} */
const parensTests = [];
/**
* Prints a node with its parentheses and comments, as `printEstree` does.
* @param {number} node the node, or an import's attributes as the negated import
* @param {boolean=} forceParens whether to parenthesize it whatever its parent
* @returns {void}
*/
const printNode = (node, forceParens) => {
const type = typeOf(node);
const generate = generators[type];
if (isScopeType(type)) {
output.active_scope = node;
} else if (
!output.use_asm &&
type === NodeType.ExpressionStatement &&
tree.textOf(Part.directive, node) === "use asm"
) {
output.use_asm = output.active_scope;
}
stack.push(node);
const parens = parensTests[type];
const parenthesized =
forceParens === true || (parens !== undefined && parens(node) === true);
if (parenthesized) output.print("(");
output.prepend_comments(node);
if (output.sourceMap) {
const map = mappings[type];
if (map !== undefined) map(node);
}
generate(node);
output.append_comments(node);
if (parenthesized) output.print(")");
stack.pop();
if (node === output.use_asm) output.use_asm = null;
};
/**
* terser's `display_body` of the statements a node's `body` lists.
* @param {number} owner the program, block or static block
* @param {boolean} isToplevel whether they are the program's
* @param {boolean} allowDirectives whether leading strings are directives
* @returns {void}
*/
const displayBody = (owner, isToplevel, allowDirectives) => {
const count = tree.fieldCount(Field.body, owner);
output.in_directive = allowDirectives;
for (let i = 0; i < count; i++) {
const statement = tree.field(i, Field.body, owner);
const isEmpty = typeOf(statement) === NodeType.EmptyStatement;
if (
output.in_directive === true &&
!(isDirective(statement) || isEmpty || isStringStatement(statement))
) {
output.in_directive = false;
}
if (!isEmpty) printNode(statement);
// terser drops an empty statement's comments with it.
else output.prepend_comments(statement);
if (output.in_directive === true && isStringStatement(statement)) {
output.in_directive = false;
}
}
output.in_directive = false;
};
/**
* terser's `print_braced`.
* @param {number} owner the node the braces belong to
* @param {number} body the node whose `body` lists the statements
* @param {boolean} allowDirectives whether leading strings are directives
* @returns {void}
*/
const printBraced = (owner, body, allowDirectives) => {
if (tree.fieldCount(Field.body, body) === 0) {
printBracedEmpty(owner);
return;
}
output.print("{");
displayBody(body, false, allowDirectives);
output.add_mapping(endTokenOf(owner));
output.print("}");
};
/**
* terser's `print_braced_empty`.
* @param {number} owner the node the braces belong to
* @returns {void}
*/
const printBracedEmpty = (owner) => {
output.print("{");
output.append_comments(owner, true);
output.add_mapping(endTokenOf(owner));
output.print("}");
};
/**
* terser's `make_block`: a statement in braces.
* @param {number} statement the statement
* @returns {void}
*/
const makeBlock = (statement) => {
const type = typeOf(statement);
if (type === NodeType.EmptyStatement) {
output.print("{}");
} else if (type === NodeType.BlockStatement) {
printNode(statement);
} else {
output.print("{");
printNode(statement);
output.print("}");
}
};
/**
* terser's `force_statement`: a statement's body, braced where it must be.
* @param {number} statement the body
* @returns {void}
*/
const printMaybeBracedBody = (statement) => {
const type = typeOf(statement);
if (type === NodeType.EmptyStatement) {
output.force_semicolon();
} else if (
(type === NodeType.VariableDeclaration &&
tree.textOf(Part.kind, statement) !== "var") ||
isClassType(type)
) {
makeBlock(statement);
} else {
printNode(statement);
}
};
/**
* terser's `make_then`: an `if`'s consequent before an `else`, braced
* where an `if` without one inside would take that `else`.
* @param {number} node the `if`
* @returns {void}
*/
const makeThen = (node) => {
const consequent = tree.field(0, Field.consequent, node);
if (
output.option("ie8") &&
typeOf(consequent) === NodeType.DoWhileStatement
) {
makeBlock(consequent);
return;
}
let body = consequent;
for (;;) {
const type = typeOf(body);
if (type === NodeType.IfStatement) {
const alternate = tree.field(0, Field.alternate, body);
if (alternate === 0) {
makeBlock(consequent);
return;
}
body = alternate;
} else if (
type === NodeType.LabeledStatement ||
type === NodeType.WithStatement ||
type === NodeType.ForStatement ||
isForInType(type) ||
type === NodeType.WhileStatement ||
type === NodeType.DoWhileStatement
) {
body = tree.field(0, Field.body, body);
} else {
break;
}
}
printMaybeBracedBody(consequent);
};
/**
* terser's `Exit._do_print`: a `return` or `throw`.
* @param {number} node the statement
* @param {string} keyword its keyword
* @returns {void}
*/
const printExit = (node, keyword) => {
output.print(keyword);
const argument = childOf(Field.argument, node);
if (argument !== 0) {
output.space();
// Comments not yet printed may hold a line break before the value.
/** @type {Comment[] | undefined} */
const comments = output.tokenTables.commentsOf(
startTokenOf(argument),
false
);
const parenthesized =
comments &&
comments.length > 0 &&
!output.printed_comments.has(comments);
if (parenthesized) output.print("(");
// A string print reads no comment, so it parenthesizes the value
// whose comments were dropped here: what it counts differs.
const diverges =
recording !== null && !parenthesized && comments && comments.length > 0;
const before = diverges ? output.pos() : 0;
printNode(argument);
if (parenthesized) output.print(")");
if (diverges) {
const recorded = /** @type {MangleRecording} */ (recording);
recorded.counted.push("()");
if (output.codeAt(before) === 32) recorded.uncounted.push(" ");
}
}
output.semicolon();
};
/**
* terser's `LoopControl._do_print`: a `break` or `continue`.
* @param {number} node the statement
* @param {string} keyword its keyword
* @returns {void}
*/
const printLoopControl = (node, keyword) => {
output.print(keyword);
const label = tree.field(0, Field.label, node);
if (label !== 0) {
output.space();
printNode(label);
}
output.semicolon();
};
/**
* A parenthesized expression, as `with_parens` prints it.
* @param {number} node the expression
* @returns {void}
*/
const printParenthesized = (node) => {
output.print("(");
printNode(node);
output.print(")");
};
/**
* terser's `parenthesize_for_noin`.
* @param {number} node the node printed
* @param {boolean} noin whether `in` would be read as a `for` loop's
* @returns {void}
*/
const parenthesizeForNoin = (node, noin) => {
printNode(node, noin && findNode(node, findIn));
};
/**
* terser's `print_mapped_name`: the local side of an import or export.
* @param {number} node the name
* @returns {void}
*/
const printMappedName = (node) => {
if (tree.type(node) === NodeType.Literal) {
output.print_string(moduleName(node), quoteOf(node));
} else {
printNode(node);
}
};
/**
* terser's `print_foreign_name`: the other module's side, printed as written.
* @param {number} node the name
* @returns {void}
*/
const printForeignName = (node) => {
if (tree.type(node) === NodeType.Literal) {
output.print_string(moduleName(node), quoteOf(node));
} else {
output.print(tree.name(node));
}
};
/**
* terser's `NameMapping` printer for a named specifier.
* @param {number} local the local name
* @param {number} foreign the other module's name
* @param {boolean} isImport whether it is an import's
* @returns {void}
*/
const printNameMapping = (local, foreign, isImport) => {
const same = localName(local) === moduleName(foreign);
const definition = definitions === null ? null : definitions[local];
if (
recording === null ||
!isMinifierVariable(definition) ||
isUnmangleable(
definition,
/** @type {MangleOptions} */ (recordingOptions),
null
)
) {
if (same) {
printMappedName(local);
return;
}
if (isImport) printForeignName(foreign);
else printMappedName(local);
output.space();
output.print("as");
output.space();
if (isImport) printNode(local);
else printForeignName(foreign);
return;
}
// Mangling decides whether the names still match: the clause is a
// piece the patch writes or drops.
const start = output.pos();
if (isImport) {
if (!same) {
printForeignName(foreign);
output.space();
output.print("as");
output.space();
}
const index = recordSpecifier(start, 0, definition, foreign, true);
printNode(local);
// The local name's piece follows; the space before it is the clause's.
const recorded = recording;
recorded.lengths[index] = recorded.starts[index + 1] - start;
return;
}
printNode(local);
const clauseStart = output.pos();
if (!same) {
output.space();
output.print("as");
output.space();
printForeignName(foreign);
}
recordSpecifier(
clauseStart,
output.pos() - clauseStart,
definition,
foreign,
false
);
};
/**
* Records an import's or export's `as` clause, whose final shape mangling
* decides, as a piece.
* @param {number} start where the clause starts in the text
* @param {number} length how long it is, 0 where the names matched
* @param {MinifierVariable} local the local name's definition
* @param {number} foreign the other module's name
* @param {boolean} isImport whether the clause precedes the local name
* @returns {number} which piece it is
*/
const recordSpecifier = (start, length, local, foreign, isImport) => {
const recorded = /** @type {MangleRecording} */ (recording);
const index = recordPieceAt(recorded, start, length, local);
const name = moduleName(foreign);
recorded.specifiers.push({
index,
foreign: name,
text:
tree.type(foreign) === NodeType.Literal
? output.encode_string(name, quoteOf(foreign))
: name,
isImport
});
return index;
};
/**
* Named imports or exports in braces.
* @param {number} node the import or export
* @param {number} from the first specifier printed
* @returns {void}
*/
const printSpecifiers = (node, from) => {
const count = tree.fieldCount(Field.specifiers, node);
output.print("{");
for (let i = from; i < count; i++) {
output.space();
printNode(tree.field(i, Field.specifiers, node));
if (i < count - 1) output.print(",");
}
output.space();
output.print("}");
};
/**
* Import attributes, printed as the object terser holds them in.
* @param {number} node the import or export
* @returns {void}
*/
const printAttributes = (node) => {
if (tree.fieldCount(Field.attributes, node) === 0) return;
output.print("with");
printNode(-node);
};
/**
* An import's or export's source and attributes, past the specifiers.
* @param {number} node the declaration
* @returns {void}
*/
const printSource = (node) => {
const from = tree.field(0, Field.source, node);
if (from !== 0) {
output.space();
output.print("from");
output.space();
printNode(from);
}
printAttributes(node);
output.semicolon();
};
/**
* terser's `Class` printer.
* @param {number} node a class declaration or expression
* @returns {void}
*/
const printClass = (node) => {
output.print("class");
output.space();
const id = tree.field(0, Field.id, node);
if (id !== 0) {
printNode(id);
output.space();
}
const superClass = childOf(Field.superClass, node);
if (superClass !== 0) {
const type = typeOf(superClass);
const parens =
type !== NodeType.Identifier &&
type !== NodeType.MemberExpression &&
type !== NodeType.ClassExpression &&
type !== NodeType.FunctionExpression;
output.print("extends");
if (parens) output.print("(");
else output.space();
printNode(superClass);
if (parens) output.print(")");
else output.space();
}
const body = tree.field(0, Field.body, node);
const count = tree.fieldCount(Field.body, body);
if (count === 0) {
output.print("{}");
return;
}
output.print("{");
for (let i = 0; i < count; i++) printNode(tree.field(i, Field.body, body));
output.print("}");
};
/**
* terser's `Lambda._do_print`.
* @param {number} node the function
* @param {boolean} isMethod whether it is a method's value, printed without `function`
* @returns {void}
*/
const printLambda = (node, isMethod) => {
const id = tree.field(0, Field.id, node);
if (!isMethod) {
if (tree.flag(Flag.async, node)) {
output.print("async");
output.space();
}
output.print("function");
if (tree.flag(Flag.generator, node)) output.star();
if (id !== 0) output.space();
}
if (id !== 0) printNode(id);
printArguments(node, Field.params);
output.space();
printBraced(node, tree.field(0, Field.body, node), true);
};
/**
* @param {number} node a call, `new` or function
* @param {number} field its arguments or parameters
* @returns {void}
*/
const printArguments = (node, field) => {
const count = tree.fieldCount(field, node);
output.print("(");
for (let i = 0; i < count; i++) {
if (i) output.comma();
printNode(itemOf(i, field, node));
}
output.print(")");
};
/**
* A private member's name, which the correct phase prints as a name.
* @param {string} name the name, without its `#`
* @returns {void}
*/
const printPrivateName = (name) => {
output.print("#");
output.print_name(name);
};
/**
* terser's `_print_getter_setter`: a getter, setter or method.
* @param {number} node the property or method definition
* @param {string | undefined} keyword what precedes its name
* @returns {void}
*/
const printMethod = (node, keyword) => {
if (tree.flag(Flag.static, node)) {
output.print("static");
output.space();
}
if (keyword) {
output.print(keyword);
output.space();
}
const key = childOf(Field.key, node);
const isPrivate = tree.type(key) === NodeType.PrivateIdentifier;
if (tree.flag(Flag.computed, node) && !isPrivate) {
output.print("[");
printNode(key);
output.print("]");
} else {
if (isPrivate) printPrivateName(privateNameOf(key));
else printPropertyName(keyName(key), keyQuoteOf(node), output);
mapNameEnd(key);
}
printLambda(tree.field(0, Field.value, node), true);
};
/**
* @param {number} node a method's or property's
* @returns {string | undefined} the keyword its kind prints before its name
*/
const methodKeyword = (node) => {
const kind = tree.textOf(Part.kind, node);
if (kind === "get" || kind === "set") return kind;
const value = tree.field(0, Field.value, node);
const generator = tree.flag(Flag.generator, value);
const async = tree.flag(Flag.async, value);
if (generator && async) return "async*";
if (generator) return "*";
if (async) return "async";
};
/**
* terser's `ObjectKeyVal` printer: a property not a method.
* @param {number} node the property
* @returns {void}
*/
const printKeyValue = (node) => {
const key = childOf(Field.key, node);
const value = childOf(Field.value, node);
const quote = keyQuoteOf(node);
const name = tree.flag(Flag.computed, node) ? undefined : keyName(key);
const tryShorthand = Boolean(
output.option("shorthand") && name !== undefined
);
const valueType = typeOf(value);
if (
tryShorthand &&
valueType === NodeType.Identifier &&
(tokens.marks[value] & MARK_INFINITY) === 0 &&
printedName(value) === name &&
!ALL_RESERVED_WORDS.has(name)
) {
if (!printPropertyName(name, quote, output)) {
output.colon();
printNode(value);
}
} else if (
tryShorthand &&
valueType === NodeType.AssignmentPattern &&
typeOf(childOf(Field.left, value)) === NodeType.Identifier &&
printedName(childOf(Field.left, value)) === name
) {
if (!printPropertyName(name, quote, output)) {
output.colon();
printNode(childOf(Field.left, value));
}
output.space();
output.print("=");
output.space();
printNode(childOf(Field.right, value));
} else {
if (name !== undefined) {
printPropertyName(name, quote, output);
} else {
output.print("[");
printNode(key);
output.print("]");
}
output.colon();
printNode(value);
}
};
/**
* @param {number} node a property or import attribute
* @returns {boolean} whether it is a key and its value, not a method or accessor
*/
const isKeyValue = (node) =>
tree.type(node) === NodeType.ImportAttribute ||
(tree.textOf(Part.kind, node) === "init" && !tree.flag(Flag.method, node));
/**
* A pattern's or array's elements, a final hole followed by a comma of
* its own lest it read as a trailing comma.
* @param {number} node the array or pattern
* @param {number} field its elements or properties
* @returns {void}
*/
const printElements = (node, field) => {
const count = tree.fieldCount(field, node);
for (let i = 0; i < count; i++) {
const element = itemOf(i, field, node);
if (i) output.comma();
if (element !== 0) printNode(element);
else if (i === count - 1) output.comma();
}
};
/**
* terser's `Binary` printer.
* @param {number} node a binary, logical or assignment expression, or an assignment pattern
* @returns {void}
*/
const printBinary = (node) => {
const operator = operatorOf(node);
printNode(childOf(Field.left, node));
// `>`, `>=`, `>>` or `>>>` straight after `--` would read as `-->`.
if (operator[0] === ">" && output.last().endsWith("--")) {
output.print(" ");
} else {
output.space();
}
output.print(operator);
output.space();
printNode(childOf(Field.right, node));
};
/**
* terser's `Call` printer.
* @param {number} node a call or `new`
* @returns {void}
*/
const printCall = (node) => {
const callee = childOf(Field.callee, node);
printNode(callee);
if (
typeOf(node) === NodeType.NewExpression &&
tree.fieldCount(Field.arguments, node) === 0
) {
return;
}
const calleeType = typeOf(callee);
if (isCallType(calleeType) || isLambdaType(calleeType)) {
output.add_mapping(startTokenOf(node));
}
if (tree.flag(Flag.optional, node)) output.print("?.");
printArguments(node, Field.arguments);
};
/**
* terser's `UnaryPrefix` and `UnaryPostfix` printers.
* @param {number} node a unary or update expression
* @returns {void}
*/
const printUnary = (node) => {
const argument = childOf(Field.argument, node);
const operator = /** @type {string} */ (tree.textOf(Part.operator, node));
if (!tree.flag(Flag.prefix, node)) {
printNode(argument);
output.print(operator);
return;
}
// Lest it print `<!--`.
if (operator === "--" && output.last().endsWith("!")) output.print(" ");
output.print(operator);
if (
/^[a-z]/i.test(operator) ||
(/[+-]$/.test(operator) &&
isUnaryPrefix(argument) &&
/^[+-]/.test(
/** @type {string} */ (tree.textOf(Part.operator, argument))
))
) {
output.space();
}
printNode(argument);
};
/**
* terser's `RegExp` printer.
* @param {number} node a regular expression literal
* @returns {void}
*/
const printRegExp = (node) => {
const raw = /** @type {string} */ (tree.textOf(Part.raw, node));
const end = raw.lastIndexOf("/");
const flags = raw.slice(end + 1);
let pattern = regexpSourceFix(regexpSourceAsRead(raw.slice(1, end)));
pattern = pattern.replace(REGEXP_SLASH_SCRIPT, escapeSlashScript);
if (
REGEXP_STARTS_WITH_SCRIPT.test(pattern) &&
output.last().endsWith("<")
) {
output.print(" ");
}
output.print(
output.to_utf8(
`/${pattern}/${flags ? sortRegexpFlags(flags) : ""}`,
false,
true
)
);
const parent = parentAt(0);
if (
isBinary(parent) &&
/^\w/.test(operatorOf(parent)) &&
childOf(Field.left, parent) === node
) {
output.print(" ");
}
};
/**
* Whether a string printed here may be a template: not a directive, nor
* an import's or export's source or attribute, a tag, or asm.js.
* @returns {boolean} true when it may
*/
const takesTemplate = () => {
if (output.in_directive || output.use_asm) return false;
const parent = parentAt(0);
const parentType = typeOf(parent);
const holderType =
parentType === NodeType.Property ? typeOf(parentAt(2)) : parentType;
return !(
holderType === NodeType.ImportDeclaration ||
holderType === NodeType.ExportNamedDeclaration ||
holderType === NodeType.ExportAllDeclaration ||
parentType === NodeType.ImportExpression ||
parentType === NodeType.TaggedTemplateExpression ||
// After `await` or `yield` a template reads as a tag's where either
// names a variable, and a member's string prints apart from the rest.
parentType === NodeType.AwaitExpression ||
parentType === NodeType.YieldExpression ||
parentType === NodeType.MemberExpression
);
};
/**
* @param {number} node a node
* @returns {boolean} whether it is printed as the right side of `#x in`
*/
const isPrivateInRight = (node) => {
const parent = parentAt(0);
return isPrivateIn(parent) && childOf(Field.right, parent) === node;
};
/**
* terser's `needs_parens` of a unary, update or `await`, before what only
* one of them asks.
* @param {number} node the node
* @param {number} parent its parent
* @returns {boolean} whether it is parenthesized
*/
const isCalledOrAccessed = (node, parent) => {
const type = typeOf(parent);
return (
(type === NodeType.MemberExpression &&
childOf(Field.object, parent) === node) ||
(isCallType(type) && childOf(Field.callee, parent) === node)
);
};
/**
* @param {number} node an arrow
* @returns {void}
*/
const printArrow = (node) => {
const parent = parentAt(0);
const parentType = typeOf(parent);
const parenthesized =
(parentType === NodeType.BinaryExpression && !isPrivateIn(parent)) ||
parentType === NodeType.LogicalExpression ||
isUnaryType(parentType) ||
(isCallType(parentType) && childOf(Field.callee, parent) === node);
if (parenthesized) output.print("(");
if (tree.flag(Flag.async, node)) {
output.print("async");
output.space();
}
const first = itemOf(0, Field.params, node);
if (
tree.fieldCount(Field.params, node) === 1 &&
typeOf(first) === NodeType.Identifier
) {
printNode(first);
} else {
printArguments(node, Field.params);
}
output.space();
output.print("=>");
output.space();
const body = childOf(Field.body, node);
let returned = body;
let returns = typeOf(body) !== NodeType.BlockStatement;
if (!returns && tree.fieldCount(Field.body, body) === 1) {
const statement = tree.field(0, Field.body, body);
if (typeOf(statement) === NodeType.ReturnStatement) {
returns = true;
returned = childOf(Field.argument, statement);
}
}
if (!returns) {
printBraced(node, body, false);
} else if (returned === 0) {
output.print("{}");
} else if (leftIsObject(returned)) {
output.print("(");
printNode(returned);
output.print(")");
} else {
printNode(returned);
}
if (parenthesized) output.print(")");
};
/**
* terser's `add_source_map` of a method's or private property's name: its end.
* @param {number} key the name
* @returns {void}
*/
const mapNameEnd = (key) => {
if (!output.sourceMap) return;
const end = endTokenOf(key);
const token = output.tokenTables.of(end);
const tokenType = token && token.type;
if (tokenType === "name" || tokenType === "privatename") {
output.add_mapping(end, tree.name(key));
} else {
output.add_mapping(end);
}
};
/**
* terser's `add_source_map` of most nodes: the node's start.
* @param {number} node the node printed
* @returns {void}
*/
const mapStart = (node) => {
output.add_mapping(startTokenOf(node));
};
/**
* terser's `add_source_map` of a getter, setter or method, whose key maps the name.
* @param {number} node the method printed
* @returns {void}
*/
const mapMethodStart = (node) => {
output.add_mapping(startTokenOf(node), false);
};
/**
* The name a property's mapping carries, as terser's `add_mapping` reads it
* off the key: a symbol's name, or the string its member starts with.
* @param {number} member a property, import attribute or class field
* @returns {string | null} the name, null for none
*/
const mapNameOf = (member) => {
const key = childOf(Field.key, member);
const computed = tree.flag(Flag.computed, member);
// terser's `SymbolProperty`, the key of an object's or pattern's property.
if (!computed && tree.type(member) !== NodeType.PropertyDefinition) {
return keyName(key);
}
const keyType = typeOf(key);
if (
computed &&
!(
(keyType === NodeType.Identifier &&
(tokens.marks[key] & MARK_INFINITY) === 0) ||
keyType === NodeType.ThisExpression ||
keyType === NodeType.Super
)
) {
return null;
}
const start = output.tokenTables.of(startTokenOf(member));
if (start && start.type === "string") return start.value;
if (keyType === NodeType.ThisExpression) return "this";
if (keyType === NodeType.Super) return "super";
return keyName(key);
};
/**
* terser's `add_source_map` of a property, named by its key.
* @param {number} node the property printed
* @returns {void}
*/
const mapProperty = (node) => {
const name = mapNameOf(node);
output.add_mapping(startTokenOf(node), name === null ? undefined : name);
};
// Each printed type's mapping, as terser's class maps it; a type absent here
// is one terser maps nowhere, its children mapping themselves.
/** @type {((node: number) => void)[]} */
const mappings = [];
for (const type of [
NodeType.ArrayExpression,
NodeType.ArrowFunctionExpression,
NodeType.BreakStatement,
NodeType.CatchClause,
NodeType.ClassDeclaration,
NodeType.ClassExpression,
NodeType.ContinueStatement,
NodeType.DebuggerStatement,
NodeType.DoWhileStatement,
NodeType.ForInStatement,
NodeType.ForOfStatement,
NodeType.ForStatement,
NodeType.FunctionDeclaration,
NodeType.FunctionExpression,
NodeType.Identifier,
NodeType.IfStatement,
NodeType.Literal,
NodeType.NewExpression,
NodeType.ObjectExpression,
NodeType.ReturnStatement,
NodeType.Super,
NodeType.SwitchCase,
NodeType.SwitchStatement,
NodeType.TemplateElement,
NodeType.TemplateLiteral,
NodeType.ThisExpression,
NodeType.ThrowStatement,
NodeType.TryStatement,
NodeType.VariableDeclaration,
NodeType.WhileStatement,
NodeType.WithStatement
]) {
mappings[type] = mapStart;
}
mappings[NodeType.BlockStatement] = (node) => {
// terser's try block is no BlockStatement, and maps nowhere.
const parent = parentAt(0);
if (
typeOf(parent) !== NodeType.TryStatement ||
tree.field(0, Field.block, parent) !== node
) {
mapStart(node);
}
};
mappings[NodeType.ExpressionStatement] = (node) => {
// A directive maps; terser's SimpleStatement does not.
if (tree.textOf(Part.directive, node) !== null) mapStart(node);
};
mappings[NodeType.MethodDefinition] = mapMethodStart;
mappings[NodeType.PrivateIdentifier] = mapNameEnd;
mappings[NodeType.Property] = (node) => {
if (isKeyValue(node)) mapProperty(node);
else mapMethodStart(node);
};
mappings[NodeType.PropertyDefinition] = mapProperty;
generators[NodeType.Program] = (node) => {
/** @type {(stream: OutputStream<PrintNode | number>, text: string, print: () => void) => void} */ (
modules.printWithLineBreaksOf
)(output, source, () => {
displayBody(node, true, true);
output.print("");
});
};
generators[NodeType.ExpressionStatement] = (node) => {
const directive = tree.textOf(Part.directive, node);
const expression = childOf(Field.expression, node);
if (directive !== null) {
const value = /** @type {string} */ (tree.value(expression));
// Printed as spelled where it holds escapes, as `printEstree` prints it.
if (directive !== value) {
const quote = quoteOf(expression);
const spelled = output.option("inline_script")
? inlineScriptSpelling(directive)
: directive;
output.print(`${quote}${spelled}${quote}`);
} else {
output.print_string(value, quoteOf(expression));
}
} else {
printNode(expression);
}
output.semicolon();
};
generators[NodeType.BlockStatement] = (node) => {
const parent = parentAt(0);
// terser's `Finally` is the block, so prints its keyword too.
if (
typeOf(parent) === NodeType.TryStatement &&
tree.field(0, Field.finalizer, parent) === node
) {
output.print("finally");
output.space();
}
printBraced(node, node, false);
};
generators[NodeType.EmptyStatement] = () => {
output.semicolon();
};
generators[NodeType.DebuggerStatement] = () => {
output.print("debugger");
output.semicolon();
};
generators[NodeType.LabeledStatement] = (node) => {
printNode(tree.field(0, Field.label, node));
output.colon();
const body = tree.field(0, Field.body, node);
// Without `semicolons`, terser defers an empty body's `;` to a line
// break, which leaves the label with no statement.
if (typeOf(body) === NodeType.EmptyStatement) output.force_semicolon();
else printNode(body);
};
generators[NodeType.DoWhileStatement] = (node) => {
output.print("do");
output.space();
makeBlock(tree.field(0, Field.body, node));
output.space();
output.print("while");
output.space();
printParenthesized(childOf(Field.test, node));
output.semicolon();
};
generators[NodeType.WhileStatement] = (node) => {
output.print("while");
output.space();
printParenthesized(childOf(Field.test, node));
output.space();
printMaybeBracedBody(tree.field(0, Field.body, node));
};
generators[NodeType.ForStatement] = (node) => {
const init = childOf(Field.init, node);
const test = childOf(Field.test, node);
const update = childOf(Field.update, node);
output.print("for");
output.space();
output.print("(");
if (init !== 0) {
if (typeOf(init) === NodeType.VariableDeclaration) printNode(init);
else parenthesizeForNoin(init, true);
output.print(";");
output.space();
} else {
output.print(";");
}
if (test !== 0) {
printNode(test);
output.print(";");
output.space();
} else {
output.print(";");
}
if (update !== 0) printNode(update);
output.print(")");
output.space();
printMaybeBracedBody(tree.field(0, Field.body, node));
};
generators[NodeType.ForInStatement] = generators[NodeType.ForOfStatement] = (
node
) => {
output.print("for");
if (tree.flag(Flag.await, node)) {
output.space();
output.print("await");
}
output.space();
output.print("(");
printNode(childOf(Field.left, node));
output.space();
output.print(typeOf(node) === NodeType.ForOfStatement ? "of" : "in");
output.space();
printNode(childOf(Field.right, node));
output.print(")");
output.space();
printMaybeBracedBody(tree.field(0, Field.body, node));
};
generators[NodeType.WithStatement] = (node) => {
output.print("with");
output.space();
printParenthesized(childOf(Field.object, node));
output.space();
printMaybeBracedBody(tree.field(0, Field.body, node));
};
generators[NodeType.ReturnStatement] = (node) => {
printExit(node, "return");
};
generators[NodeType.ThrowStatement] = (node) => {
printExit(node, "throw");
};
generators[NodeType.BreakStatement] = (node) => {
printLoopControl(node, "break");
};
generators[NodeType.ContinueStatement] = (node) => {
printLoopControl(node, "continue");
};
generators[NodeType.IfStatement] = (node) => {
const alternate = tree.field(0, Field.alternate, node);
output.print("if");
output.space();
printParenthesized(childOf(Field.test, node));
output.space();
if (alternate === 0) {
printMaybeBracedBody(tree.field(0, Field.consequent, node));
return;
}
makeThen(node);
output.space();
output.print("else");
output.space();
if (typeOf(alternate) === NodeType.IfStatement) printNode(alternate);
else printMaybeBracedBody(alternate);
};
generators[NodeType.SwitchStatement] = (node) => {
const count = tree.fieldCount(Field.cases, node);
output.print("switch");
output.space();
printParenthesized(childOf(Field.discriminant, node));
output.space();
if (count === 0) {
printBracedEmpty(node);
return;
}
output.print("{");
for (let i = 0; i < count; i++) printNode(tree.field(i, Field.cases, node));
output.print("}");
};
generators[NodeType.SwitchCase] = (node) => {
const test = childOf(Field.test, node);
if (test !== 0) {
output.print("case");
output.space();
printNode(test);
output.print(":");
} else {
output.print("default:");
}
const count = tree.fieldCount(Field.consequent, node);
for (let i = 0; i < count; i++) {
printNode(tree.field(i, Field.consequent, node));
}
};
generators[NodeType.TryStatement] = (node) => {
output.print("try");
output.space();
printNode(tree.field(0, Field.block, node));
const handler = tree.field(0, Field.handler, node);
if (handler !== 0) {
output.space();
printNode(handler);
}
const finalizer = tree.field(0, Field.finalizer, node);
if (finalizer !== 0) {
output.space();
printNode(finalizer);
}
};
generators[NodeType.CatchClause] = (node) => {
output.print("catch");
const param = tree.field(0, Field.param, node);
if (param !== 0) {
output.space();
printParenthesized(param);
}
output.space();
printBraced(node, tree.field(0, Field.body, node), false);
};
generators[NodeType.VariableDeclaration] = (node) => {
const count = tree.fieldCount(Field.declarations, node);
output.print(/** @type {string} */ (tree.textOf(Part.kind, node)));
output.space();
for (let i = 0; i < count; i++) {
if (i) output.comma();
printNode(tree.field(i, Field.declarations, node));
}
const parent = parentAt(0);
const parentType = typeOf(parent);
const isLoopHead =
parentType === NodeType.ForStatement
? childOf(Field.init, parent) === node
: isForInType(parentType) && childOf(Field.left, parent) === node;
if (!isLoopHead) output.semicolon();
};
generators[NodeType.VariableDeclarator] = (node) => {
printNode(tree.field(0, Field.id, node));
const init = childOf(Field.init, node);
if (init === 0) return;
output.space();
output.print("=");
output.space();
const grandparentType = typeOf(parentAt(1));
parenthesizeForNoin(
init,
grandparentType === NodeType.ForStatement || isForInType(grandparentType)
);
};
generators[NodeType.ImportDeclaration] = (node) => {
const count = tree.fieldCount(Field.specifiers, node);
const first = tree.field(0, Field.specifiers, node);
const hasDefault =
first !== 0 && typeOf(first) === NodeType.ImportDefaultSpecifier;
const named = hasDefault ? count - 1 : count;
output.print("import");
output.space();
const phase = tree.textOf(Part.phase, node);
if (phase) {
output.print(phase);
output.space();
}
if (hasDefault) printNode(tree.field(0, Field.local, first));
if (named > 0) {
if (hasDefault) {
output.print(",");
output.space();
}
const firstNamed = tree.field(hasDefault ? 1 : 0, Field.specifiers, node);
if (typeOf(firstNamed) === NodeType.ImportNamespaceSpecifier) {
printNode(firstNamed);
} else {
printSpecifiers(node, hasDefault ? 1 : 0);
}
}
if (hasDefault || named > 0) {
output.space();
output.print("from");
output.space();
}
printNode(tree.field(0, Field.source, node));
printAttributes(node);
output.semicolon();
};
generators[NodeType.ImportSpecifier] = (node) => {
printNameMapping(
tree.field(0, Field.local, node),
tree.field(0, Field.imported, node),
true
);
};
generators[NodeType.ImportNamespaceSpecifier] = (node) => {
output.print("*");
output.space();
output.print("as");
output.space();
printNode(tree.field(0, Field.local, node));
};
generators[NodeType.ExportSpecifier] = (node) => {
printNameMapping(
tree.field(0, Field.local, node),
tree.field(0, Field.exported, node),
false
);
};
generators[NodeType.ExportNamedDeclaration] = (node) => {
output.print("export");
output.space();
const declaration = tree.field(0, Field.declaration, node);
if (declaration !== 0) {
printNode(declaration);
return;
}
printSpecifiers(node, 0);
printSource(node);
};
generators[NodeType.ExportAllDeclaration] = (node) => {
output.print("export");
output.space();
output.print("*");
const exported = tree.field(0, Field.exported, node);
// Without a name, terser's `export *` holds an unquoted `*`.
if (exported !== 0) {
output.space();
output.print("as");
output.space();
printForeignName(exported);
}
printSource(node);
};
generators[NodeType.ExportDefaultDeclaration] = (node) => {
const declaration = childOf(Field.declaration, node);
const type = typeOf(declaration);
output.print("export");
output.space();
output.print("default");
output.space();
// terser writes a named function or class expression bare, which reads
// back as a declaration binding its name in the module.
if (
exportsValue(node) &&
(type === NodeType.FunctionExpression ||
type === NodeType.ClassExpression) &&
tree.field(0, Field.id, declaration) !== 0
) {
output.with_parens(() => printNode(declaration));
output.semicolon();
return;
}
printNode(declaration);
if (
!isClassType(type) &&
type !== NodeType.FunctionExpression &&
type !== NodeType.FunctionDeclaration
) {
output.semicolon();
}
};
generators[NodeType.Identifier] = (node) => {
if (recording !== null) {
recordName(node);
return;
}
output.print_name(printedName(node));
if (countOptions === null) return;
// Mangling renames it, so the letters counted leave it out.
const definition = /** @type {TreeDefinition[]} */ (definitions)[node];
if (
definition &&
!(
isMinifierVariable(definition) &&
isUnmangleable(definition, countOptions, null)
)
) {
/** @type {Required<IdentifierSource>} */ (
countOptions.nth_identifier
).consider(tree.name(node), -1);
}
};
/**
* Prints a name as written, recording it as a piece where mangling may
* rename it, its name left out of the letters counted.
* @param {number} node an identifier
* @returns {void}
*/
const recordName = (node) => {
const printed = output.identifierToUtf8(printedName(node), true);
output.print(printed);
const definition = /** @type {TreeDefinition[]} */ (definitions)[node];
if (
!definition ||
(isMinifierVariable(definition) &&
isUnmangleable(
definition,
/** @type {MangleOptions} */ (recordingOptions),
null
))
) {
return;
}
recordPieceAt(
/** @type {MangleRecording} */ (recording),
output.pos() - printed.length,
-printed.length,
definition
);
};
generators[NodeType.PrivateIdentifier] = (node) => {
output.print(`#${privateNameOf(node)}`);
};
generators[NodeType.Literal] = (node) => {
const value = tree.value(node);
if (typeof value === "string") {
// A string holding a line break, as a template where its program says.
const template =
value.includes("\n") && takesTemplate()
? /** @type {(stream: OutputStream<PrintNode | number>, value: string) => string | undefined} */ (
modules.lineBreakTemplateOf
)(output, value)
: undefined;
if (template !== undefined) output.print(template);
else output.print_string(value, quoteOf(node), output.in_directive);
return;
}
const raw = tree.textOf(Part.raw, node);
if (typeof value === "number") {
output.print(
(output.option("keep_numbers") || output.use_asm) && raw
? raw
: makeNum(value)
);
} else if (typeof value === "bigint") {
const digits = /** @type {string} */ (raw).slice(0, -1).replace(/_/g, "");
output.print(output.option("keep_numbers") && raw ? raw : `${digits}n`);
return;
} else if (raw !== null && raw.charCodeAt(0) === 47) {
printRegExp(node);
} else {
output.print(String(value));
return;
}
// An `in` or `instanceof` after it is spaced apart from what it ends in.
literalEnd = output.currentPosition;
};
generators[NodeType.ThisExpression] = () => {
output.print("this");
};
generators[NodeType.Super] = () => {
output.print("super");
};
generators[NodeType.MetaProperty] = (node) => {
output.print(
tree.name(tree.field(0, Field.meta, node)) === "import"
? "import.meta"
: "new.target"
);
};
generators[NodeType.ArrayExpression] = (node) => {
const count = tree.fieldCount(Field.elements, node);
output.print("[");
if (count > 0) output.space();
printElements(node, Field.elements);
if (count > 0) output.space();
output.print("]");
};
generators[NodeType.ArrayPattern] = (node) => {
output.print("[");
printElements(node, Field.elements);
output.print("]");
};
generators[NodeType.ObjectPattern] = (node) => {
output.print("{");
printElements(node, Field.properties);
output.print("}");
};
generators[NodeType.ObjectExpression] = (node) => {
// An import's attributes print as the object terser holds them in.
const owner = node < 0 ? -node : node;
const field = node < 0 ? Field.attributes : Field.properties;
const count = tree.fieldCount(field, owner);
if (count === 0) {
printBracedEmpty(node);
return;
}
output.print("{");
for (let i = 0; i < count; i++) {
if (i) output.print(",");
printNode(tree.field(i, field, owner));
}
output.print("}");
};
generators[NodeType.Property] = (node) => {
const keyValue = isKeyValue(node);
const key = childOf(Field.key, node);
const parent = parentAt(0);
// `{ __proto__ }` is an own property and `{ __proto__: x }` the prototype, a
// difference terser's tree keeps only in the tokens, so print by them.
if (
!keyValue ||
tree.flag(Flag.computed, node) ||
keyName(key) !== "__proto__" ||
typeOf(parent) !== NodeType.ObjectExpression
) {
if (keyValue) printKeyValue(node);
else printMethod(node, methodKeyword(node));
return;
}
const value = childOf(Field.value, node);
const asShorthand =
typeOf(value) === NodeType.Identifier &&
(tokens.marks[value] & MARK_INFINITY) === 0 &&
printedName(value) === "__proto__" &&
output.option("shorthand");
const start = startTokenOf(node);
if (start !== 0 && start === endTokenOf(node)) {
if (asShorthand) {
output.print_name("__proto__");
} else {
output.with_square(() => output.print_string("__proto__"));
output.colon();
printNode(value);
}
return;
}
if (!asShorthand) {
printKeyValue(node);
return;
}
output.print_name("__proto__");
output.colon();
printNode(value);
};
generators[NodeType.MethodDefinition] = (node) => {
printMethod(node, methodKeyword(node));
};
generators[NodeType.PropertyDefinition] = (node) => {
if (tree.flag(Flag.static, node)) {
output.print("static");
output.space();
}
const key = childOf(Field.key, node);
if (tree.type(key) === NodeType.PrivateIdentifier) {
printPrivateName(privateNameOf(key));
} else if (tree.flag(Flag.computed, node)) {
output.print("[");
printNode(key);
output.print("]");
} else {
printPropertyName(keyName(key), keyQuoteOf(node), output);
}
const value = childOf(Field.value, node);
if (value !== 0) {
output.print("=");
printNode(value);
}
output.semicolon();
};
generators[NodeType.StaticBlock] = (node) => {
output.print("static");
output.space();
printBraced(node, node, false);
};
generators[NodeType.SpreadElement] = generators[NodeType.RestElement] = (
node
) => {
output.print("...");
printNode(childOf(Field.argument, node));
};
generators[NodeType.AssignmentPattern] = printBinary;
generators[NodeType.AssignmentExpression] = printBinary;
generators[NodeType.LogicalExpression] = printBinary;
generators[NodeType.BinaryExpression] = (node) => {
const operator = tree.textOf(Part.operator, node);
if (isPrivateIn(node)) {
printNode(tree.field(0, Field.left, node));
output.space();
output.print("in");
output.space();
printNode(childOf(Field.right, node));
return;
}
if (operator !== "in" && operator !== "instanceof") {
printBinary(node);
return;
}
// A number or regular expression ending in `.` or `/` would take the
// operator for its fraction or flags.
printNode(childOf(Field.left, node));
if (literalEnd === output.currentPosition && /[/.]$/.test(output.last())) {
output.print(" ");
} else {
output.space();
}
output.print(operator);
output.space();
printNode(childOf(Field.right, node));
};
generators[NodeType.UnaryExpression] = printUnary;
generators[NodeType.UpdateExpression] = printUnary;
generators[NodeType.ConditionalExpression] = (node) => {
printNode(childOf(Field.test, node));
output.space();
output.print("?");
output.space();
printNode(childOf(Field.consequent, node));
output.space();
output.colon();
printNode(childOf(Field.alternate, node));
};
generators[NodeType.SequenceExpression] = (node) => {
const count = tree.fieldCount(Field.expressions, node);
for (let i = 0; i < count; i++) {
if (i > 0) output.comma();
printNode(itemOf(i, Field.expressions, node));
}
};
generators[NodeType.CallExpression] = printCall;
generators[NodeType.NewExpression] = (node) => {
output.print("new");
output.space();
printCall(node);
};
generators[NodeType.ImportExpression] = (node) => {
const phase = tree.textOf(Part.phase, node);
output.print(phase ? `import.${phase}` : "import");
output.print("(");
printNode(childOf(Field.source, node));
const options = childOf(Field.options, node);
if (options !== 0) {
output.comma();
printNode(options);
}
output.print(")");
};
generators[NodeType.ChainExpression] = (node) => {
printNode(childOf(Field.expression, node));
};
generators[NodeType.MemberExpression] = (node) => {
const object = childOf(Field.object, node);
const property = childOf(Field.property, node);
const optional = tree.flag(Flag.optional, node);
printNode(object);
if (tree.flag(Flag.computed, node)) {
if (optional) output.print("?.");
output.print("[");
printNode(property);
output.print("]");
return;
}
if (tree.type(property) === NodeType.PrivateIdentifier) {
if (optional) output.print("?");
output.print(".#");
output.add_mapping(endTokenOf(node));
output.print_name(privateNameOf(property));
return;
}
const name = tree.name(property);
const printComputed = /** @type {boolean} */ (
ALL_RESERVED_WORDS.has(name)
? output.option("ie8")
: !isIdentifierString(
name,
output.option("ecma") >= 2015 && !output.option("safari10")
)
);
if (optional) output.print("?.");
if (printComputed) {
output.print("[");
output.add_mapping(endTokenOf(node));
output.print_string(name);
output.print("]");
return;
}
// `1..x`: a dot straight after an integer literal is its fraction's; an
// octal literal kept as written takes no fraction.
if (
isNumber(object) &&
/** @type {number} */ (tree.value(object)) >= 0 &&
!/[xa-f.)]/i.test(output.last()) &&
!/^0(?:o|[0-7]+$)/i.test(output.last())
) {
output.print(".");
}
if (!optional) output.print(".");
output.add_mapping(endTokenOf(node));
output.print_name(name);
};
generators[NodeType.TemplateLiteral] = (node) => {
const isTagged = typeOf(parentAt(0)) === NodeType.TaggedTemplateExpression;
const count = tree.fieldCount(Field.quasis, node);
const expressions = tree.fieldCount(Field.expressions, node);
output.print("`");
for (let i = 0; i < count; i++) {
const { raw, cooked } = /** @type {{ raw: string, cooked: string }} */ (
tree.value(tree.field(i, Field.quasis, node))
);
const text = isTagged ? raw : cooked;
if (isTagged) output.print(text);
else output.print_template_string_chars(text);
if (i < expressions) {
output.print("${");
printNode(itemOf(i, Field.expressions, node));
output.print("}");
}
}
output.print("`");
};
generators[NodeType.TaggedTemplateExpression] = (node) => {
const tag = childOf(Field.tag, node);
const type = typeOf(tag);
// terser's list of tags it parenthesizes misses these, and each printed
// bare reads the template as part of its operand, or stops parsing.
if (
type === NodeType.YieldExpression ||
type === NodeType.AwaitExpression ||
type === NodeType.ChainExpression ||
isPrivateIn(tag)
) {
printNode(tag, true);
printNode(tree.field(0, Field.quasi, node));
return;
}
const parenthesizeTag =
isLambdaType(type) ||
isBinary(tag) ||
type === NodeType.ConditionalExpression ||
type === NodeType.SequenceExpression ||
isUnaryType(type) ||
(type === NodeType.MemberExpression &&
!tree.flag(Flag.computed, tag) &&
tree.type(tree.field(0, Field.property, tag)) === NodeType.Identifier &&
typeOf(childOf(Field.object, tag)) === NodeType.ObjectExpression);
if (parenthesizeTag) output.print("(");
printNode(tag);
if (parenthesizeTag) output.print(")");
printNode(tree.field(0, Field.quasi, node));
};
generators[NodeType.AwaitExpression] = (node) => {
output.print("await");
output.space();
const argument = childOf(Field.argument, node);
const type = typeOf(argument);
const parens = !(
isCallType(type) ||
type === NodeType.Identifier ||
type === NodeType.MemberExpression ||
isUnaryType(type) ||
type === NodeType.Literal ||
type === NodeType.AwaitExpression ||
type === NodeType.ObjectExpression
);
if (parens) output.print("(");
printNode(argument);
if (parens) output.print(")");
};
generators[NodeType.YieldExpression] = (node) => {
output.print(tree.flag(Flag.delegate, node) ? "yield*" : "yield");
const argument = childOf(Field.argument, node);
if (argument !== 0) {
output.space();
printNode(argument);
}
};
generators[NodeType.FunctionExpression] = generators[
NodeType.FunctionDeclaration
] = (node) => {
printLambda(node, false);
};
generators[NodeType.ArrowFunctionExpression] = (node) => {
if (!isPrivateInRight(node)) {
printArrow(node);
return;
}
output.print("(");
printArrow(node);
output.print(")");
};
generators[NodeType.ClassExpression] = printClass;
generators[NodeType.ClassDeclaration] = printClass;
/**
* terser's `needs_parens` of a binary or logical expression or an
* assignment pattern.
* @param {number} node the node
* @returns {boolean | undefined} whether it is parenthesized
*/
const binaryNeedsParens = (node) => {
const parent = parentAt(0);
const parentType = typeOf(parent);
if (isUnaryType(parentType) || isCalledOrAccessed(node, parent)) {
return true;
}
const operator = operatorOf(node);
if (isBinary(parent)) {
const parentOperator = operatorOf(parent);
// `??` mixed with `||` or `&&` is a syntax error without parens.
if (
(operator === "??" &&
(parentOperator === "||" || parentOperator === "&&")) ||
(parentOperator === "??" && (operator === "||" || operator === "&&"))
) {
return true;
}
const parentPrecedence = PRECEDENCE[parentOperator];
const precedence = PRECEDENCE[operator];
if (
parentPrecedence > precedence ||
(parentPrecedence === precedence &&
(node === childOf(Field.right, parent) || parentOperator === "**"))
) {
return true;
}
}
if (isPrivateIn(parent)) {
const parentPrecedence = PRECEDENCE.in;
const precedence = PRECEDENCE[operator];
if (
parentPrecedence > precedence ||
(parentPrecedence === precedence &&
node === childOf(Field.right, parent))
) {
return true;
}
}
};
/**
* terser's `needs_parens` of a private `in`.
* @param {number} node the node
* @returns {boolean | undefined} whether it is parenthesized
*/
const privateInNeedsParens = (node) => {
const parent = parentAt(0);
if (isUnaryType(typeOf(parent)) || isCalledOrAccessed(node, parent)) {
return true;
}
if (isBinary(parent)) {
const parentOperator = operatorOf(parent);
const parentPrecedence = PRECEDENCE[parentOperator];
const precedence = PRECEDENCE.in;
if (
parentPrecedence > precedence ||
(parentPrecedence === precedence &&
(node === childOf(Field.right, parent) || parentOperator === "**"))
) {
return true;
}
}
if (isPrivateIn(parent) && node === childOf(Field.right, parent)) {
return true;
}
};
/**
* terser's `needs_parens` of an assignment or a conditional, and the right
* side of a private `in`.
* @param {number} node the node
* @returns {boolean | undefined} whether it is parenthesized
*/
const assignNeedsParens = (node) => {
if (isPrivateInRight(node)) return true;
const parent = parentAt(0);
const parentType = typeOf(parent);
if (
isUnaryType(parentType) ||
(isBinary(parent) && parentType !== NodeType.AssignmentExpression) ||
isCalledOrAccessed(node, parent) ||
(parentType === NodeType.ConditionalExpression &&
childOf(Field.test, parent) === node)
) {
return true;
}
if (typeOf(node) !== NodeType.AssignmentExpression) return;
const leftType = typeOf(tree.field(0, Field.left, node));
// `({a, b} = c)`, an object destructuring assignment.
if (leftType === NodeType.ObjectPattern) return true;
// V8 reads the arguments before a bare destructuring assignment as one
// pattern and refuses `f(a += 1, [b] = c)`, which the spec allows.
return (
leftType === NodeType.ArrayPattern &&
isCallType(parentType) &&
argumentIndex(parent, node) > 0
);
};
/**
* terser's `needs_parens` of a unary or update expression.
* @param {number} node the node
* @returns {boolean} whether it is parenthesized
*/
const unaryNeedsParens = (node) => {
const parent = parentAt(0);
if (isCalledOrAccessed(node, parent)) return true;
if (!isBinary(parent) || operatorOf(parent) !== "**") return false;
const operator = tree.textOf(Part.operator, node);
return (
isUnaryPrefix(node) &&
childOf(Field.left, parent) === node &&
operator !== "++" &&
operator !== "--"
);
};
parensTests[NodeType.FunctionExpression] = (node) => {
if (!output.has_parens() && firstInStatement(true)) return true;
const parent = parentAt(0);
const parentType = typeOf(parent);
if (
output.option("webkit") &&
parentType === NodeType.MemberExpression &&
childOf(Field.object, parent) === node
) {
return true;
}
if (
output.option("wrap_iife") &&
isCallType(parentType) &&
childOf(Field.callee, parent) === node
) {
return true;
}
return (
output.option("wrap_func_args") &&
isCallType(parentType) &&
argumentIndex(parent, node) !== -1
);
};
parensTests[NodeType.ArrowFunctionExpression] = (node) => {
const parent = parentAt(0);
const parentType = typeOf(parent);
if (
output.option("wrap_func_args") &&
isCallType(parentType) &&
argumentIndex(parent, node) !== -1
) {
return true;
}
return (
(parentType === NodeType.MemberExpression &&
childOf(Field.object, parent) === node) ||
(parentType === NodeType.ConditionalExpression &&
childOf(Field.test, parent) === node)
);
};
// Otherwise `{...}` would read as a block.
parensTests[NodeType.ObjectExpression] = () =>
!output.has_parens() && firstInStatement();
parensTests[NodeType.ClassExpression] = () => firstInStatement(true);
parensTests[NodeType.UnaryExpression] = unaryNeedsParens;
parensTests[NodeType.UpdateExpression] = unaryNeedsParens;
parensTests[NodeType.AwaitExpression] = (node) => {
const parent = parentAt(0);
return (
isCalledOrAccessed(node, parent) ||
(isBinary(parent) &&
tree.textOf(Part.operator, parent) === "**" &&
childOf(Field.left, parent) === node) ||
(output.option("safari10") && isUnaryPrefix(parent))
);
};
parensTests[NodeType.SequenceExpression] = (node) => {
if (isPrivateInRight(node)) return true;
const parent = parentAt(0);
const type = typeOf(parent);
return (
isCallType(type) ||
isUnaryType(type) ||
isBinary(parent) ||
type === NodeType.VariableDeclarator ||
(type === NodeType.MemberExpression &&
node !== childOf(Field.property, parent)) ||
type === NodeType.ArrayExpression ||
isObjectPropertyType(type) ||
type === NodeType.ConditionalExpression ||
type === NodeType.ArrowFunctionExpression ||
type === NodeType.SpreadElement ||
type === NodeType.RestElement ||
(type === NodeType.ForOfStatement &&
node === childOf(Field.right, parent)) ||
type === NodeType.YieldExpression ||
type === NodeType.ImportExpression ||
type === NodeType.ExportNamedDeclaration ||
type === NodeType.ExportDefaultDeclaration ||
type === NodeType.ExportAllDeclaration
);
};
parensTests[NodeType.BinaryExpression] = (node) =>
isPrivateIn(node) ? privateInNeedsParens(node) : binaryNeedsParens(node);
parensTests[NodeType.LogicalExpression] = binaryNeedsParens;
parensTests[NodeType.AssignmentPattern] = binaryNeedsParens;
parensTests[NodeType.AssignmentExpression] = assignNeedsParens;
parensTests[NodeType.ConditionalExpression] = assignNeedsParens;
parensTests[NodeType.YieldExpression] = (node) => {
if (isPrivateInRight(node)) return true;
const parent = parentAt(0);
// `a = yield 3` needs none.
return (
(isBinary(parent) && operatorOf(parent) !== "=") ||
isCalledOrAccessed(node, parent) ||
(typeOf(parent) === NodeType.ConditionalExpression &&
childOf(Field.test, parent) === node) ||
isUnaryType(typeOf(parent))
);
};
parensTests[NodeType.ChainExpression] = (node) =>
isCalledOrAccessed(node, parentAt(0));
parensTests[NodeType.MemberExpression] = (node) => {
const parent = parentAt(0);
// A call inside `new (a.b().c)` would otherwise take the `new`'s arguments.
return (
typeOf(parent) === NodeType.NewExpression &&
childOf(Field.callee, parent) === node &&
findNode(node, findCall)
);
};
parensTests[NodeType.TaggedTemplateExpression] = (node) => {
const parent = parentAt(0);
return (
typeOf(parent) === NodeType.NewExpression &&
childOf(Field.callee, parent) === node
);
};
parensTests[NodeType.CallExpression] = (node) => {
const parent = parentAt(0);
const parentType = typeOf(parent);
const calleeIsFunction =
typeOf(childOf(Field.callee, node)) === NodeType.FunctionExpression;
if (
(parentType === NodeType.NewExpression &&
childOf(Field.callee, parent) === node) ||
(parentType === NodeType.ExportDefaultDeclaration && calleeIsFunction)
) {
return true;
}
// Safari bug: https://bugs.webkit.org/show_bug.cgi?id=123506
if (
!calleeIsFunction ||
parentType !== NodeType.MemberExpression ||
childOf(Field.object, parent) !== node
) {
return false;
}
const grandparent = parentAt(1);
return (
typeOf(grandparent) === NodeType.AssignmentExpression &&
childOf(Field.left, grandparent) === parent
);
};
// As `printEstree` keeps them: `new import(a)` does not parse.
parensTests[NodeType.ImportExpression] = (node) => {
const parent = parentAt(0);
return (
typeOf(parent) === NodeType.NewExpression &&
childOf(Field.callee, parent) === node
);
};
parensTests[NodeType.NewExpression] = (node) => {
const parent = parentAt(0);
const type = typeOf(parent);
return (
tree.fieldCount(Field.arguments, node) === 0 &&
(type === NodeType.MemberExpression ||
(isCallType(type) && childOf(Field.callee, parent) === node) ||
(type === NodeType.TaggedTemplateExpression &&
childOf(Field.tag, parent) === node))
);
};
parensTests[NodeType.Literal] = (node) => {
const parent = parentAt(0);
if (
typeOf(parent) !== NodeType.MemberExpression ||
childOf(Field.object, parent) !== node ||
!isNumber(node)
) {
return false;
}
const value = /** @type {number} */ (tree.value(node));
return value < 0 || makeNum(value).startsWith("0");
};
// A statement or loop head may not start with `let [`, a for-of head with
// `let` or a bare `async`: each reads as a declaration or an arrow.
parensTests[NodeType.Identifier] = (node) => {
const name = nameOf(node);
if (name !== "let" && name !== "async") return false;
const parent = parentAt(0);
let child = node;
let head;
for (let i = 0; (head = parentAt(i)); i++) {
// Any member access starts with its object here, a private one too.
const starts =
typeOf(head) === NodeType.MemberExpression
? childOf(Field.object, head) === child
: startsParent(head, child);
if (!starts) break;
child = head;
}
if (!head) return false;
const headType = typeOf(head);
if (
headType === NodeType.ForOfStatement &&
childOf(Field.left, head) === child
) {
return name === "let" || (child === node && !tree.flag(Flag.await, head));
}
return (
name === "let" &&
((headType === NodeType.ExpressionStatement &&
childOf(Field.expression, head) === child) ||
(headType === NodeType.ForStatement &&
childOf(Field.init, head) === child) ||
(headType === NodeType.ForInStatement &&
childOf(Field.left, head) === child)) &&
typeOf(parent) === NodeType.MemberExpression &&
tree.flag(Flag.computed, parent) &&
!tree.flag(Flag.optional, parent)
);
};
/**
* Prints a program `parse.parseTree` read, as `printProgram` prints
* terser's tree of the same source.
* @param {ParsedTree} parsed the parsed source
* @param {OutputStream} stream the stream printed into
* @param {string} text the source
* @param {TreeNames | null=} names the names mangling gave, null where it is off
* @returns {void}
*/
modules.printTree = (parsed, stream, text, names = null) => {
stream.tokenTables = parsed.tables;
printParsed(parsed, stream, text, names);
};
/**
* Prints a parsed program into a stream whose token tables are set.
* @param {ParsedTree} parsed the parsed source
* @param {OutputStream} stream the stream printed into
* @param {string} text the source
* @param {TreeNames | null} names the names mangling gave, null where it is off
* @returns {void}
*/
const printParsed = (parsed, stream, text, names) => {
tree = parsed.tree;
tokens = parsed.read;
output = /** @type {OutputStream<number>} */ (
/** @type {OutputStream<number> | OutputStream} */ (stream)
);
source = text;
stack = [];
literalEnd = -1;
if (names !== null) {
definitions = names.definitions;
privateNames = names.privateNames;
}
stream.nodeAccess = /** @type {NodeAccess} */ (
/** @type {NodeAccess | NodeAccess<number>} */ (treeNodeAccess)
);
try {
printNode(parsed.program);
} finally {
tree = /** @type {SyntaxTree} */ (
/** @type {SyntaxTree | null} */ (null)
);
tokens = /** @type {TreeTokens} */ (
/** @type {TreeTokens | null} */ (null)
);
output = /** @type {OutputStream<number>} */ (
/** @type {OutputStream<number> | null} */ (null)
);
source = "";
definitions = null;
privateNames = null;
}
};
/**
* The private names a class declares, read off the tree being mangled.
* @param {number} node a class
* @returns {string[]} the names
*/
const privateNamesDeclaredIn = (node) => {
/** @type {string[]} */
const names = [];
const body = tree.field(0, Field.body, node);
const count = tree.fieldCount(Field.body, body);
for (let i = 0; i < count; i++) {
const member = tree.field(i, Field.body, body);
const type = tree.type(member);
const key = tree.field(0, Field.key, member);
if (
(type === NodeType.PropertyDefinition ||
type === NodeType.MethodDefinition) &&
tree.type(key) === NodeType.PrivateIdentifier
) {
names.push(tree.name(key));
}
}
return names;
};
/**
* terser's `manglePrivateProperties` over the tree: a name for each private
* name, handed out in the order `renameMangledEstree` meets them.
* @param {number} node a node
* @param {PrivateNames} state the names handed out
* @returns {void}
*/
const renamePrivateNames = (node, state) => {
const type = tree.type(node);
/** @type {number} */
let renamed = 0;
if (
type === NodeType.MethodDefinition ||
type === NodeType.PropertyDefinition
) {
renamed = tree.field(0, Field.key, node);
} else if (type === NodeType.MemberExpression) {
renamed = tree.field(0, Field.property, node);
} else if (
type === NodeType.BinaryExpression &&
tree.textOf(Part.operator, node) === "in"
) {
renamed = tree.field(0, Field.left, node);
}
if (renamed !== 0 && tree.type(renamed) === NodeType.PrivateIdentifier) {
const name = tree.name(renamed);
const { kept, names } = state;
if ((kept === null || !kept.has(name)) && !names.has(name)) {
let mangled = state.identifiers.get(++state.counter);
if (kept !== null) {
while (kept.has(mangled)) {
mangled = state.identifiers.get(++state.counter);
}
}
names.set(name, mangled);
}
}
const fields =
type === NodeType.SwitchCase ? SWITCH_CASE_WALK : CHILD_FIELDS[type];
for (let f = 0; f < fields.length; f++) {
const count = tree.fieldCount(fields[f], node);
for (let i = 0; i < count; i++) {
const child = tree.field(i, fields[f], node);
if (child !== 0) renamePrivateNames(child, state);
}
}
};
// A case's test, walked before its statements as terser's walk does.
const SWITCH_CASE_WALK = [Field.test, Field.consequent];
/**
* Mangles a source `parse.parseTree` read as `minify` mangles terser's tree
* of it; where the stream prints as a string print counts, it is printed
* now, recorded for `patchRecording`, else `printTree` prints it later.
* @param {ParsedTree} parsed the parsed source
* @param {OutputStream} stream the stream the result is printed into
* @param {string} text the source
* @param {MangleOptions} options the formatted mangle options
* @param {boolean} mayNamePrivate whether the source may name a private member
* @returns {TreeNames} the names
*/
modules.mangleTree = (parsed, stream, text, options, mayNamePrivate) => {
const { tables, read } = parsed;
const analysis = analyzeMinifierTree(
parsed.tree,
parsed.program,
read.types,
{
...options,
parameterScopes: true,
classDeclarators: true,
spelledNames: spelledNamesCollected(
modules,
/** @type {boolean} */ (stream.options.shorthand)
)
},
(node, message) => {
const start = /** @type {NodeToken} */ (tables.of(read.starts[node]));
return modules.parse.js_error(
message,
start.file,
start.line,
start.col,
start.pos
);
}
);
/** @type {TreeNames} */
const names = {
definitions: analysis.definitions,
privateNames: null,
recorded: null,
text: ""
};
/** @type {IdentifierSource} */
const identifiers = options.nth_identifier;
if (modules.countsFrequency(identifiers)) {
if (!analysis.namesPrivate && printsAsString(stream.options)) {
const recorded = createMangleRecording();
recording = recorded;
recordingOptions = options;
try {
modules.printTree(parsed, stream, text, names);
} finally {
recording = null;
recordingOptions = null;
}
names.recorded = recorded;
names.text = stream.get();
}
if (names.recorded !== null && !stream.wroteComments) {
countRecording(identifiers, names.text, names.recorded);
} else {
// A comment written sends the count to a print of its own.
identifiers.reset();
const counting = modules.createStringStream();
counting.consumePart = (/** @type {string} */ part) =>
identifiers.consider(part, 1);
countOptions = options;
try {
printParsed(parsed, counting, text, names);
} finally {
countOptions = null;
}
identifiers.consider(counting.get(), 1);
}
identifiers.sort();
}
modules.mangleManglingScopes(analysis, options);
if (!mayNamePrivate) return names;
tree = parsed.tree;
try {
const kept = modules.privateNamesReadByEval(
analysis,
/** @type {(node: PrintNode) => string[]} */ (
/** @type {((node: PrintNode) => string[]) | ((node: number) => string[])} */ (
privateNamesDeclaredIn
)
)
);
// A private method's `name` is its private name, which `keep_fnames`
// keeps as it keeps the other functions' names.
if (analysis.namesPrivate && options.keep_fnames !== true) {
/** @type {PrivateNames} */
const state = {
identifiers: options.nth_identifier,
counter: -1,
names: new Map(),
kept
};
renamePrivateNames(parsed.program, state);
names.privateNames = state.names;
}
} finally {
tree = /** @type {SyntaxTree} */ (
/** @type {SyntaxTree | null} */ (null)
);
}
return names;
};
};
/**
* Which names spelled beside a name the mangler's analysis collects, under the
* improvements: an import's or export's, and a key's where shorthand prints.
* @param {TerserModules} modules terser's modules
* @param {boolean} shorthand whether a property whose value is named as its key prints alone
* @returns {number} the analysis's `spelledNames`: 0 for none, 1 for a module's, 2 for a key's too
*/
const spelledNamesCollected = (modules, shorthand) => {
const { improvements } = modules;
if (improvements === undefined || !improvements.enabled) return 0;
return shorthand ? 2 : 1;
};
/**
* terser's `SymbolDef#unmangleable`: whether mangling keeps a name as written.
* @param {MinifierVariable} definition the definition
* @param {MangleOptions} options the formatted mangle options
* @param {Set<MinifierVariable> | null} keptDefinitions the declarators `keep_fnames` keeps, while mangling
* @param {Set<MinifierVariable> | null=} keptClasses the declarators naming an anonymous class `keep_classnames` keeps
* @returns {boolean} whether its name is kept
*/
const isUnmangleable = (definition, options, keptDefinitions, keptClasses) => {
const { name } = definition;
const scope = /** @type {MinifierScope} */ (definition.scope);
const first = definition.kind;
if (
keptDefinitions !== null &&
keptDefinitions.has(definition) &&
keepName(options.keep_fnames, name)
) {
return true;
}
if (keptClasses && keptClasses.has(definition)) return true;
return (
(definition.global && !options.toplevel) ||
(definition.export & EXPORT_KEEP_NAME) !== 0 ||
definition.undeclared ||
(!options.eval && (scope.usesEval || scope.usesWith)) ||
((first === DECLARES_LAMBDA || first === DECLARES_DEFUN) &&
keepName(options.keep_fnames, name)) ||
((first === DECLARES_CLASS || first === DECLARES_DEF_CLASS) &&
keepName(options.keep_classnames, name))
);
};
// A call's children as terser's `walk` reaches them.
const CALL_WALK_KEYS = ["callee", "arguments"];
// Each node's children, as the ESTree mangler's renaming walks them: in
// terser's walk order, but for a call's, which each walk reaches its own way.
/** @type {Record<string, string[]>} */
const ESTREE_RENAMING_CHILD_KEYS = {
ArrayExpression: ["elements"],
ArrayPattern: ["elements"],
ArrowFunctionExpression: ["params", "body"],
AssignmentExpression: ["left", "right"],
AssignmentPattern: ["left", "right"],
AwaitExpression: ["argument"],
BinaryExpression: ["left", "right"],
BlockStatement: ["body"],
BreakStatement: ["label"],
CatchClause: ["param", "body"],
ChainExpression: ["expression"],
ClassBody: ["body"],
ClassDeclaration: ["id", "superClass", "body"],
ClassExpression: ["id", "superClass", "body"],
ConditionalExpression: ["test", "consequent", "alternate"],
ContinueStatement: ["label"],
DebuggerStatement: [],
DoWhileStatement: ["body", "test"],
EmptyStatement: [],
ExportAllDeclaration: ["source"],
ExportDefaultDeclaration: ["exported_value", "exported_definition"],
ExportNamedDeclaration: [
"exported_value",
"exported_definition",
"exported_names",
"source"
],
ExportSpecifier: ["local"],
ExpressionStatement: ["expression"],
ForInStatement: ["left", "right", "body"],
ForOfStatement: ["left", "right", "body"],
ForStatement: ["init", "test", "update", "body"],
FunctionDeclaration: ["id", "params", "body"],
FunctionExpression: ["id", "params", "body"],
IfStatement: ["test", "consequent", "alternate"],
ImportDeclaration: ["imported_name", "imported_names", "source"],
ImportExpression: ["args"],
ImportNamespaceSpecifier: ["local"],
ImportSpecifier: ["local"],
LabeledStatement: ["label", "body"],
LogicalExpression: ["left", "right"],
MemberExpression: ["object", "property"],
MetaProperty: [],
MethodDefinition: ["key", "value"],
ObjectExpression: ["properties"],
ObjectPattern: ["properties"],
Program: ["body"],
Property: ["key", "value"],
PropertyDefinition: ["key", "value"],
RestElement: ["argument"],
ReturnStatement: ["argument"],
SequenceExpression: ["expressions"],
SpreadElement: ["argument"],
StaticBlock: ["body"],
Super: [],
SwitchCase: ["test", "consequent"],
SwitchStatement: ["discriminant", "cases"],
TaggedTemplateExpression: ["tag", "quasi"],
TemplateElement: [],
TemplateLiteral: ["segments"],
ThisExpression: [],
ThrowStatement: ["argument"],
TryStatement: ["block", "handler", "finalizer"],
UnaryExpression: ["argument"],
UpdateExpression: ["argument"],
VariableDeclaration: ["declarations"],
VariableDeclarator: ["id", "init"],
WhileStatement: ["test", "body"],
WithStatement: ["object", "body"],
YieldExpression: ["argument"]
};
/** @typedef {Map<MinifierVariable, Map<string, { saving: number, sites: number }>>} SpelledByDefinition each definition a key or an import or export spells a name beside, what each such name saves it and at how many of its uses */
/**
* terser's `manglePrivateProperties`: one name for each private name, in
* the order its transform first meets them.
* @typedef {object} PrivateNames
* @property {{ get: (index: number) => string }} identifiers the identifier source
* @property {number} counter the names handed out, less one
* @property {Map<string, string>} names each private name's mangled one
* @property {Set<string> | null} kept the names a direct `eval` reads, which stay
*/
/**
* Installs the mangler, which names the scopes `analyzeMinifierScope` reads
* off the compressed tree with its names as written.
* @param {TerserModules} modules terser's modules
* @returns {void}
*/
const installEstreeMangle = (modules) => {
const jsError = modules.parse.js_error;
/**
* Throws the parse error a misplaced or repeated declaration is.
* @param {PrintNode} node the node a name of which is wrong
* @param {string} message the message
* @param {TokenTables=} tables what the tree's token numbers are read through
* @returns {never} never
*/
const raise = (node, message, tables) => {
const start = /** @type {NodeToken} */ (
tables ? tables.of(node.startToken) : node.startToken
);
return jsError(message, start.file, start.line, start.col, start.pos);
};
/**
* terser's `figure_out_scope(options)` for its mangler.
* @param {PrintProgram} program the tree, its names as written
* @param {MangleOptions} options the mangle options
* @param {boolean=} releases whether nobody keeps the tree, so each node read drops what only the compressor read
* @param {boolean=} shorthand whether a property whose value is named as its key prints alone
* @returns {MinifierScopeAnalysis} the scopes
*/
const analyzeManglingScopes = (program, options, releases, shorthand) => {
const { corrections } = modules;
const spelledNames = spelledNamesCollected(modules, shorthand === true);
return analyzeMinifierScope(
program,
// The correct phase scopes a parameter list apart from its body.
corrections !== undefined && corrections.enabled
? {
...options,
parameterScopes: true,
classDeclarators: true,
spelledNames
}
: spelledNames !== 0
? { ...options, spelledNames }
: options,
raise,
releases === true ? releaseCompressorFields : undefined
);
};
modules.analyzeManglingScopes = analyzeManglingScopes;
modules.isUnmangleable = isUnmangleable;
const { ALL_RESERVED_WORDS } = modules.parse;
const { base54 } = modules.scope;
/**
* The definition a catch parameter redefines in the enclosing function
* scope, which owns the name the two share.
* @param {MinifierVariable} definition the catch parameter's definition
* @returns {MinifierVariable | undefined} the definition it redefines
*/
const redefinedCatchDefinition = (definition) => {
const { scope } = definition;
return definition.kind === DECLARES_CATCH && scope.variableScope !== scope
? /** @type {MinifierVariable | undefined} */ (
scope.variableScope.getBinding(definition.name)
)
: undefined;
};
/**
* The scope whose count of names hands a definition its name: a block's
* function counts in its function, where terser's `next_mangled` does.
* @param {MinifierVariable} definition the definition
* @param {Set<MinifierScope>} blockDefunScopes the function scopes holding a block's function
* @returns {MinifierScope} the scope
*/
const countingScopeOf = (definition, blockDefunScopes) => {
const owner = /** @type {MinifierScope} */ (definition.scope);
return blockDefunScopes.size !== 0 &&
blockDefunScopes.has(/** @type {MinifierScope} */ (owner.variableScope))
? /** @type {MinifierScope} */ (owner.variableScope)
: owner;
};
/**
* Each definition a key or an import or export spells a name beside, with
* the characters each name spelled saves it.
* @param {MinifierScopeAnalysis} analysis the scopes
* @returns {SpelledByDefinition | null} the definitions, none where nothing spells one
*/
const spelledNamesOf = (analysis) => {
const names = analysis.spelledNames;
if (names === null || names.identifiers.length === 0) return null;
const { identifiers, spellings, savings } = names;
/** @type {SpelledByDefinition} */
const byDefinition = new Map();
for (let i = 0; i < identifiers.length; i++) {
const definition = identifiers[i].definition;
if (!isMinifierVariable(definition)) continue;
let saved = byDefinition.get(definition);
if (saved === undefined) {
saved = new Map();
byDefinition.set(definition, saved);
}
const known = saved.get(spellings[i]);
if (known === undefined) {
saved.set(spellings[i], { saving: savings[i], sites: 1 });
} else {
known.saving += savings[i];
known.sites++;
}
}
return byDefinition.size === 0 ? null : byDefinition;
};
/**
* The name spelled beside a definition that saves it the most, where a
* longer name costs its every use more than the spelling saves.
* @param {MinifierVariable} definition the definition
* @param {Map<string, { saving: number, sites: number }> | undefined} saved what each name spelled beside it saves, and at how many uses
* @returns {string | undefined} the name, none where none saves anything
*/
const spelledNameFor = (definition, saved) => {
if (saved === undefined) return;
const uses = definition.referenceCount + 1;
const { improvements } = modules;
const atSites = improvements !== undefined && improvements.enabled;
/** @type {string | undefined} */
let best;
let bestSaving = 0;
for (const [name, { saving, sites }] of saved) {
// Its every use costs the bytes it writes past a name one long.
let net = saving - (Buffer.byteLength(name) - 1) * uses;
// WHY: a name used only where it is spelled repeats what is written
// beside it, which gzips smaller whatever the program's size: measured
// over test262 and swc's corpus, as a rule applied everywhere.
if (atSites && sites >= uses && net <= 0 && saving > 0) net = saving;
if (net > bestSaving) {
best = name;
bestSaving = net;
}
}
return best;
};
/**
* terser's `mangle_names` over an analysis: names each label, then each
* definition it may rename, setting their `mangled_name`.
* @param {MinifierScopeAnalysis} analysis the scopes, as `analyzeMinifierScope` read them
* @param {MangleOptions} options the formatted mangle options
* @returns {void}
*/
const mangleManglingScopes = (analysis, options) => {
const identifiers = options.nth_identifier;
const { reserved, cache } = options;
const { labels, blockDefunScopes, catchParameters } = analysis;
/** @type {Set<MinifierVariable> | null} */
let keptDefinitions = null;
const { functionDeclarators, classDeclarators } = analysis;
if (functionDeclarators !== null) {
keptDefinitions = new Set();
for (let i = 0; i < functionDeclarators.length; i++) {
const identifier = functionDeclarators[i];
if (keepName(options.keep_fnames, identifier.name)) {
keptDefinitions.add(
/** @type {MinifierVariable} */ (identifier.definition)
);
}
}
}
/** @type {Set<MinifierVariable> | null} */
let keptClasses = null;
if (classDeclarators !== null) {
keptClasses = new Set();
for (let i = 0; i < classDeclarators.length; i++) {
const identifier = classDeclarators[i];
if (keepName(options.keep_classnames, identifier.name)) {
keptClasses.add(
/** @type {MinifierVariable} */ (identifier.definition)
);
}
}
}
for (let i = 0; i < labels.length; i++) {
const label = labels[i];
let counter = label.parent === null ? -1 : label.parent.counter;
/** @type {string} */
let name;
do {
name = identifiers.get(++counter);
} while (ALL_RESERVED_WORDS.has(name));
label.counter = counter;
label.mangled_name = name;
}
/** @type {MinifierVariable[]} */
const toMangle = [];
// An export's own name, which no scope hands out even where it is unseen.
/** @type {Set<string>} */
const unmangleableNames = new Set();
/**
* @param {MinifierVariable} definition a definition in some scope
* @returns {void}
*/
const collect = (definition) => {
if (definition.export & EXPORT_KEEP_NAME) {
unmangleableNames.add(definition.name);
} else if (!reserved.has(definition.name)) {
toMangle.push(definition);
}
};
if (cache) {
for (const definition of analysis.globals.values()) collect(definition);
}
const legacyCatch = options.ie8 || options.safari10;
// WHY: a switch's block scope opens before the function scopes of its
// discriminant, so the order catch parameters were met in is not this
// walk's, which would name one before the function around it.
/** @type {Map<MinifierScope, MinifierVariable[]>} */
const catchParametersByScope = new Map();
if (!legacyCatch) {
for (let i = 0; i < catchParameters.length; i++) {
const definition = /** @type {MinifierVariable} */ (
catchParameters[i].definition
);
const scope = /** @type {MinifierScope} */ (definition.scope);
const list = catchParametersByScope.get(scope);
if (list === undefined) catchParametersByScope.set(scope, [definition]);
else list.push(definition);
}
}
// The scopes in the order opened: a scope's children, the first on top.
/** @type {MinifierScope[]} */
const pending = [analysis.toplevel];
while (pending.length !== 0) {
const scope = /** @type {MinifierScope} */ (pending.pop());
const { variables, childScopes } = scope;
for (let j = childScopes.length; --j >= 0;) {
pending.push(/** @type {MinifierScope} */ (childScopes[j]));
}
for (let j = 0; j < variables.length; j++) {
collect(/** @type {MinifierVariable} */ (variables[j]));
}
// A catch parameter is named where the walk meets it, reserved or not.
const parameters = catchParametersByScope.get(scope);
if (parameters !== undefined) {
for (let j = 0; j < parameters.length; j++) {
toMangle.push(parameters[j]);
}
}
}
if (
cache ||
legacyCatch ||
options.keep_fnames ||
options.keep_classnames ||
identifiers !== base54
) {
mangleEveryOption(
toMangle,
options,
keptDefinitions,
unmangleableNames,
blockDefunScopes,
keptClasses
);
return;
}
/** @type {Map<MinifierScope, Set<string>>} */
const takenByScope = new Map();
// WHY: the names a scope may not reuse are the ones its enclosed
// definitions carry, and terser rereads that list for every candidate it
// tries — quadratic in a bundle's one big scope. Reading it once is safe
// because an enclosed definition belongs to an outer scope, and outer
// scopes are mangled before inner ones, so the answer no longer moves
// by the time a scope first asks.
/**
* @param {MinifierScope} owner the scope handing out a name
* @returns {Set<string>} the names it may not hand out
*/
const takenIn = (owner) => {
const known = takenByScope.get(owner);
if (known !== undefined) return known;
/** @type {Set<string>} */
const taken = new Set();
const { enclosed } = owner;
for (let i = 0; i < enclosed.length; i++) {
const definition = enclosed[i];
const name =
definition.mangled_name ||
(isUnmangleable(definition, options, null) && definition.name);
if (name) taken.add(name);
}
takenByScope.set(owner, taken);
return taken;
};
/** @type {MinifierVariable[]} */
const renamed = [];
// Where each name handed out stands in the order `identifiers` gives them.
/** @type {Map<string, number>} */
const nameIndexes = new Map();
const { improvements } = modules;
const improving = improvements !== undefined && improvements.enabled;
const spellings = improving ? spelledNamesOf(analysis) : null;
// Each definition a name spelled beside it saves more than it costs.
/** @type {Map<MinifierVariable, string>} */
const wanted = new Map();
if (spellings !== null) {
for (const [definition, saved] of spellings) {
const spelling = spelledNameFor(definition, saved);
if (spelling !== undefined) wanted.set(definition, spelling);
}
}
// Each scope that counts out names a definition may take spelled, and
// the names given in it, by counting or spelled, which neither repeats.
/** @type {Map<MinifierScope, Set<string>> | null} */
let givenIn = null;
/** @type {Set<string>} */
const spelled = new Set();
// The names a scope's definitions want spelled, which counting skips there.
/** @type {Map<MinifierScope, Set<string>> | null} */
let wantedIn = null;
if (wanted.size !== 0) {
givenIn = new Map();
wantedIn = new Map();
for (const [definition, spelling] of wanted) {
const counting = countingScopeOf(definition, blockDefunScopes);
givenIn.set(counting, new Set());
const names = wantedIn.get(counting);
if (names === undefined) wantedIn.set(counting, new Set([spelling]));
else names.add(spelling);
}
}
for (let i = 0; i < toMangle.length; i++) {
const definition = toMangle[i];
if (
definition.mangled_name ||
isUnmangleable(definition, options, null)
) {
continue;
}
const redefinition = redefinedCatchDefinition(definition);
if (redefinition) {
definition.mangled_name =
redefinition.mangled_name || redefinition.name;
renamed.push(definition);
continue;
}
const owner = /** @type {MinifierScope} */ (definition.scope);
// Safari reads `(function x(x) {})` in strict mode as a syntax error.
/** @type {string | null} */
let shadowed = null;
const named = owner.functionName;
if (named !== null && definition.kind === DECLARES_FUNARG) {
shadowed = named.mangled_name || named.name;
}
const counting = countingScopeOf(definition, blockDefunScopes);
const taken = takenIn(counting);
const given = givenIn === null ? undefined : givenIn.get(counting);
const wantedHere = wantedIn === null ? undefined : wantedIn.get(counting);
if (given !== undefined) {
const spelling = wanted.get(definition);
if (
spelling !== undefined &&
!ALL_RESERVED_WORDS.has(spelling) &&
!reserved.has(spelling) &&
!unmangleableNames.has(spelling) &&
!taken.has(spelling) &&
!given.has(spelling) &&
shadowed !== spelling
) {
definition.mangled_name = spelling;
given.add(spelling);
spelled.add(spelling);
continue;
}
}
/** @type {string} */
let name;
for (;;) {
name = identifiers.get(++counting.cname);
if (ALL_RESERVED_WORDS.has(name)) continue;
if (reserved.has(name)) continue;
if (unmangleableNames.has(name)) continue;
if (taken.has(name)) continue;
if (shadowed !== null && shadowed === name) continue;
if (given !== undefined && given.has(name)) continue;
if (wantedHere !== undefined && wantedHere.has(name)) continue;
break;
}
if (given !== undefined) given.add(name);
definition.mangled_name = name;
renamed.push(definition);
nameIndexes.set(name, counting.cname);
}
if (improving) {
shortenBusiestNames(toMangle, renamed, nameIndexes, analysis, [
unmangleableNames,
reserved,
spelled
]);
}
};
/**
* Hands the names of the shortest length to the names referenced most,
* keeping each length's names in the order they were handed out.
* Done only where it saves enough bytes to outweigh what reordering costs gzip.
* @param {MinifierVariable[]} toMangle the definitions the mangler considered
* @param {MinifierVariable[]} named the definitions it named, each once
* @param {Map<string, number>} nameIndexes each name handed out, by its place in the order names are handed out
* @param {MinifierScopeAnalysis} analysis the scopes
* @param {Set<string>[]} keptNames names written as they are, which no name may become
* @returns {void}
*/
const shortenBusiestNames = (
toMangle,
named,
nameIndexes,
analysis,
keptNames
) => {
let longer = false;
for (const name of nameIndexes.keys()) {
if (name.length > 1) {
longer = true;
break;
}
}
// Every name one character long: there is no shorter name to hand out.
if (!longer) return;
/** @type {Map<string, number>} */
const references = new Map();
for (let i = 0; i < named.length; i++) {
const definition = named[i];
const name = /** @type {string} */ (definition.mangled_name);
references.set(
name,
(references.get(name) || 0) + definition.referenceCount
);
}
/** @type {Set<string>} */
const kept = new Set();
for (let i = 0; i < toMangle.length; i++) {
if (toMangle[i].mangled_name === null) kept.add(toMangle[i].name);
}
for (const name of analysis.globals.keys()) kept.add(name);
/** @type {string[]} */
const names = [];
for (const name of nameIndexes.keys()) {
if (!kept.has(name) && !keptNames.some((set) => set.has(name))) {
names.push(name);
}
}
/**
* @param {string} a a name
* @param {string} b another
* @returns {number} which is handed out first
*/
const byIndex = (a, b) =>
/** @type {number} */ (nameIndexes.get(a)) -
/** @type {number} */ (nameIndexes.get(b));
names.sort(byIndex);
const busiest = [...names].sort(
(a, b) =>
/** @type {number} */ (references.get(b)) -
/** @type {number} */ (references.get(a)) || byIndex(a, b)
);
/** @type {Map<string, string>} */
const renames = new Map();
let saved = 0;
for (let start = 0; start < names.length;) {
const { length } = names[start];
let end = start;
while (end < names.length && names[end].length === length) end++;
const chosen = busiest.slice(start, end).sort(byIndex);
for (let i = 0; i < chosen.length; i++) {
const name = chosen[i];
renames.set(name, names[start + i]);
saved +=
(name.length - length) * /** @type {number} */ (references.get(name));
}
start = end;
}
if (saved < SHORTER_NAMES_MIN_SAVING) return;
for (let i = 0; i < named.length; i++) {
const definition = named[i];
const shorter = renames.get(
/** @type {string} */ (definition.mangled_name)
);
if (shorter !== undefined) definition.mangled_name = shorter;
}
};
/**
* terser's `mangle_names` as written, for the options the fast path leaves
* out: each definition asks its scope for a name, whose enclosed definitions
* it rescans.
* @param {MinifierVariable[]} toMangle the definitions, in the order named
* @param {MangleOptions} options the formatted mangle options
* @param {Set<MinifierVariable> | null} keptDefinitions the declarators `keep_fnames` keeps
* @param {Set<string>} unmangleableNames names no definition may take
* @param {Set<MinifierScope>} blockDefunScopes the function scopes holding a block's function
* @param {Set<MinifierVariable> | null} keptClasses the declarators naming an anonymous class `keep_classnames` keeps
* @returns {void}
*/
const mangleEveryOption = (
toMangle,
options,
keptDefinitions,
unmangleableNames,
blockDefunScopes,
keptClasses
) => {
const identifiers = options.nth_identifier;
const { reserved } = options;
const cache = options.cache && options.cache.props;
// The toplevel skips the names the cache hands out too.
/** @type {Set<string>} */
const mangledNames = new Set();
if (cache) {
for (const mangled of cache.values()) mangledNames.add(mangled);
}
// A short name kept as written may collide with one handed out.
if (options.keep_fnames || options.keep_classnames) {
for (let i = 0; i < toMangle.length; i++) {
const definition = toMangle[i];
if (
definition.name.length < 6 &&
isUnmangleable(definition, options, keptDefinitions, keptClasses)
) {
unmangleableNames.add(definition.name);
}
}
}
/**
* terser's `next_mangled`: the next name a scope hands out that shadows
* nothing it or a scope inside it reads.
* @param {MinifierScope} from the scope handing out a name
* @returns {string} the name
*/
const nextMangled = (from) => {
let scope = from;
if (
blockDefunScopes.size !== 0 &&
blockDefunScopes.has(/** @type {MinifierScope} */ (scope.variableScope))
) {
scope = /** @type {MinifierScope} */ (scope.variableScope);
}
const { enclosed } = scope;
outer: for (;;) {
const name = identifiers.get(++scope.cname);
if (ALL_RESERVED_WORDS.has(name)) continue;
if (reserved.has(name)) continue;
if (unmangleableNames.has(name)) continue;
for (let i = enclosed.length; --i >= 0;) {
const definition = enclosed[i];
if (
name ===
(definition.mangled_name ||
(isUnmangleable(
definition,
options,
keptDefinitions,
keptClasses
) &&
definition.name))
) {
continue outer;
}
}
return name;
}
};
for (let i = 0; i < toMangle.length; i++) {
const definition = toMangle[i];
// A module's globals cannot refer to another module's local bindings.
if (
definition.global &&
!(definition.undeclared && options.module) &&
cache &&
cache.has(definition.name)
) {
definition.mangled_name = /** @type {string} */ (
cache.get(definition.name)
);
continue;
}
if (
definition.mangled_name ||
isUnmangleable(definition, options, keptDefinitions, keptClasses)
) {
continue;
}
let owner = /** @type {MinifierScope} */ (definition.scope);
if (options.ie8 && definition.kind === DECLARES_LAMBDA) {
owner = /** @type {MinifierScope} */ (owner.upper);
}
const redefinition = redefinedCatchDefinition(definition);
/** @type {string} */
let name;
if (redefinition) {
name = redefinition.mangled_name || redefinition.name;
} else if (owner.upper === null) {
do {
name = nextMangled(owner);
} while (mangledNames.has(name));
} else {
const named = owner.functionName;
const shadowed =
named !== null && definition.kind === DECLARES_FUNARG
? named.mangled_name || named.name
: null;
for (;;) {
name = nextMangled(owner);
if (!shadowed || shadowed !== name) break;
}
}
definition.mangled_name = name;
if (definition.global && cache) cache.set(definition.name, name);
}
};
modules.mangleManglingScopes = mangleManglingScopes;
/**
* @param {PrintNode} key a key not computed
* @returns {string} the name it is
*/
const keyNameOf = (key) => /** @type {PrintIdentifier} */ (key).name;
/**
* What terser's walk reads as a computed key's `name`: a name read, a
* function's or class's own name, or nothing.
* @param {PrintNode} key a computed key
* @returns {string} the name, as terser hands it to property mangling
*/
const computedKeyNameOf = (key) => {
/** @type {string | PrintIdentifier | null | undefined} */
let name;
if (key.type === "Identifier") {
if (key.atom !== true) name = key.name;
} else if (
key.type === "FunctionExpression" ||
key.type === "ClassExpression"
) {
name = key.id || null;
}
// Not always a string, which terser's sets and regexp read as it is.
return /** @type {string} */ (name);
};
/**
* terser's `definesProperty`, which prints the callee: only a member
* `defineProperty`, not optional, off a name printed `Object`, prints so.
* @param {PrintCallExpression | PrintNewExpression} node a call
* @returns {boolean} whether it calls `Object.defineProperty`
*/
const definesProperty = (node) => {
const { callee } = node;
if (
callee.type !== "MemberExpression" ||
callee.computed ||
callee.optional
) {
return false;
}
const { object, property } = callee;
return (
property.type === "Identifier" &&
property.name === "defineProperty" &&
object.type === "Identifier" &&
printedNameOf(object) === "Object"
);
};
/**
* Whether the expression a member is read off, followed as terser does
* through what it holds as an `expression`, ends in an undeclared global.
* @param {PrintMemberExpression} node the member
* @returns {boolean} whether its root is undeclared
*/
const readsUndeclared = (node) => {
let root = node.object;
for (;;) {
switch (root.type) {
case "MemberExpression":
root = root.object;
continue;
case "CallExpression":
case "NewExpression":
root = root.callee;
continue;
case "ChainExpression":
root = root.expression;
continue;
case "AwaitExpression":
case "UnaryExpression":
case "UpdateExpression":
root = root.argument;
continue;
case "YieldExpression":
if (!root.argument) return false;
root = root.argument;
continue;
case "Identifier": {
const definition = /** @type {MinifierVariable | null} */ (
root.definition
);
return definition !== null && definition.undeclared;
}
default:
return false;
}
}
};
const isStringNode = /** @type {(node: PrintNode) => boolean} */ (
modules.ast.isStringNode
);
/**
* @param {PrintLiteral} node a literal
* @returns {boolean} whether a `@__KEY__` annotation marks it a property name
*/
const isAnnotatedKey = (node) =>
node._annotations !== undefined && (node._annotations & KEY) !== 0;
/**
* @param {PrintLiteral} node a string, renamed as a property name
* @returns {void}
*/
const clearKeyAnnotation = (node) => {
const annotations = node._annotations;
if (annotations !== undefined && (annotations & KEY) !== 0) {
node._annotations = annotations & ~KEY;
}
};
/**
* terser's `addStrings`: each string a key may be read from.
* @param {PrintNode} node where the key is computed
* @param {PropertyNames} names the names found
* @returns {void}
*/
const addEstreeStrings = (node, names) => {
if (node.type === "SequenceExpression") {
addEstreeStrings(node.expressions[node.expressions.length - 1], names);
} else if (node.type === "Literal") {
if (isStringNode(node)) names.add(/** @type {string} */ (node.value));
} else if (node.type === "ConditionalExpression") {
addEstreeStrings(node.consequent, names);
addEstreeStrings(node.alternate, names);
}
};
/**
* terser's `mangleStrings`: renames each string a key may be read from.
* @param {PrintNode} node where the key is computed
* @param {PropertyNames} names the names
* @returns {void}
*/
const mangleEstreeStrings = (node, names) => {
if (node.type === "SequenceExpression") {
mangleEstreeStrings(node.expressions[node.expressions.length - 1], names);
} else if (node.type === "Literal") {
if (isStringNode(node)) {
// Cleared so a key annotation is not mangled a second time.
clearKeyAnnotation(node);
node.value = names.mangle(/** @type {string} */ (node.value));
}
} else if (node.type === "ConditionalExpression") {
mangleEstreeStrings(node.consequent, names);
mangleEstreeStrings(node.alternate, names);
}
};
/**
* terser's first walk of `mangle_properties`: every name a property is
* found by, in its walk's order.
* @param {PrintNode} node a node
* @param {PropertyNames} names the names found
* @returns {void}
*/
const findEstreeProperties = (node, names) => {
const { keepQuoted } = names;
switch (node.type) {
case "Identifier":
case "PrivateIdentifier":
return;
case "Literal":
if (isAnnotatedKey(node)) {
names.add(/** @type {string} */ (node.value));
}
return;
case "Property":
if (keepQuoted && node.quote) break;
if (!node.computed) {
names.add(keyNameOf(node.key));
} else if (node.kind !== "init" || node.method) {
names.add(computedKeyNameOf(node.key));
}
break;
case "MethodDefinition":
case "PropertyDefinition":
if (node.key.type === "PrivateIdentifier") break;
if (keepQuoted && node.quote) break;
names.add(
node.computed ? computedKeyNameOf(node.key) : keyNameOf(node.key)
);
break;
case "MemberExpression":
if (node.computed) {
if (!keepQuoted) addEstreeStrings(node.property, names);
} else if (
node.property.type === "Identifier" &&
(names.undeclared || !readsUndeclared(node)) &&
(!keepQuoted || !node.quote)
) {
names.add(node.property.name);
}
break;
case "CallExpression":
case "NewExpression": {
if (definesProperty(node)) {
const descriptor = node.arguments[1];
if (descriptor === undefined) {
// terser walks the argument it lacks, and throws as it does.
/** @type {{ walk: () => void }} */ (descriptor).walk();
}
addEstreeStrings(descriptor, names);
}
findEstreeList(node.arguments, names);
findEstreeProperties(node.callee, names);
return;
}
case "BinaryExpression":
if (node.operator === "in" && node.left.type !== "PrivateIdentifier") {
addEstreeStrings(node.left, names);
}
break;
default:
}
const keys = ESTREE_RENAMING_CHILD_KEYS[node.type];
const fields = /** @type {PrintFields} */ (
/** @type {PrintFields | PrintNode} */ (node)
);
for (let i = 0; i < keys.length; i++) {
const child = fields[keys[i]];
if (child === null || child === undefined) continue;
if (Array.isArray(child)) findEstreeList(child, names);
else findEstreeProperties(child, names);
}
};
/**
* @param {(PrintNode | null)[]} nodes nodes, holes null
* @param {PropertyNames} names the names found
* @returns {void}
*/
const findEstreeList = (nodes, names) => {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node) findEstreeProperties(node, names);
}
};
/** @type {PrivateNames | null} */
let privateNames = null;
// Where the tree is handed back, the members a mapping name was set on.
/** @type {(PrintProperty | PrintPropertyDefinition)[] | null} */
let mapNamesSet = null;
const mapNameOf =
/** @type {(member: PrintProperty | PrintPropertyDefinition, tables: TokenTables) => string | null} */ (
modules.mapNameOf
);
// What the tree being renamed reads its token numbers through.
/** @type {TokenTables} */
let renameTables = NO_TOKEN_TABLES;
/** @type {PropertyNames | null} */
let propertyNames = null;
/**
* @param {PrintPrivateIdentifier} identifier a private name
* @returns {void}
*/
const renamePrivate = (identifier) => {
const state = /** @type {PrivateNames} */ (privateNames);
const { kept } = state;
if (kept !== null && kept.has(identifier.name)) return;
let mangled = state.names.get(identifier.name);
if (!mangled) {
mangled = state.identifiers.get(++state.counter);
if (kept !== null) {
while (kept.has(mangled)) {
mangled = state.identifiers.get(++state.counter);
}
}
state.names.set(identifier.name, mangled);
}
identifier.name = mangled;
};
/**
* Renames a key not computed, and the name its mapping carries where
* that is read off the key.
* @param {PrintProperty | PrintMethodDefinition | PrintPropertyDefinition} node the member
* @param {PropertyNames} names the names
* @returns {void}
*/
const renameKey = (node, names) => {
const { key } = node;
const name = keyNameOf(key);
const mangled = names.mangle(name);
if (mangled === name) return;
/** @type {PrintIdentifier} */ (key).name = mangled;
if (node.type === "Property") {
if (node.kind === "init" && !node.method) setMapName(node, mangled);
} else if (node.type === "PropertyDefinition") {
const start = renameTables.of(node.startToken);
if (!start || start.type !== "string") setMapName(node, mangled);
}
};
/**
* Sets the name a member's mapping carries, which renaming its key leaves
* as it was read.
* @param {PrintProperty | PrintPropertyDefinition} member the member
* @param {string | null} name the name
* @returns {void}
*/
const setMapName = (member, name) => {
member.mapName = name;
if (mapNamesSet !== null) mapNamesSet.push(member);
};
/**
* Gives each name the name mangling chose, and renames private names and,
* where on, properties: terser's transforms, in their order.
* @param {PrintNode} node a node
* @returns {void}
*/
const renameEstree = (node) => {
const names = propertyNames;
switch (node.type) {
case "Identifier":
case "PrivateIdentifier":
return;
case "Literal":
if (names !== null && isAnnotatedKey(node)) {
clearKeyAnnotation(node);
node.value = names.mangle(/** @type {string} */ (node.value));
}
return;
case "Property":
if (
names !== null &&
!node.computed &&
(!names.keepQuoted || !node.quote)
) {
renameKey(node, names);
}
break;
case "MethodDefinition":
case "PropertyDefinition": {
const { key } = node;
if (key.type === "PrivateIdentifier") {
if (privateNames === null) break;
// A private field's mapping carries the name it was written with.
if (
node.type === "PropertyDefinition" &&
node.mapName === undefined
) {
setMapName(node, mapNameOf(node, renameTables));
}
renamePrivate(key);
} else if (
names !== null &&
!node.computed &&
(!names.keepQuoted || !node.quote)
) {
renameKey(node, names);
}
break;
}
case "MemberExpression": {
const { property } = node;
if (property.type === "PrivateIdentifier") {
if (privateNames !== null) renamePrivate(property);
} else if (names !== null) {
if (node.computed) {
if (!names.keepQuoted) mangleEstreeStrings(property, names);
} else if (!names.keepQuoted || !node.quote) {
const identifier = /** @type {PrintIdentifier} */ (property);
identifier.name = names.mangle(identifier.name);
}
}
break;
}
case "BinaryExpression":
if (node.operator === "in") {
const { left } = node;
if (left.type === "PrivateIdentifier") {
if (privateNames !== null) renamePrivate(left);
} else if (names !== null) {
mangleEstreeStrings(left, names);
}
}
break;
case "CallExpression":
case "NewExpression":
if (names !== null && definesProperty(node)) {
mangleEstreeStrings(node.arguments[1], names);
}
renameEstree(node.callee);
renameEstreeList(node.arguments);
return;
default:
}
const keys = ESTREE_RENAMING_CHILD_KEYS[node.type];
const fields = /** @type {PrintFields} */ (
/** @type {PrintFields | PrintNode} */ (node)
);
for (let i = 0; i < keys.length; i++) {
const child = fields[keys[i]];
if (child === null || child === undefined) continue;
if (Array.isArray(child)) renameEstreeList(child);
else renameEstree(child);
}
};
/**
* @param {(PrintNode | null)[]} nodes nodes, holes null
* @returns {void}
*/
const renameEstreeList = (nodes) => {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node) renameEstree(node);
}
};
/**
* Names a tree `analyzeManglingScopes` read as `mangleManglingScopes`
* chose, then mangles its private names and, given their options, its
* properties, as terser's `minify` does in turn.
* @param {PrintNode} tree the tree
* @param {MangleOptions} options the formatted mangle options
* @param {PropertyNames | null} properties property mangling's names, unless off
* @param {boolean} hasPrivateNames whether the tree may name a private member
* @param {(PrintProperty | PrintPropertyDefinition)[] | null} setMapNames where the tree is handed back, collects each member a mapping name is set on
* @param {Set<string> | null=} kept the private names a direct `eval` reads, which stay
* @returns {void}
*/
const renameMangledEstree = (
tree,
options,
properties,
hasPrivateNames,
setMapNames,
kept = null
) => {
if (properties === null && !hasPrivateNames) return;
if (properties !== null) findEstreeProperties(tree, properties);
privateNames = hasPrivateNames
? {
identifiers: options.nth_identifier,
counter: -1,
names: new Map(),
kept
}
: null;
mapNamesSet = setMapNames;
renameTables =
/** @type {PrintProgram} */ (tree).tokenTables || NO_TOKEN_TABLES;
try {
// Interleaved, the two would call an identifier source of the
// caller's in another order: only terser's own is known to be pure.
if (
privateNames !== null &&
properties !== null &&
(privateNames.identifiers !== base54 ||
properties.identifiers !== base54)
) {
renameEstree(tree);
privateNames = null;
}
propertyNames = properties;
if (privateNames !== null || propertyNames !== null) renameEstree(tree);
} finally {
privateNames = null;
propertyNames = null;
mapNamesSet = null;
renameTables = NO_TOKEN_TABLES;
}
};
modules.renameMangledEstree = renameMangledEstree;
/**
* @param {PrintNode} node a class
* @returns {string[]} the private names its members declare
*/
const privateNamesDeclaredBy = (node) => {
/** @type {string[]} */
const names = [];
for (const member of /** @type {PrintClass} */ (node).body.body) {
if (
(member.type === "PropertyDefinition" ||
member.type === "MethodDefinition") &&
member.key.type === "PrivateIdentifier"
) {
names.push(member.key.name);
}
}
return names;
};
/**
* The private names a class declares where a direct `eval` in it can read
* them: from a string, which renaming cannot reach.
* @param {MinifierScopeAnalysis} analysis the scopes
* @param {(node: PrintNode) => string[]=} declaredBy the private names a class scope's node declares
* @returns {Set<string> | null} the names, or null where no `eval` can read one
*/
const privateNamesReadByEval = (
analysis,
declaredBy = privateNamesDeclaredBy
) => {
/** @type {Set<string> | null} */
let names = null;
// `usesEval` marks every scope around an `eval`, so a scope without it
// holds none.
const pending = [analysis.toplevel];
while (pending.length > 0) {
const scope = /** @type {MinifierScope} */ (pending.pop());
if (!scope.usesEval) continue;
for (const child of scope.childScopes) {
pending.push(/** @type {MinifierScope} */ (child));
}
if (scope.type !== "class") continue;
const declared = declaredBy(scope.node);
for (let i = 0; i < declared.length; i++) {
if (names === null) names = new Set();
names.add(declared[i]);
}
}
return names;
};
modules.privateNamesReadByEval = privateNamesReadByEval;
/**
* The first function in a tree, its children in terser's `walk` order.
* @param {PrintNode} node a node
* @returns {PrintFunction | undefined} the function, if any
*/
const firstFunction = (node) => {
if (
node.type === "FunctionExpression" ||
node.type === "ArrowFunctionExpression"
) {
return node;
}
const keys =
node.type === "CallExpression" || node.type === "NewExpression"
? CALL_WALK_KEYS
: ESTREE_RENAMING_CHILD_KEYS[node.type];
// A name or literal holds none.
if (keys === undefined) return undefined;
const fields = /** @type {PrintFields} */ (
/** @type {PrintFields | PrintNode} */ (node)
);
for (let i = 0; i < keys.length; i++) {
const child = fields[keys[i]];
if (child === null || child === undefined) continue;
const children = Array.isArray(child) ? child : [child];
for (let j = 0; j < children.length; j++) {
const element = children[j];
if (!element) continue;
const found = firstFunction(element);
if (found !== undefined) return found;
}
}
return undefined;
};
/**
* terser's mangling of a `new Function` body minified as a program of its
* own: the program read, mangled, and its first function.
* @param {Node} program terser's program, compressed
* @param {ScopeMangleOptions} given the mangle options
* @returns {PrintFunction | undefined} the function, mangled
*/
modules.mangledFunctionOf = (program, given) => {
const options = formatMangleOptions(given, base54);
const tree = /** @type {PrintProgram} */ (program);
const analysis = analyzeManglingScopes(tree, options, true);
modules.computeEstreeCharFrequency(tree, options);
mangleManglingScopes(analysis, options);
return firstFunction(tree);
};
};
// The phases webpack has taken over, each writing exactly what terser wrote,
// only faster, but `correct`, fixing what it wrote wrong, and `improve`,
// writing less where that provably runs the same; add phases here only.
/**
* @internal
* @typedef {{ name: string, install: (modules: TerserModules) => void }} TerserPhase
*/
/** @type {Phase[]} */
const PHASES = /** @type {Phase[]} */ (
/** @type {(Phase | TerserPhase)[]} */ ([
{ name: "helpers", install: installHelpers },
{ name: "nodes", install: installNodes },
{ name: "defines", install: installDefines },
{ name: "estree", install: installEstree },
{ name: "parse", install: installFusedParse },
{ name: "compressor", install: installCompressor },
{ name: "size", install: installSize },
{ name: "evaluate", install: installEvaluate },
{ name: "inference", install: installInference },
{ name: "drop", install: installDropSideEffectFree },
{ name: "optimize", install: installOptimize },
{ name: "equivalent", install: installEquivalent },
{ name: "hoist", install: installHoist },
{ name: "scope", install: installScope },
{ name: "unused", install: installUnused },
{ name: "reduce", install: installReduce },
{ name: "minify", install: installMinify },
{ name: "frequency", install: installFrequency },
{ name: "output", install: installOutput },
{ name: "estreePrint", install: installEstreePrint },
{ name: "estreeMangle", install: installEstreeMangle },
{ name: "correct", install: installCorrect },
{ name: "improve", install: installImprove },
{ name: "treePrint", install: installTreePrint }
])
);
/** @type {Promise<Minifier> | undefined} */
let loading;
/**
* terser's `OutputStream(options)`: the format options defaulted and checked as
* terser's stream does, written into webpack's stream, which writes no layout.
* @param {TerserModules} modules the modules, `MinifiedOutput` among them
* @param {Partial<FormatSettings>=} given the format options, as a caller passes them
* @returns {OutputStream} the stream
*/
const createOutputStream = (modules, given) => {
const options = /** @type {FormatSettings} */ (
defaults(given, FORMAT_DEFAULTS, true)
);
if (options.shorthand === undefined) options.shorthand = options.ecma > 5;
// terser compiles a comments option written as a regexp, so a bad one throws.
const { comments } = options;
if (typeof comments === "string" && /^\/.*\/[a-zA-Z]*$/.test(comments)) {
const regexpEnd = comments.lastIndexOf("/");
options.comments = new RegExp(
comments.slice(1, regexpEnd),
comments.slice(regexpEnd + 1)
);
}
return new modules.MinifiedOutput(
minifiedOptions(options),
!given,
ESTREE_NODE_ACCESS
);
};
/**
* What terser's modules export that the phases read, built by webpack: the
* node classes, the parser, the compressor, the mangler's names and the
* output stream. The phases install the rest.
* @returns {MinifierModules} the modules
*/
const createModules = () => {
/** @type {TerserModules} */
const modules = /** @type {TerserModules} */ ({});
modules.ast = /** @type {TerserAst} */ (
/** @type {TerserAst | Ast} */ (createAst())
);
modules.parse = /** @type {TerserModules["parse"]} */ (createParseTables());
modules.scope = { base54: createFrequency(mergeSort) };
modules.compress = createCompressorFactory(modules);
modules.output = {
OutputStream: (/** @type {Partial<FormatSettings>=} */ given) =>
createOutputStream(modules, given)
};
modules.domprops = {
domprops: require("./syntax-printer-data").domProperties()
};
return /** @type {MinifierModules} */ (
/** @type {MinifierModules | TerserModules} */ (modules)
);
};
/**
* webpack's minifier: the modules with every phase installed. Built once, so
* a worker builds it once.
* @returns {Promise<Minifier>} the minifier and the phases it is built from
*/
const load = () => {
if (loading === undefined) {
loading = Promise.resolve().then(() => {
const modules = createModules();
for (const phase of PHASES) phase.install(modules);
return {
minify: modules.minify,
phases: PHASES.map((phase) => phase.name),
corrections: modules.corrections,
improvements: modules.improvements,
modules
};
});
}
return loading;
};
module.exports = {
NODE_KIND_ANCESTRY,
plainIdentifier,
load,
PHASES,
FORMAT_DEFAULTS,
IGNORED_FORMAT_OPTIONS,
createAst,
createModules,
createCompressHelpers,
createUnicode
};