diff --git a/lib/DefinePlugin.js b/lib/DefinePlugin.js index c7fb1d85c47..79de6c918ca 100644 --- a/lib/DefinePlugin.js +++ b/lib/DefinePlugin.js @@ -118,6 +118,7 @@ class RuntimeValue { * @param {string} key the defined key * @param {RuntimeTemplate} runtimeTemplate the runtime template * @param {boolean|undefined|null=} asiSafe asi safe (undefined: unknown, null: unneeded) + * @param {Set|undefined=} objKeys used keys * @returns {string} code converted to string that evaluates */ const stringifyObj = ( @@ -126,7 +127,8 @@ const stringifyObj = ( valueCacheVersions, key, runtimeTemplate, - asiSafe + asiSafe, + objKeys ) => { let code; let arr = Array.isArray(obj); @@ -137,7 +139,12 @@ const stringifyObj = ( ) .join(",")}]`; } else { - code = `{${Object.keys(obj) + let keys = Object.keys(obj); + if (objKeys) { + if (objKeys.size === 0) keys = []; + else keys = keys.filter(k => objKeys.has(k)); + } + code = `{${keys .map(key => { const code = obj[key]; return ( @@ -169,6 +176,7 @@ const stringifyObj = ( * @param {string} key the defined key * @param {RuntimeTemplate} runtimeTemplate the runtime template * @param {boolean|undefined|null=} asiSafe asi safe (undefined: unknown, null: unneeded) + * @param {Set|undefined=} objKeys used keys * @returns {string} code converted to string that evaluates */ const toCode = ( @@ -177,7 +185,8 @@ const toCode = ( valueCacheVersions, key, runtimeTemplate, - asiSafe + asiSafe, + objKeys ) => { if (code === null) { return "null"; @@ -211,7 +220,8 @@ const toCode = ( valueCacheVersions, key, runtimeTemplate, - asiSafe + asiSafe, + objKeys ); } if (typeof code === "bigint") { @@ -426,7 +436,8 @@ class DefinePlugin { compilation.valueCacheVersions, originalKey, runtimeTemplate, - !parser.isAsiPosition(expr.range[0]) + !parser.isAsiPosition(expr.range[0]), + parser.destructuringAssignmentPropertiesFor(expr) ); if (WEBPACK_REQUIRE_FUNCTION_REGEXP.test(strCode)) { return toConstantDependency(parser, strCode, [ @@ -523,7 +534,8 @@ class DefinePlugin { compilation.valueCacheVersions, key, runtimeTemplate, - !parser.isAsiPosition(expr.range[0]) + !parser.isAsiPosition(expr.range[0]), + parser.destructuringAssignmentPropertiesFor(expr) ); if (WEBPACK_REQUIRE_FUNCTION_REGEXP.test(strCode)) { diff --git a/lib/dependencies/HarmonyImportDependencyParserPlugin.js b/lib/dependencies/HarmonyImportDependencyParserPlugin.js index 9777333cc5d..ba74c9bbcd6 100644 --- a/lib/dependencies/HarmonyImportDependencyParserPlugin.js +++ b/lib/dependencies/HarmonyImportDependencyParserPlugin.js @@ -196,6 +196,8 @@ module.exports = class HarmonyImportDependencyParserPlugin { exportPresenceMode, settings.assertions ); + dep.referencedPropertiesInDestructuring = + parser.destructuringAssignmentPropertiesFor(expr); dep.shorthand = parser.scope.inShorthand; dep.directImport = true; dep.asiSafe = !parser.isAsiPosition(expr.range[0]); @@ -233,6 +235,8 @@ module.exports = class HarmonyImportDependencyParserPlugin { exportPresenceMode, settings.assertions ); + dep.referencedPropertiesInDestructuring = + parser.destructuringAssignmentPropertiesFor(expr); dep.asiSafe = !parser.isAsiPosition(expr.range[0]); dep.loc = expr.loc; parser.state.module.addDependency(dep); diff --git a/lib/dependencies/HarmonyImportSpecifierDependency.js b/lib/dependencies/HarmonyImportSpecifierDependency.js index 35354ca7bb9..6115614bd07 100644 --- a/lib/dependencies/HarmonyImportSpecifierDependency.js +++ b/lib/dependencies/HarmonyImportSpecifierDependency.js @@ -52,6 +52,8 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency { this.asiSafe = undefined; /** @type {Set | boolean} */ this.usedByExports = undefined; + /** @type {Set} */ + this.referencedPropertiesInDestructuring = undefined; } // TODO webpack 6 remove @@ -121,7 +123,7 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency { */ getReferencedExports(moduleGraph, runtime) { let ids = this.getIds(moduleGraph); - if (ids.length === 0) return Dependency.EXPORTS_OBJECT_REFERENCED; + if (ids.length === 0) return this._getReferencedExportsInDestructuring(); let namespaceObjectAsContext = this.namespaceObjectAsContext; if (ids[0] === "default") { const selfModule = moduleGraph.getParentModule(this); @@ -134,7 +136,8 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency { ) { case "default-only": case "default-with-named": - if (ids.length === 1) return Dependency.EXPORTS_OBJECT_REFERENCED; + if (ids.length === 1) + return this._getReferencedExportsInDestructuring(); ids = ids.slice(1); namespaceObjectAsContext = true; break; @@ -152,7 +155,27 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency { ids = ids.slice(0, -1); } - return [ids]; + return this._getReferencedExportsInDestructuring(ids); + } + + /** + * @param {string[]=} ids ids + * @returns {(string[] | ReferencedExport)[]} referenced exports + */ + _getReferencedExportsInDestructuring(ids) { + if (this.referencedPropertiesInDestructuring) { + /** @type {ReferencedExport[]} */ + const refs = []; + for (const key of this.referencedPropertiesInDestructuring) { + refs.push({ + name: ids ? ids.concat([key]) : [key], + canMangle: false + }); + } + return refs; + } else { + return ids ? [ids] : Dependency.EXPORTS_OBJECT_REFERENCED; + } } /** @@ -226,6 +249,7 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency { write(this.shorthand); write(this.asiSafe); write(this.usedByExports); + write(this.referencedPropertiesInDestructuring); super.serialize(context); } @@ -241,6 +265,7 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency { this.shorthand = read(); this.asiSafe = read(); this.usedByExports = read(); + this.referencedPropertiesInDestructuring = read(); super.deserialize(context); } } diff --git a/lib/javascript/JavascriptParser.js b/lib/javascript/JavascriptParser.js index 58bcc4a64b3..6d9e9e79027 100644 --- a/lib/javascript/JavascriptParser.js +++ b/lib/javascript/JavascriptParser.js @@ -331,6 +331,8 @@ class JavascriptParser extends Parser { /** @type {(StatementNode|ExpressionNode)[]} */ this.statementPath = undefined; this.prevStatement = undefined; + /** @type {WeakMap>} */ + this.destructuringAssignmentProperties = undefined; this.currentTagData = undefined; this._initializeEvaluating(); } @@ -1411,6 +1413,15 @@ class JavascriptParser extends Parser { }); } + /** + * @param {ExpressionNode} node node + * @returns {Set|undefined} destructured identifiers + */ + destructuringAssignmentPropertiesFor(node) { + if (!this.destructuringAssignmentProperties) return undefined; + return this.destructuringAssignmentProperties.get(node); + } + getRenameIdentifier(expr) { const result = this.evaluateExpression(expr); if (result.isIdentifier()) { @@ -1557,6 +1568,8 @@ class JavascriptParser extends Parser { case "ClassDeclaration": this.blockPreWalkClassDeclaration(statement); break; + case "ExpressionStatement": + this.blockPreWalkExpressionStatement(statement); } this.prevStatement = this.statementPath.pop(); } @@ -1890,6 +1903,37 @@ class JavascriptParser extends Parser { this.scope.topLevelScope = wasTopLevel; } + blockPreWalkExpressionStatement(statement) { + const expression = statement.expression; + switch (expression.type) { + case "AssignmentExpression": + this.preWalkAssignmentExpression(expression); + } + } + + preWalkAssignmentExpression(expression) { + if ( + expression.left.type !== "ObjectPattern" || + !this.destructuringAssignmentProperties + ) + return; + const keys = this._preWalkObjectPattern(expression.left); + if (!keys) return; + + // check multiple assignments + if (this.destructuringAssignmentProperties.has(expression)) { + const set = this.destructuringAssignmentProperties.get(expression); + this.destructuringAssignmentProperties.delete(expression); + for (const id of set) keys.add(id); + } + + this.destructuringAssignmentProperties.set(expression.right, keys); + + if (expression.right.type === "AssignmentExpression") { + this.preWalkAssignmentExpression(expression.right); + } + } + blockPreWalkImportDeclaration(statement) { const source = statement.source.value; this.hooks.import.call(statement, source); @@ -2087,6 +2131,7 @@ class JavascriptParser extends Parser { for (const declarator of statement.declarations) { switch (declarator.type) { case "VariableDeclarator": { + this.preWalkVariableDeclarator(declarator); if (!this.hooks.preDeclarator.call(declarator, statement)) { this.enterPattern(declarator.id, (name, decl) => { let hook = hookMap.get(name); @@ -2104,6 +2149,47 @@ class JavascriptParser extends Parser { } } + _preWalkObjectPattern(objectPattern) { + const ids = new Set(); + const properties = objectPattern.properties; + for (let i = 0; i < properties.length; i++) { + const property = properties[i]; + if (property.type !== "Property") return; + const key = property.key; + if (key.type === "Identifier") { + ids.add(key.name); + } else { + const id = this.evaluateExpression(key); + const str = id.asString(); + if (str) { + ids.add(str); + } else { + // could not evaluate key + return; + } + } + } + + return ids; + } + + preWalkVariableDeclarator(declarator) { + if ( + !declarator.init || + declarator.id.type !== "ObjectPattern" || + !this.destructuringAssignmentProperties + ) + return; + const keys = this._preWalkObjectPattern(declarator.id); + + if (!keys) return; + this.destructuringAssignmentProperties.set(declarator.init, keys); + + if (declarator.init.type === "AssignmentExpression") { + this.preWalkAssignmentExpression(declarator.init); + } + } + walkVariableDeclaration(statement) { for (const declarator of statement.declarations) { switch (declarator.type) { @@ -3367,12 +3453,14 @@ class JavascriptParser extends Parser { this.statementPath = []; this.prevStatement = undefined; if (this.hooks.program.call(ast, comments) === undefined) { + this.destructuringAssignmentProperties = new WeakMap(); this.detectMode(ast.body); this.preWalkStatements(ast.body); this.prevStatement = undefined; this.blockPreWalkStatements(ast.body); this.prevStatement = undefined; this.walkStatements(ast.body); + this.destructuringAssignmentProperties = undefined; } this.hooks.finish.call(ast, comments); this.scope = oldScope; diff --git a/test/cases/parsing/harmony-destructuring-assignment/counter.js b/test/cases/parsing/harmony-destructuring-assignment/counter.js new file mode 100644 index 00000000000..a33b7727575 --- /dev/null +++ b/test/cases/parsing/harmony-destructuring-assignment/counter.js @@ -0,0 +1,9 @@ +export let counter = 0; +export const d = 1; +export const c = 1; + +export const exportsInfo = { + counter: __webpack_exports_info__.counter.used, + d: __webpack_exports_info__.d.used, + c: __webpack_exports_info__.c.used +}; diff --git a/test/cases/parsing/harmony-destructuring-assignment/counter2.js b/test/cases/parsing/harmony-destructuring-assignment/counter2.js new file mode 100644 index 00000000000..21dbf67c4b0 --- /dev/null +++ b/test/cases/parsing/harmony-destructuring-assignment/counter2.js @@ -0,0 +1,7 @@ +export let counter = 0; +export const d = 1; + +export const exportsInfo = { + counter: __webpack_exports_info__.counter.used, + d: __webpack_exports_info__.d.used +}; diff --git a/test/cases/parsing/harmony-destructuring-assignment/counter3.js b/test/cases/parsing/harmony-destructuring-assignment/counter3.js new file mode 100644 index 00000000000..21dbf67c4b0 --- /dev/null +++ b/test/cases/parsing/harmony-destructuring-assignment/counter3.js @@ -0,0 +1,7 @@ +export let counter = 0; +export const d = 1; + +export const exportsInfo = { + counter: __webpack_exports_info__.counter.used, + d: __webpack_exports_info__.d.used +}; diff --git a/test/cases/parsing/harmony-destructuring-assignment/counter4.js b/test/cases/parsing/harmony-destructuring-assignment/counter4.js new file mode 100644 index 00000000000..43eff0ee0a3 --- /dev/null +++ b/test/cases/parsing/harmony-destructuring-assignment/counter4.js @@ -0,0 +1,15 @@ +export let counter = 0; +export const d = 1; +export const c = 1; +export const e = 1; +export const f = 1; +export const g = 1; + +export const exportsInfo = { + counter: __webpack_exports_info__.counter.used, + d: __webpack_exports_info__.d.used, + c: __webpack_exports_info__.c.used, + e: __webpack_exports_info__.e.used, + f: __webpack_exports_info__.f.used, + g: __webpack_exports_info__.g.used +}; diff --git a/test/cases/parsing/harmony-destructuring-assignment/index.js b/test/cases/parsing/harmony-destructuring-assignment/index.js new file mode 100644 index 00000000000..42e573e3900 --- /dev/null +++ b/test/cases/parsing/harmony-destructuring-assignment/index.js @@ -0,0 +1,55 @@ +import * as C from "./reexport-namespace"; +import { counter } from "./reexport-namespace"; +import { exportsInfo } from "./counter"; +import { exportsInfo as exportsInfo2 } from "./counter2"; +import * as counter3 from "./counter3"; +import * as counter4 from "./counter4"; + +it("expect tree-shake unused exports #1", () => { + const { D } = C; + expect(D).toBe(1); + expect(C.exportsInfo.D).toBe(true); + expect(C.exportsInfo.E).toBe(false); +}); + +it("expect tree-shake unused exports #2", () => { + const { d, c } = C.counter; + const { ['d']: d1 } = counter; + expect(d).toBe(1); + expect(c).toBe(1); + expect(d1).toBe(1); + expect(exportsInfo.d).toBe(true); + expect(exportsInfo.c).toBe(true); + expect(exportsInfo.counter).toBe(false); +}); + +it("expect multiple assignment work correctly", () => { + const { e, d: d1 } = counter4; + let c1; + const { f, d: d2 } = { c: c1 } = counter4; + expect(c1).toBe(1); + expect(d1).toBe(1); + expect(d2).toBe(1); + expect(e).toBe(1); + expect(f).toBe(1); + expect(counter4.exportsInfo.c).toBe(true); + expect(counter4.exportsInfo.d).toBe(true); + expect(counter4.exportsInfo.e).toBe(true); + expect(counter4.exportsInfo.f).toBe(true); + expect(counter4.exportsInfo.g).toBe(false); + expect(counter4.exportsInfo.counter).toBe(false); +}); + +it("expect tree-shake bailout when rest element is used", () => { + const { d, ...rest } = counter3; + expect(d).toBe(1); + expect(rest.exportsInfo.d).toBe(true); + expect(rest.exportsInfo.counter).toBe(true); +}); + +it("expect no support of \"deep\" tree-shaking", () => { + const { counter2: { d } } = C; + expect(d).toBe(1); + expect(exportsInfo2.d).toBe(true); + expect(exportsInfo2.counter).toBe(true); +}); diff --git a/test/cases/parsing/harmony-destructuring-assignment/reexport-namespace.js b/test/cases/parsing/harmony-destructuring-assignment/reexport-namespace.js new file mode 100644 index 00000000000..4a41ad89f66 --- /dev/null +++ b/test/cases/parsing/harmony-destructuring-assignment/reexport-namespace.js @@ -0,0 +1,14 @@ +import * as counter from "./counter"; +export { counter }; +import * as counter2 from "./counter2"; +export { counter2 }; + +export const D = 1; +export const E = 1; + +export const exportsInfo = { + D: __webpack_exports_info__.D.used, + E: __webpack_exports_info__.E.used, + counter: __webpack_exports_info__.counter.used, + counter2: __webpack_exports_info__.counter2.used, +}; diff --git a/test/cases/parsing/harmony-destructuring-assignment/test.filter.js b/test/cases/parsing/harmony-destructuring-assignment/test.filter.js new file mode 100644 index 00000000000..181167c763e --- /dev/null +++ b/test/cases/parsing/harmony-destructuring-assignment/test.filter.js @@ -0,0 +1,4 @@ +module.exports = function(config) { + // This test can't run in development mode + return config.mode !== "development"; +}; diff --git a/test/configCases/plugins/define-plugin/index.js b/test/configCases/plugins/define-plugin/index.js index e3cde299308..f79974071a2 100644 --- a/test/configCases/plugins/define-plugin/index.js +++ b/test/configCases/plugins/define-plugin/index.js @@ -248,3 +248,10 @@ it("should expand properly", function() { expect(require("./dir/" + (tmp + A_DOT_J + tmp) + "s")).toBe(a); expect(require("./dir/" + (tmp + A_DOT_J) + tmp + "s")).toBe(a); }); + +it("destructuring assignment", () => { + const {used} = OBJECT2; + const {['used']: used2, used: used3} = OBJECT2.sub; + expect(used).toBe(used2); + expect(used).toBe(used3); +}); diff --git a/test/configCases/plugins/define-plugin/webpack.config.js b/test/configCases/plugins/define-plugin/webpack.config.js index 4f202b594c6..12810899a97 100644 --- a/test/configCases/plugins/define-plugin/webpack.config.js +++ b/test/configCases/plugins/define-plugin/webpack.config.js @@ -47,7 +47,15 @@ module.exports = { return module instanceof Module; } ), - A_DOT_J: '"a.j"' + A_DOT_J: '"a.j"', + OBJECT2: { + used: 1, + unused: "(() => throw new Error('unused property was rendered'))()", + sub: { + used: 1, + unused: "(() => throw new Error('unused property was rendered'))()" + } + } }) ] }; diff --git a/types.d.ts b/types.d.ts index b1ec17297e7..deccf6949fd 100644 --- a/types.d.ts +++ b/types.d.ts @@ -5277,7 +5277,11 @@ declare class JavascriptParser extends Parser { | ForOfStatement )[]; prevStatement: any; + destructuringAssignmentProperties: WeakMap>; currentTagData: any; + destructuringAssignmentPropertiesFor( + node: Expression + ): undefined | Set; getRenameIdentifier(expr?: any): undefined | string | VariableInfoInterface; walkClass(classy: ClassExpression | ClassDeclaration): void; preWalkStatements(statements?: any): void; @@ -5321,6 +5325,8 @@ declare class JavascriptParser extends Parser { walkForOfStatement(statement?: any): void; preWalkFunctionDeclaration(statement?: any): void; walkFunctionDeclaration(statement?: any): void; + blockPreWalkExpressionStatement(statement?: any): void; + preWalkAssignmentExpression(expression?: any): void; blockPreWalkImportDeclaration(statement?: any): void; enterDeclaration(declaration?: any, onIdent?: any): void; blockPreWalkExportNamedDeclaration(statement?: any): void; @@ -5330,6 +5336,7 @@ declare class JavascriptParser extends Parser { blockPreWalkExportAllDeclaration(statement?: any): void; preWalkVariableDeclaration(statement?: any): void; blockPreWalkVariableDeclaration(statement?: any): void; + preWalkVariableDeclarator(declarator?: any): void; walkVariableDeclaration(statement?: any): void; blockPreWalkClassDeclaration(statement?: any): void; walkClassDeclaration(statement?: any): void;