diff --git a/lib/JavascriptParser.js b/lib/JavascriptParser.js index a031c76b95a..408dbb1cbdd 100644 --- a/lib/JavascriptParser.js +++ b/lib/JavascriptParser.js @@ -9,19 +9,21 @@ const { Parser } = require("acorn"); const { SyncBailHook, HookMap } = require("tapable"); const vm = require("vm"); const BasicEvaluatedExpression = require("./BasicEvaluatedExpression"); -const StackedSetMap = require("./util/StackedSetMap"); +const StackedMap = require("./util/StackedMap"); // Syntax: https://developer.mozilla.org/en/SpiderMonkey/Parser_API const parser = Parser.extend(require("./parsing/importAwaitAcornPlugin")); -/** - * @typedef {Object} VariableInfo - * @property {string | true} freeName - * @property {TagInfo} tagInfo - */ +class VariableInfo { + constructor(declaredScope, freeName, tagInfo) { + this.declaredScope = declaredScope; + this.freeName = freeName; + this.tagInfo = tagInfo; + } +} -/** @typedef {string | true | VariableInfo} ExportedVariableInfo */ +/** @typedef {string | ScopeInfo | VariableInfo} ExportedVariableInfo */ /** * @typedef {Object} TagInfo @@ -32,7 +34,7 @@ const parser = Parser.extend(require("./parsing/importAwaitAcornPlugin")); /** * @typedef {Object} ScopeInfo - * @property {StackedSetMap} definitions + * @property {StackedMap} definitions * @property {boolean | "arrow"} topLevelScope * @property {boolean} inShorthand * @property {boolean} isStrict @@ -2132,16 +2134,16 @@ class JavascriptParser { * @returns {any} resut of hook */ callHooksForInfoWithFallback(hookMap, info, fallback, defined, ...args) { - if (info === true) { - if (defined !== undefined) { - return defined(); - } - return; - } let name; if (typeof info === "string") { name = info; } else { + if (!(info instanceof VariableInfo)) { + if (defined !== undefined) { + return defined(); + } + return; + } let tagInfo = info.tagInfo; while (tagInfo !== undefined) { const hook = hookMap.get(tagInfo.tag); @@ -2474,7 +2476,7 @@ class JavascriptParser { inTry: false, inShorthand: false, isStrict: false, - definitions: new StackedSetMap() + definitions: new StackedMap() }; const state = (this.state = initialState || {}); this.comments = comments; @@ -2529,7 +2531,7 @@ class JavascriptParser { getTagData(name, tag) { const info = this.scope.definitions.get(name); - if (typeof info === "object") { + if (info instanceof VariableInfo) { let tagInfo = info.tagInfo; while (tagInfo !== undefined) { if (tagInfo.tag === tag) return tagInfo.data; @@ -2543,38 +2545,33 @@ class JavascriptParser { /** @type {VariableInfo} */ let newInfo; if (oldInfo === undefined) { - newInfo = { - freeName: name, - tagInfo: { - tag, - data, - next: undefined - } - }; - } else if (oldInfo === true) { - newInfo = { - freeName: true, - tagInfo: { - tag, - data, - next: undefined - } - }; + newInfo = new VariableInfo(this.scope, name, { + tag, + data, + next: undefined + }); + } else if (oldInfo instanceof VariableInfo) { + newInfo = new VariableInfo(oldInfo.declaredScope, oldInfo.freeName, { + tag, + data, + next: oldInfo.tagInfo + }); } else { - newInfo = { - freeName: oldInfo.freeName, - tagInfo: { - tag, - data, - next: oldInfo.tagInfo - } - }; + newInfo = new VariableInfo(oldInfo, true, { + tag, + data, + next: undefined + }); } this.scope.definitions.set(name, newInfo); } defineVariable(name) { - this.scope.definitions.set(name, true); + const oldInfo = this.scope.definitions.get(name); + // Don't redefine variable in same scope to keep existing tags + if (oldInfo instanceof VariableInfo && oldInfo.declaredScope === this.scope) + return; + this.scope.definitions.set(name, this.scope); } undefineVariable(name) { @@ -2589,8 +2586,6 @@ class JavascriptParser { const value = this.scope.definitions.get(name); if (value === undefined) { return name; - } else if (value === true) { - return true; } else { return value; } @@ -2606,10 +2601,10 @@ class JavascriptParser { if (variableInfo === name) { this.scope.definitions.delete(name); } else { - this.scope.definitions.set(name, { - freeName: variableInfo, - tagInfo: undefined - }); + this.scope.definitions.set( + name, + new VariableInfo(this.scope, variableInfo, undefined) + ); } } else { this.scope.definitions.set(name, variableInfo); @@ -2664,14 +2659,14 @@ class JavascriptParser { return undefined; } const rootInfo = this.getVariableInfo(rootName); - /** @type {string | true} */ + /** @type {string | ScopeInfo} */ let resolvedRoot; - if (typeof rootInfo === "object") { + if (rootInfo instanceof VariableInfo) { resolvedRoot = rootInfo.freeName; } else { resolvedRoot = rootInfo; } - if (resolvedRoot === true) { + if (typeof resolvedRoot !== "string") { return undefined; } let name = resolvedRoot; diff --git a/lib/optimize/InnerGraphPlugin.js b/lib/optimize/InnerGraphPlugin.js index 868d423154b..700d903e946 100644 --- a/lib/optimize/InnerGraphPlugin.js +++ b/lib/optimize/InnerGraphPlugin.js @@ -15,6 +15,20 @@ const topLevelSymbolTag = Symbol("top level symbol"); /** @typedef {Map | true>} InnerGraph */ +const isPure = expr => { + switch (expr.type) { + case "Identifier": + return true; + case "Literal": + return true; + case "ConditionalExpression": + return ( + isPure(expr.test) && isPure(expr.consequent) && isPure(expr.alternate) + ); + } + return false; +}; + class TopLevelSymbol { /** * @param {string} name name of the function @@ -65,7 +79,7 @@ class InnerGraphPlugin { parser.state.harmonyAllExportDependentDependencies = new Set(); }); - parser.hooks.finish.tap("HarmonyImportDependencyParserPlugin", () => { + parser.hooks.finish.tap("InnerGraphPlugin", () => { const innerGraph = /** @type {InnerGraph} */ (parser.state.harmonyInnerGraph); if (!innerGraph) return; @@ -158,14 +172,26 @@ class InnerGraphPlugin { ) { const innerGraph = /** @type {InnerGraph} */ (parser.state.harmonyInnerGraph); - parser.defineVariable("*default*"); - const fn = new TopLevelSymbol("*default*", innerGraph); - parser.tagVariable("*default*", topLevelSymbolTag, fn); + const name = "*default*"; + parser.defineVariable(name); + const fn = new TopLevelSymbol(name, innerGraph); + parser.tagVariable(name, topLevelSymbolTag, fn); statementWithTopLevelSymbol.set(statement, fn); } } } }); + const tagVar = name => { + const innerGraph = + /** @type {InnerGraph} */ (parser.state.harmonyInnerGraph); + parser.defineVariable(name); + const existingTag = parser.getTagData(name, topLevelSymbolTag); + const fn = existingTag || new TopLevelSymbol(name, innerGraph); + if (!existingTag) { + parser.tagVariable(name, topLevelSymbolTag, fn); + } + return fn; + }; /** @type {WeakMap<{}, TopLevelSymbol>} */ const declWithTopLevelSymbol = new WeakMap(); parser.hooks.preDeclarator.tap( @@ -181,30 +207,26 @@ class InnerGraphPlugin { decl.init.type === "ArrowFunctionExpression" || decl.init.type === "ClassExpression" ) { - const innerGraph = - /** @type {InnerGraph} */ (parser.state.harmonyInnerGraph); - parser.defineVariable(decl.id.name); - const fn = new TopLevelSymbol(decl.id.name, innerGraph); - parser.tagVariable(decl.id.name, topLevelSymbolTag, fn); + const name = decl.id.name; + const fn = tagVar(name); declWithTopLevelSymbol.set(decl, fn); return true; } if ( - decl.init.range[0] - decl.id.range[1] > 9 && - parser - .getComments([decl.id.range[1], decl.init.range[0]]) - .some( - comment => - comment.type === "Block" && - /^\s*(#|@)__PURE__\s*$/.test(comment.value) - ) + (decl.init.range[0] - decl.id.range[1] > 9 && + parser + .getComments([decl.id.range[1], decl.init.range[0]]) + .some( + comment => + comment.type === "Block" && + /^\s*(#|@)__PURE__\s*$/.test(comment.value) + )) || + isPure(decl.init) ) { const innerGraph = /** @type {InnerGraph} */ (parser.state.harmonyInnerGraph); const name = decl.id.name; - parser.defineVariable(name); - const fn = new TopLevelSymbol(name, innerGraph); - parser.tagVariable(name, topLevelSymbolTag, fn); + const fn = tagVar(name); declWithTopLevelSymbol.set(decl, fn); const dep = new PureExpressionDependency(decl.init.range); dep.loc = decl.loc; diff --git a/lib/optimize/ModuleConcatenationPlugin.js b/lib/optimize/ModuleConcatenationPlugin.js index ad26330e9aa..a8952b70aee 100644 --- a/lib/optimize/ModuleConcatenationPlugin.js +++ b/lib/optimize/ModuleConcatenationPlugin.js @@ -12,7 +12,7 @@ const HarmonyCompatibilityDependency = require("../dependencies/HarmonyCompatibi const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency"); const ModuleHotAcceptDependency = require("../dependencies/ModuleHotAcceptDependency"); const ModuleHotDeclineDependency = require("../dependencies/ModuleHotDeclineDependency"); -const StackedSetMap = require("../util/StackedSetMap"); +const StackedMap = require("../util/StackedMap"); const ConcatenatedModule = require("./ConcatenatedModule"); /** @typedef {import("../Compilation")} Compilation */ @@ -487,21 +487,21 @@ class ConcatConfiguration { constructor(rootModule, cloneFrom) { this.rootModule = rootModule; if (cloneFrom) { - /** @type {StackedSetMap} */ + /** @type {StackedMap} */ this.modules = cloneFrom.modules.createChild(); - /** @type {StackedSetMap} */ + /** @type {StackedMap} */ this.warnings = cloneFrom.warnings.createChild(); } else { - /** @type {StackedSetMap} */ - this.modules = new StackedSetMap(); - this.modules.add(rootModule); - /** @type {StackedSetMap} */ - this.warnings = new StackedSetMap(); + /** @type {StackedMap} */ + this.modules = new StackedMap(); + this.modules.set(rootModule, true); + /** @type {StackedMap} */ + this.warnings = new StackedMap(); } } add(module) { - this.modules.add(module); + this.modules.set(module, true); } has(module) { diff --git a/lib/util/StackedMap.js b/lib/util/StackedMap.js new file mode 100644 index 00000000000..bb5e776ccca --- /dev/null +++ b/lib/util/StackedMap.js @@ -0,0 +1,166 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const TOMBSTONE = Symbol("tombstone"); +const UNDEFINED_MARKER = Symbol("undefined"); + +/** + * @template T + * @typedef {T | undefined} Cell + */ + +/** + * @template T + * @typedef {T | typeof TOMBSTONE | typeof UNDEFINED_MARKER} InternalCell + */ + +/** + * @template K + * @template V + * @param {[K, InternalCell]} pair the internal cell + * @returns {[K, Cell]} its “safe” representation + */ +const extractPair = pair => { + const key = pair[0]; + const val = pair[1]; + if (val === UNDEFINED_MARKER || val === TOMBSTONE) { + return [key, undefined]; + } else { + return /** @type {[K, Cell]} */ (pair); + } +}; + +/** + * @template K + * @template V + */ +class StackedMap { + /** + * @param {Map>[]=} parentStack an optional parent + */ + constructor(parentStack) { + /** @type {Map>} */ + this.map = new Map(); + /** @type {Map>[]} */ + this.stack = parentStack === undefined ? [] : parentStack.slice(); + this.stack.push(this.map); + } + + /** + * @param {K} item the key of the element to add + * @param {V} value the value of the element to add + * @returns {void} + */ + set(item, value) { + this.map.set(item, value === undefined ? UNDEFINED_MARKER : value); + } + + /** + * @param {K} item the item to delete + * @returns {void} + */ + delete(item) { + if (this.stack.length > 1) { + this.map.set(item, TOMBSTONE); + } else { + this.map.delete(item); + } + } + + /** + * @param {K} item the item to test + * @returns {boolean} true if the item exists in this set + */ + has(item) { + const topValue = this.map.get(item); + if (topValue !== undefined) { + return topValue !== TOMBSTONE; + } + if (this.stack.length > 1) { + for (let i = this.stack.length - 2; i >= 0; i--) { + const value = this.stack[i].get(item); + if (value !== undefined) { + this.map.set(item, value); + return value !== TOMBSTONE; + } + } + this.map.set(item, TOMBSTONE); + } + return false; + } + + /** + * @param {K} item the key of the element to return + * @returns {Cell} the value of the element + */ + get(item) { + const topValue = this.map.get(item); + if (topValue !== undefined) { + return topValue === TOMBSTONE || topValue === UNDEFINED_MARKER + ? undefined + : topValue; + } + if (this.stack.length > 1) { + for (let i = this.stack.length - 2; i >= 0; i--) { + const value = this.stack[i].get(item); + if (value !== undefined) { + this.map.set(item, value); + return value === TOMBSTONE || value === UNDEFINED_MARKER + ? undefined + : value; + } + } + this.map.set(item, TOMBSTONE); + } + return undefined; + } + + _compress() { + if (this.stack.length === 1) return; + this.map = new Map(); + for (const data of this.stack) { + for (const pair of data) { + if (pair[1] === TOMBSTONE) { + this.map.delete(pair[0]); + } else { + this.map.set(pair[0], pair[1]); + } + } + } + this.stack = [this.map]; + } + + asArray() { + this._compress(); + return Array.from(this.map.keys()); + } + + asSet() { + this._compress(); + return new Set(this.map.keys()); + } + + asPairArray() { + this._compress(); + return Array.from(this.map.entries(), extractPair); + } + + asMap() { + return new Map(this.asPairArray()); + } + + get size() { + this._compress(); + return this.map.size; + } + + createChild() { + return new StackedMap(this.stack); + } +} + +module.exports = StackedMap; diff --git a/lib/util/StackedSetMap.js b/lib/util/StackedSetMap.js index 091c4595304..bb5e776ccca 100644 --- a/lib/util/StackedSetMap.js +++ b/lib/util/StackedSetMap.js @@ -10,12 +10,12 @@ const UNDEFINED_MARKER = Symbol("undefined"); /** * @template T - * @typedef {T | true | undefined} Cell + * @typedef {T | undefined} Cell */ /** * @template T - * @typedef {T | true | typeof TOMBSTONE | typeof UNDEFINED_MARKER} InternalCell + * @typedef {T | typeof TOMBSTONE | typeof UNDEFINED_MARKER} InternalCell */ /** @@ -38,7 +38,7 @@ const extractPair = pair => { * @template K * @template V */ -class StackedSetMap { +class StackedMap { /** * @param {Map>[]=} parentStack an optional parent */ @@ -50,14 +50,6 @@ class StackedSetMap { this.stack.push(this.map); } - /** - * @param {K} item the item to add - * @returns {void} - */ - add(item) { - this.map.set(item, true); - } - /** * @param {K} item the key of the element to add * @param {V} value the value of the element to add @@ -167,8 +159,8 @@ class StackedSetMap { } createChild() { - return new StackedSetMap(this.stack); + return new StackedMap(this.stack); } } -module.exports = StackedSetMap; +module.exports = StackedMap; diff --git a/lib/util/TrackingSet.js b/lib/util/TrackingSet.js deleted file mode 100644 index c92a410d5ea..00000000000 --- a/lib/util/TrackingSet.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ - -"use strict"; - -/** - * @template T - * @template U - * @typedef {import("./StackedSetMap")} StackedSetMap - */ - -/** - * @template T - * @template U - */ -class TrackingSet { - /** - * @param {StackedSetMap} set the set to track - */ - constructor(set) { - /** @type {StackedSetMap} */ - this.set = set; - /** @type {Set} */ - this.set2 = new Set(); - this.stack = set.stack; - } - - /** - * @param {T} item the item to add - * @returns {void} - */ - add(item) { - this.set2.add(item); - this.set.add(item); - } - - /** - * @param {T} item the item to delete - * @returns {void} - */ - delete(item) { - this.set2.delete(item); - this.set.delete(item); - } - - /** - * @param {T} item the item to test - * @returns {boolean} true if the item exists in this set - */ - has(item) { - return this.set.has(item); - } - - createChild() { - return this.set.createChild(); - } - - getAddedItems() { - return this.set2; - } -} - -module.exports = TrackingSet; diff --git a/test/__snapshots__/StatsTestCases.test.js.snap b/test/__snapshots__/StatsTestCases.test.js.snap index 625019464ad..203f79d5628 100644 --- a/test/__snapshots__/StatsTestCases.test.js.snap +++ b/test/__snapshots__/StatsTestCases.test.js.snap @@ -635,26 +635,26 @@ Entrypoint entry-1 = vendor-1.js entry-1.js `; exports[`StatsTestCases should print correct stats for commons-plugin-issue-4980 1`] = ` -"Hash: 4de9d58c5e07bace3a862cc016e810ab8d72814f +"Hash: e8a1307ea3c8604531519325cea6a11512fc55f7 Child - Hash: 4de9d58c5e07bace3a86 + Hash: e8a1307ea3c860453151 Time: Xms Built at: 1970-04-20 12:42:42 Asset Size Chunks Chunk Names app.20e7d920f7a5dd7ba86a.js 6.72 KiB {143} [emitted] app - vendor.44602d2bdfb280718742.js 606 bytes {736} [emitted] vendor - Entrypoint app = vendor.44602d2bdfb280718742.js app.20e7d920f7a5dd7ba86a.js + vendor.3ca106870164d059e3b7.js 606 bytes {736} [emitted] vendor + Entrypoint app = vendor.3ca106870164d059e3b7.js app.20e7d920f7a5dd7ba86a.js [117] ./entry-1.js + 2 modules 190 bytes {143} [built] [381] ./constants.js 87 bytes {736} [built] + 4 hidden modules Child - Hash: 2cc016e810ab8d72814f + Hash: 9325cea6a11512fc55f7 Time: Xms Built at: 1970-04-20 12:42:42 Asset Size Chunks Chunk Names app.9ff34c93a677586ea3e1.js 6.74 KiB {143} [emitted] app - vendor.44602d2bdfb280718742.js 606 bytes {736} [emitted] vendor - Entrypoint app = vendor.44602d2bdfb280718742.js app.9ff34c93a677586ea3e1.js + vendor.3ca106870164d059e3b7.js 606 bytes {736} [emitted] vendor + Entrypoint app = vendor.3ca106870164d059e3b7.js app.9ff34c93a677586ea3e1.js [381] ./constants.js 87 bytes {736} [built] [655] ./entry-2.js + 2 modules 197 bytes {143} [built] + 4 hidden modules" @@ -1103,7 +1103,7 @@ chunk {trees} trees.js (trees) 71 bytes [rendered] `; exports[`StatsTestCases should print correct stats for import-context-filter 1`] = ` -"Hash: ad66943c56c470a291e2 +"Hash: 10cc1554ce1aa0fa0104 Time: Xms Built at: 1970-04-20 12:42:42 Asset Size Chunks Chunk Names @@ -1525,11 +1525,11 @@ If you don't want to include a polyfill, you can use an empty module like this: `; exports[`StatsTestCases should print correct stats for module-reasons 1`] = ` -"Hash: d467214b226e6b223582 +"Hash: 7c9265c9403a708454f8 Time: Xms Built at: 1970-04-20 12:42:42 - Asset Size Chunks Chunk Names -main.js 2.8 KiB {179} [emitted] main + Asset Size Chunks Chunk Names +main.js 2.89 KiB {179} [emitted] main Entrypoint main = main.js [237] ./index.js + 2 modules 102 bytes {179} [built] entry ./index main @@ -2467,7 +2467,7 @@ Entrypoint e2 = runtime.js e2.js" `; exports[`StatsTestCases should print correct stats for scope-hoisting-bailouts 1`] = ` -"Hash: 8d50ea5a3930d4e325f5 +"Hash: f517daabbf7f98de61bd Time: Xms Built at: 1970-04-20 12:42:42 Entrypoint index = index.js @@ -2593,7 +2593,7 @@ Entrypoint main = main.js `; exports[`StatsTestCases should print correct stats for side-effects-simple-unused 1`] = ` -"Hash: 0dd96a6d2b50b602067c +"Hash: d07e55dc2ba899045482 Time: Xms Built at: 1970-04-20 12:42:42 Asset Size Chunks Chunk Names @@ -3625,11 +3625,11 @@ chunk {794} default/async-a.js (async-a) 134 bytes <{179}> [rendered] `; exports[`StatsTestCases should print correct stats for tree-shaking 1`] = ` -"Hash: d1c58bcd9027a12fc6a5 +"Hash: 394ab56fdb998a8398e6 Time: Xms Built at: 1970-04-20 12:42:42 Asset Size Chunks Chunk Names -bundle.js 7.78 KiB {179} [emitted] main +bundle.js 7.86 KiB {179} [emitted] main Entrypoint main = bundle.js [10] ./index.js 315 bytes {179} [built] [no exports] diff --git a/test/configCases/inner-graph/blockScopes/module.js b/test/configCases/inner-graph/blockScopes/module.js new file mode 100644 index 00000000000..cbec71ca04a --- /dev/null +++ b/test/configCases/inner-graph/blockScopes/module.js @@ -0,0 +1,47 @@ +import { A, B, C1, C2, D1, D2, E1, E2, E3, F, G } from "./test"; + +export { a, b, c, d, e }; + +if (Math.random() > 0.5) { + var a = () => A; + let b = () => B; +} + +let b; + +var c = () => C1; +couldCallExportC(); +var c = () => C2; +couldCallExportC(); + +while (Math.random() > 0.5) { + let d = () => D1; +} + +while (Math.random() > 0.5) { + var d = () => D2; +} + +while (Math.random() > 0.5) { + let d = () => D1; +} + +if(false) { + E1(); +} + +export var e = true ? E2 : E3; + +export { f, g } + +if(true) { + let inner = () => F; + + var f = () => inner(); +} + +if(true) { + const inner = () => G; + + var g = () => inner(); +} diff --git a/test/configCases/inner-graph/blockScopes/webpack.config.js b/test/configCases/inner-graph/blockScopes/webpack.config.js new file mode 100644 index 00000000000..6df8a488b0b --- /dev/null +++ b/test/configCases/inner-graph/blockScopes/webpack.config.js @@ -0,0 +1,51 @@ +const createTestCases = require("../_helpers/createTestCases"); +module.exports = createTestCases({ + nothing: { + usedExports: [], + expect: { + "./test": [] + } + }, + a: { + usedExports: ["a"], + expect: { + "./test": ["A"] + } + }, + b: { + usedExports: ["b"], + expect: { + "./test": [] + } + }, + c: { + usedExports: ["c"], + expect: { + "./test": ["C1", "C2"] + } + }, + d: { + usedExports: ["d"], + expect: { + "./test": ["D2"] + } + }, + e: { + usedExports: ["e"], + expect: { + "./test": ["E2"] + } + }, + f: { + usedExports: ["f"], + expect: { + "./test": ["F"] + } + }, + g: { + usedExports: ["g"], + expect: { + "./test": ["G"] + } + } +});