Repository: webpack/webpack (JavaScript)

Issue:
Normalize property accessors for es6 namespaces and chained member/call expressions
## Feature request

Currently webpack rewrites chained member and call expressions in dot notation.

e.g.,

```
/* module1.js */
export const obj1 = {};

/* module2.js */
import * as m1 from './module1';
export { m1 };

/* index.js */
import * as m2 from './module2';
const a = m2.m1.obj1["flip"].flap["flop"]();
```
generates
```
const a = _module2__WEBPACK_IMPORTED_MODULE_0__.m1.obj1.flip.flap.flop();
```

Some optimizing compilers such as the Google Closure Compiler `ADVANCED` mode and Terser with `keep_quoted` enabled have a problem with this rewrite. They use the quote notation as a way to say, "hands off" when it comes to minifying variable names so that external interfaces can be preserved. This leads to it seeing x["somevar"] and x.somevar as two different properties, such that x.somevar is minified and x["somevar"] is converted to x.somevar (shorter by one char but not minified). See [the Closure docs](https://developers.google.com/closure/compiler/docs/api-tutorial3#propnames) for more.

**What is the expected behavior?**

The expression after the imported object (obj1 in this case) should remain untouched.  That is,
```
const a = _module2__WEBPACK_IMPORTED_MODULE_0__.m1.obj1["flip"].flap["flop"]();
```

**What is motivation or use case for adding/changing the behavior?**

Get webpack 5 to generate output compatible with the Google Closure Compiler.

**How should this be implemented in your opinion?**

When `HarmonyImportDependencyParserPlugin.js` constructs `HarmonyImportSpecifierDependency` objects, it cannot know which parts of the member/call chain are the modules/imports and which parts are not.  One option is for it to track and store all possibilities and their expression ranges, then choose which to use later in the `FlagDependencyUsagePlugin` when webpack has knowledge about all imports and exports.

**Are you willing to work on this yourself?**

Yes.  I've created a failing-test PR (#17203) for now.


Hints:

Patch:
diff --git a/lib/dependencies/HarmonyEvaluatedImportSpecifierDependency.js b/lib/dependencies/HarmonyEvaluatedImportSpecifierDependency.js
--- a/lib/dependencies/HarmonyEvaluatedImportSpecifierDependency.js
+++ b/lib/dependencies/HarmonyEvaluatedImportSpecifierDependency.js
@@ -35,7 +35,7 @@ class HarmonyEvaluatedImportSpecifierDependency extends HarmonyImportSpecifierDe
 	 * @param {string} operator operator
 	 */
 	constructor(request, sourceOrder, ids, name, range, assertions, operator) {
-		super(request, sourceOrder, ids, name, range, false, assertions);
+		super(request, sourceOrder, ids, name, range, false, assertions, []);
 		this.operator = operator;
 	}
 
diff --git a/lib/dependencies/HarmonyImportDependencyParserPlugin.js b/lib/dependencies/HarmonyImportDependencyParserPlugin.js
--- a/lib/dependencies/HarmonyImportDependencyParserPlugin.js
+++ b/lib/dependencies/HarmonyImportDependencyParserPlugin.js
@@ -195,7 +195,8 @@ module.exports = class HarmonyImportDependencyParserPlugin {
 					settings.name,
 					expr.range,
 					exportPresenceMode,
-					settings.assertions
+					settings.assertions,
+					[]
 				);
 				dep.referencedPropertiesInDestructuring =
 					parser.destructuringAssignmentPropertiesFor(expr);
@@ -211,7 +212,7 @@ module.exports = class HarmonyImportDependencyParserPlugin {
 			.for(harmonySpecifierTag)
 			.tap(
 				"HarmonyImportDependencyParserPlugin",
-				(expression, members, membersOptionals) => {
+				(expression, members, membersOptionals, memberRangeStarts) => {
 					const settings = /** @type {HarmonySettings} */ (
 						parser.currentTagData
 					);
@@ -219,6 +220,11 @@ module.exports = class HarmonyImportDependencyParserPlugin {
 						members,
 						membersOptionals
 					);
+					const rangeStarts = memberRangeStarts.slice(
+						0,
+						memberRangeStarts.length -
+							(members.length - nonOptionalMembers.length)
+					);
 					const expr =
 						nonOptionalMembers !== members
 							? getNonOptionalMemberChain(
@@ -234,7 +240,8 @@ module.exports = class HarmonyImportDependencyParserPlugin {
 						settings.name,
 						expr.range,
 						exportPresenceMode,
-						settings.assertions
+						settings.assertions,
+						rangeStarts
 					);
 					dep.referencedPropertiesInDestructuring =
 						parser.destructuringAssignmentPropertiesFor(expr);
@@ -249,7 +256,7 @@ module.exports = class HarmonyImportDependencyParserPlugin {
 			.for(harmonySpecifierTag)
 			.tap(
 				"HarmonyImportDependencyParserPlugin",
-				(expression, members, membersOptionals) => {
+				(expression, members, membersOptionals, memberRangeStarts) => {
 					const { arguments: args, callee } = expression;
 					const settings = /** @type {HarmonySettings} */ (
 						parser.currentTagData
@@ -258,6 +265,11 @@ module.exports = class HarmonyImportDependencyParserPlugin {
 						members,
 						membersOptionals
 					);
+					const rangeStarts = memberRangeStarts.slice(
+						0,
+						memberRangeStarts.length -
+							(members.length - nonOptionalMembers.length)
+					);
 					const expr =
 						nonOptionalMembers !== members
 							? getNonOptionalMemberChain(
@@ -273,7 +285,8 @@ module.exports = class HarmonyImportDependencyParserPlugin {
 						settings.name,
 						expr.range,
 						exportPresenceMode,
-						settings.assertions
+						settings.assertions,
+						rangeStarts
 					);
 					dep.directImport = members.length === 0;
 					dep.call = true;
diff --git a/lib/dependencies/HarmonyImportSpecifierDependency.js b/lib/dependencies/HarmonyImportSpecifierDependency.js
--- a/lib/dependencies/HarmonyImportSpecifierDependency.js
+++ b/lib/dependencies/HarmonyImportSpecifierDependency.js
@@ -43,6 +43,7 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency {
 	 * @param {Range} range range
 	 * @param {TODO} exportPresenceMode export presence mode
 	 * @param {Assertions=} assertions assertions
+	 * @param {number[]=} idRangeStarts range starts for members of ids; the two arrays are right-aligned
 	 */
 	constructor(
 		request,
@@ -51,12 +52,14 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency {
 		name,
 		range,
 		exportPresenceMode,
-		assertions
+		assertions,
+		idRangeStarts // TODO webpack 6 make this non-optional. It must always be set to properly trim ids.
 	) {
 		super(request, sourceOrder, assertions);
 		this.ids = ids;
 		this.name = name;
 		this.range = range;
+		this.idRangeStarts = idRangeStarts;
 		this.exportPresenceMode = exportPresenceMode;
 		this.namespaceObjectAsContext = false;
 		this.call = undefined;
@@ -258,6 +261,7 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency {
 		write(this.ids);
 		write(this.name);
 		write(this.range);
+		write(this.idRangeStarts);
 		write(this.exportPresenceMode);
 		write(this.namespaceObjectAsContext);
 		write(this.call);
@@ -277,6 +281,7 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency {
 		this.ids = read();
 		this.name = read();
 		this.range = read();
+		this.idRangeStarts = read();
 		this.exportPresenceMode = read();
 		this.namespaceObjectAsContext = read();
 		this.call = read();
@@ -310,14 +315,78 @@ HarmonyImportSpecifierDependency.Template = class HarmonyImportSpecifierDependen
 		// Skip rendering depending when dependency is conditional
 		if (connection && !connection.isTargetActive(runtime)) return;
 
-		const ids = dep.getIds(moduleGraph);
-		const exportExpr = this._getCodeForIds(dep, source, templateContext, ids);
-		const range = dep.range;
+		const ids = dep.getIds(moduleGraph); // determine minimal set of IDs.
+		let trimmedIds = this._trimIdsToThoseImported(ids, moduleGraph, dep);
+
+		let [rangeStart, rangeEnd] = dep.range;
+		if (trimmedIds.length !== ids.length) {
+			// The array returned from dep.idRangeStarts is right-aligned with the array returned from dep.getIds.
+			// Meaning, the two arrays may not always have the same number of elements, but the last element of
+			// dep.idRangeStarts corresponds to [the starting range position of] the last element of dep.getIds.
+			// Use this to find the correct range end position based on the number of ids that were trimmed.
+			const idx =
+				dep.idRangeStarts === undefined
+					? -1 /* trigger failure case below */
+					: dep.idRangeStarts.length + (trimmedIds.length - ids.length);
+			if (idx < 0 || idx >= dep.idRangeStarts.length) {
+				// cspell:ignore minifiers
+				// Should not happen but we can't throw an error here because of backward compatibility with
+				// external plugins in wp5.  Instead, we just disable trimming for now.  This may break some minifiers.
+				trimmedIds = ids;
+				// TODO webpack 6 remove the "trimmedIds = ids" above and uncomment the following line instead.
+				// throw new Error("Missing range starts data for id replacement trimming.");
+			} else {
+				rangeEnd = dep.idRangeStarts[idx];
+			}
+		}
+
+		const exportExpr = this._getCodeForIds(
+			dep,
+			source,
+			templateContext,
+			trimmedIds
+		);
 		if (dep.shorthand) {
-			source.insert(range[1], `: ${exportExpr}`);
+			source.insert(rangeEnd, `: ${exportExpr}`);
 		} else {
-			source.replace(range[0], range[1] - 1, exportExpr);
+			source.replace(rangeStart, rangeEnd - 1, exportExpr);
+		}
+	}
+
+	/**
+	 * @summary Determine which IDs in the id chain are actually referring to namespaces or imports,
+	 * and which are deeper member accessors on the imported object.  Only the former should be re-rendered.
+	 * @param {string[]} ids ids
+	 * @param {ModuleGraph} moduleGraph moduleGraph
+	 * @param {HarmonyImportSpecifierDependency} dependency dependency
+	 * @returns {string[]} generated code
+	 */
+	_trimIdsToThoseImported(ids, moduleGraph, dependency) {
+		let trimmedIds = [];
+		const exportsInfo = moduleGraph.getExportsInfo(
+			moduleGraph.getModule(dependency)
+		);
+		let currentExportsInfo = /** @type {ExportsInfo=} */ exportsInfo;
+		for (let i = 0; i < ids.length; i++) {
+			if (i === 0 && ids[i] === "default") {
+				continue; // ExportInfo for the next level under default is still at the root ExportsInfo, so don't advance currentExportsInfo
+			}
+			const exportInfo = currentExportsInfo.getExportInfo(ids[i]);
+			if (exportInfo.provided === false) {
+				// json imports have nested ExportInfo for elements that things that are not actually exported, so check .provided
+				trimmedIds = ids.slice(0, i);
+				break;
+			}
+			const nestedInfo = exportInfo.getNestedExportsInfo();
+			if (!nestedInfo) {
+				// once all nested exports are traversed, the next item is the actual import so stop there
+				trimmedIds = ids.slice(0, i + 1);
+				break;
+			}
+			currentExportsInfo = nestedInfo;
 		}
+		// Never trim to nothing.  This can happen for invalid imports (e.g. import { notThere } from "./module", or import { anything } from "./missingModule")
+		return trimmedIds.length ? trimmedIds : ids;
 	}
 
 	/**
diff --git a/lib/javascript/BasicEvaluatedExpression.js b/lib/javascript/BasicEvaluatedExpression.js
--- a/lib/javascript/BasicEvaluatedExpression.js
+++ b/lib/javascript/BasicEvaluatedExpression.js
@@ -70,6 +70,8 @@ class BasicEvaluatedExpression {
 		this.getMembers = undefined;
 		/** @type {() => boolean[]} */
 		this.getMembersOptionals = undefined;
+		/** @type {() => number[]} */
+		this.getMemberRangeStarts = undefined;
 		/** @type {EsTreeNode} */
 		this.expression = undefined;
 	}
@@ -384,14 +386,22 @@ class BasicEvaluatedExpression {
 	 * @param {string | VariableInfoInterface} rootInfo root info
 	 * @param {() => string[]} getMembers members
 	 * @param {() => boolean[]=} getMembersOptionals optional members
+	 * @param {() => number[]=} getMemberRangeStarts range start of progressively increasing sub-expressions
 	 * @returns {this} this
 	 */
-	setIdentifier(identifier, rootInfo, getMembers, getMembersOptionals) {
+	setIdentifier(
+		identifier,
+		rootInfo,
+		getMembers,
+		getMembersOptionals,
+		getMemberRangeStarts
+	) {
 		this.type = TypeIdentifier;
 		this.identifier = identifier;
 		this.rootInfo = rootInfo;
 		this.getMembers = getMembers;
 		this.getMembersOptionals = getMembersOptionals;
+		this.getMemberRangeStarts = getMemberRangeStarts;
 		this.sideEffects = true;
 		return this;
 	}
diff --git a/lib/javascript/JavascriptParser.js b/lib/javascript/JavascriptParser.js
--- a/lib/javascript/JavascriptParser.js
+++ b/lib/javascript/JavascriptParser.js
@@ -91,7 +91,7 @@ const BasicEvaluatedExpression = require("./BasicEvaluatedExpression");
 /** @typedef {import("../Parser").ParserState} ParserState */
 /** @typedef {import("../Parser").PreparsedAst} PreparsedAst */
 /** @typedef {{declaredScope: ScopeInfo, freeName: string | true, tagInfo: TagInfo | undefined}} VariableInfoInterface */
-/** @typedef {{ name: string | VariableInfo, rootInfo: string | VariableInfo, getMembers: () => string[], getMembersOptionals: () => boolean[] }} GetInfoResult */
+/** @typedef {{ name: string | VariableInfo, rootInfo: string | VariableInfo, getMembers: () => string[], getMembersOptionals: () => boolean[], getMemberRangeStarts: () => number[] }} GetInfoResult */
 
 const EMPTY_ARRAY = [];
 const ALLOWED_MEMBER_TYPES_CALL_EXPRESSION = 0b01;
@@ -350,9 +350,15 @@ class JavascriptParser extends Parser {
 			/** @type {HookMap<SyncBailHook<[BaseCallExpression], boolean | void>>} */
 			call: new HookMap(() => new SyncBailHook(["expression"])),
 			/** Something like "a.b()" */
-			/** @type {HookMap<SyncBailHook<[CallExpression, string[], boolean[]], boolean | void>>} */
+			/** @type {HookMap<SyncBailHook<[CallExpression, string[], boolean[], number[]], boolean | void>>} */
 			callMemberChain: new HookMap(
-				() => new SyncBailHook(["expression", "members", "membersOptionals"])
+				() =>
+					new SyncBailHook([
+						"expression",
+						"members",
+						"membersOptionals",
+						"memberRangeStarts"
+					])
 			),
 			/** Something like "a.b().c.d" */
 			/** @type {HookMap<SyncBailHook<[Expression, string[], CallExpression, string[]], boolean | void>>} */
@@ -384,9 +390,15 @@ class JavascriptParser extends Parser {
 			binaryExpression: new SyncBailHook(["binaryExpression"]),
 			/** @type {HookMap<SyncBailHook<[Expression], boolean | void>>} */
 			expression: new HookMap(() => new SyncBailHook(["expression"])),
-			/** @type {HookMap<SyncBailHook<[Expression, string[], boolean[]], boolean | void>>} */
+			/** @type {HookMap<SyncBailHook<[Expression, string[], boolean[], number[]], boolean | void>>} */
 			expressionMemberChain: new HookMap(
-				() => new SyncBailHook(["expression", "members", "membersOptionals"])
+				() =>
+					new SyncBailHook([
+						"expression",
+						"members",
+						"membersOptionals",
+						"memberRangeStarts"
+					])
 			),
 			/** @type {HookMap<SyncBailHook<[Expression, string[]], boolean | void>>} */
 			unhandledExpressionMemberChain: new HookMap(
@@ -1150,7 +1162,8 @@ class JavascriptParser extends Parser {
 								info.name,
 								info.rootInfo,
 								info.getMembers,
-								info.getMembersOptionals
+								info.getMembersOptionals,
+								info.getMemberRangeStarts
 							)
 							.setRange(expr.range);
 					}
@@ -1170,7 +1183,8 @@ class JavascriptParser extends Parser {
 					name: info,
 					rootInfo: info,
 					getMembers: () => [],
-					getMembersOptionals: () => []
+					getMembersOptionals: () => [],
+					getMemberRangeStarts: () => []
 				};
 			}
 		});
@@ -1184,7 +1198,8 @@ class JavascriptParser extends Parser {
 					name: info,
 					rootInfo: info,
 					getMembers: () => [],
-					getMembersOptionals: () => []
+					getMembersOptionals: () => [],
+					getMemberRangeStarts: () => []
 				};
 			}
 		});
@@ -3248,7 +3263,8 @@ class JavascriptParser extends Parser {
 					callee.getMembers(),
 					callee.getMembersOptionals
 						? callee.getMembersOptionals()
-						: callee.getMembers().map(() => false)
+						: callee.getMembers().map(() => false),
+					callee.getMemberRangeStarts ? callee.getMemberRangeStarts() : []
 				);
 				if (result1 === true) return;
 				const result2 = this.callHooksForInfo(
@@ -3292,12 +3308,14 @@ class JavascriptParser extends Parser {
 					if (result1 === true) return;
 					const members = exprInfo.getMembers();
 					const membersOptionals = exprInfo.getMembersOptionals();
+					const memberRangeStarts = exprInfo.getMemberRangeStarts();
 					const result2 = this.callHooksForInfo(
 						this.hooks.expressionMemberChain,
 						exprInfo.rootInfo,
 						expression,
 						members,
-						membersOptionals
+						membersOptionals,
+						memberRangeStarts
 					);
 					if (result2 === true) return;
 					this.walkMemberExpressionWithExpressionName(
@@ -4253,20 +4271,23 @@ class JavascriptParser extends Parser {
 
 	/**
 	 * @param {MemberExpression} expression a member expression
-	 * @returns {{ members: string[], object: Expression | Super, membersOptionals: boolean[] }} member names (reverse order) and remaining object
+	 * @returns {{ members: string[], object: Expression | Super, membersOptionals: boolean[], memberRangeStarts: number[] }} member names (reverse order) and remaining object
 	 */
 	extractMemberExpressionChain(expression) {
 		/** @type {AnyNode} */
 		let expr = expression;
 		const members = [];
 		const membersOptionals = [];
+		const memberRangeStarts = [];
 		while (expr.type === "MemberExpression") {
 			if (expr.computed) {
 				if (expr.property.type !== "Literal") break;
 				members.push(`${expr.property.value}`);
+				memberRangeStarts.push(expr.object.range[1]);
 			} else {
 				if (expr.property.type !== "Identifier") break;
 				members.push(expr.property.name);
+				memberRangeStarts.push(expr.object.range[1]);
 			}
 			membersOptionals.push(expr.optional);
 			expr = expr.object;
@@ -4275,6 +4296,7 @@ class JavascriptParser extends Parser {
 		return {
 			members,
 			membersOptionals,
+			memberRangeStarts,
 			object: expr
 		};
 	}
@@ -4297,8 +4319,8 @@ class JavascriptParser extends Parser {
 		return { info, name };
 	}
 
-	/** @typedef {{ type: "call", call: CallExpression, calleeName: string, rootInfo: string | VariableInfo, getCalleeMembers: () => string[], name: string, getMembers: () => string[], getMembersOptionals: () => boolean[]}} CallExpressionInfo */
-	/** @typedef {{ type: "expression", rootInfo: string | VariableInfo, name: string, getMembers: () => string[], getMembersOptionals: () => boolean[]}} ExpressionExpressionInfo */
+	/** @typedef {{ type: "call", call: CallExpression, calleeName: string, rootInfo: string | VariableInfo, getCalleeMembers: () => string[], name: string, getMembers: () => string[], getMembersOptionals: () => boolean[], getMemberRangeStarts: () => number[]}} CallExpressionInfo */
+	/** @typedef {{ type: "expression", rootInfo: string | VariableInfo, name: string, getMembers: () => string[], getMembersOptionals: () => boolean[], getMemberRangeStarts: () => number[]}} ExpressionExpressionInfo */
 
 	/**
 	 * @param {MemberExpression} expression a member expression
@@ -4306,7 +4328,7 @@ class JavascriptParser extends Parser {
 	 * @returns {CallExpressionInfo | ExpressionExpressionInfo | undefined} expression info
 	 */
 	getMemberExpressionInfo(expression, allowedTypes) {
-		const { object, members, membersOptionals } =
+		const { object, members, membersOptionals, memberRangeStarts } =
 			this.extractMemberExpressionChain(expression);
 		switch (object.type) {
 			case "CallExpression": {
@@ -4332,7 +4354,8 @@ class JavascriptParser extends Parser {
 					getCalleeMembers: memoize(() => rootMembers.reverse()),
 					name: objectAndMembersToName(`${calleeName}()`, members),
 					getMembers: memoize(() => members.reverse()),
-					getMembersOptionals: memoize(() => membersOptionals.reverse())
+					getMembersOptionals: memoize(() => membersOptionals.reverse()),
+					getMemberRangeStarts: memoize(() => memberRangeStarts.reverse())
 				};
 			}
 			case "Identifier":
@@ -4351,7 +4374,8 @@ class JavascriptParser extends Parser {
 					name: objectAndMembersToName(resolvedRoot, members),
 					rootInfo,
 					getMembers: memoize(() => members.reverse()),
-					getMembersOptionals: memoize(() => membersOptionals.reverse())
+					getMembersOptionals: memoize(() => membersOptionals.reverse()),
+					getMemberRangeStarts: memoize(() => memberRangeStarts.reverse())
 				};
 			}
 		}
diff --git a/types.d.ts b/types.d.ts
--- a/types.d.ts
+++ b/types.d.ts
@@ -507,6 +507,7 @@ declare abstract class BasicEvaluatedExpression {
 	rootInfo: string | VariableInfoInterface;
 	getMembers: () => string[];
 	getMembersOptionals: () => boolean[];
+	getMemberRangeStarts: () => number[];
 	expression: NodeEstreeIndex;
 	isUnknown(): boolean;
 	isNull(): boolean;
@@ -591,7 +592,8 @@ declare abstract class BasicEvaluatedExpression {
 		identifier: string | VariableInfoInterface,
 		rootInfo: string | VariableInfoInterface,
 		getMembers: () => string[],
-		getMembersOptionals?: () => boolean[]
+		getMembersOptionals?: () => boolean[],
+		getMemberRangeStarts?: () => number[]
 	): BasicEvaluatedExpression;
 
 	/**
@@ -786,6 +788,7 @@ declare interface CallExpressionInfo {
 	name: string;
 	getMembers: () => string[];
 	getMembersOptionals: () => boolean[];
+	getMemberRangeStarts: () => number[];
 }
 declare interface CallbackAsyncQueue<T> {
 	(err?: null | WebpackError, result?: T): any;
@@ -3965,6 +3968,7 @@ declare interface ExpressionExpressionInfo {
 	name: string;
 	getMembers: () => string[];
 	getMembersOptionals: () => boolean[];
+	getMemberRangeStarts: () => number[];
 }
 declare interface ExtensionAliasOption {
 	alias: string | string[];
@@ -5312,7 +5316,10 @@ declare class JavascriptParser extends Parser {
 		topLevelAwait: SyncBailHook<[Expression], boolean | void>;
 		call: HookMap<SyncBailHook<[BaseCallExpression], boolean | void>>;
 		callMemberChain: HookMap<
-			SyncBailHook<[CallExpression, string[], boolean[]], boolean | void>
+			SyncBailHook<
+				[CallExpression, string[], boolean[], number[]],
+				boolean | void
+			>
 		>;
 		memberChainOfCallMemberChain: HookMap<
 			SyncBailHook<
@@ -5331,7 +5338,7 @@ declare class JavascriptParser extends Parser {
 		binaryExpression: SyncBailHook<[BinaryExpression], boolean | void>;
 		expression: HookMap<SyncBailHook<[Expression], boolean | void>>;
 		expressionMemberChain: HookMap<
-			SyncBailHook<[Expression, string[], boolean[]], boolean | void>
+			SyncBailHook<[Expression, string[], boolean[], number[]], boolean | void>
 		>;
 		unhandledExpressionMemberChain: HookMap<
 			SyncBailHook<[Expression, string[]], boolean | void>
@@ -5920,6 +5927,7 @@ declare class JavascriptParser extends Parser {
 			| YieldExpression
 			| Super;
 		membersOptionals: boolean[];
+		memberRangeStarts: number[];
 	};
 	getFreeInfoFromVariable(varName: string): {
 		name: string;


Test patch:
diff --git a/test/configCases/code-generation/re-export-namespace-concat/data.json b/test/configCases/code-generation/re-export-namespace-concat/data.json
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace-concat/data.json
@@ -0,0 +1,5 @@
+{
+  "nested": {
+    "object3": {}
+  }
+}
diff --git a/test/configCases/code-generation/re-export-namespace-concat/index.js b/test/configCases/code-generation/re-export-namespace-concat/index.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace-concat/index.js
@@ -0,0 +1,63 @@
+import { obj1 }  from './module1';
+import * as m_1 from './module1';
+import * as m_2 from './module2';
+import * as m_3 from './module3';
+import data from "./data";
+
+const { expectSourceToContain } = require("../../../helpers/expectSource");
+
+// It's important to preserve the same accessor syntax (quotes vs. dot notatation) after the actual export variable.
+// Else, minifiers such as Closure Compiler will not be able to minify correctly in ADVANCED mode.
+
+it("should use/preserve accessor form for import object and namespaces", function() {
+	var fs = require("fs");
+	var source = fs.readFileSync(__filename, "utf-8").toString();
+
+	// Reference the imports to generate uses in the source.
+
+	const f = false;
+	if (f) {
+		const x1 = m_1;
+		const x2 = obj1;
+
+		const z1 = obj1["plants"];
+		const z2 = obj1["funcs"]();
+		const z3 = m_1["obj1"]["pots"];
+		const z4 = m_1["obj1"]["subs"]();
+
+		const a = m_2["m_1"].obj1["flip"].flap;
+		const b = m_2["m_1"]["obj1"].zip["zap"];
+		const c = m_2.m_1.obj1["ding"].dong();
+		const d = m_2.m_1["obj1"].sing["song"]();
+
+		const aa = m_3["m_2"].m_1["obj1"]["zoom"];
+
+		const bb = obj1.up.down?.left.right;
+
+		data.nested.object3["unknownProperty"].depth = "deep";
+	}
+
+	/************ DO NOT MATCH BELOW THIS LINE ************/
+
+	// Imported objects and import namespaces should use dot notation.  Any references to the properties of exports
+	// should be preserved as either quotes or dot notation, depending on the original source.
+
+	expectSourceToContain(source, 'const x1 = module1_namespaceObject;');
+	expectSourceToContain(source, 'const x2 = obj1;');
+
+	expectSourceToContain(source, 'const z1 = obj1["plants"];');
+	expectSourceToContain(source, 'const z2 = obj1["funcs"]();');
+	expectSourceToContain(source, 'const z3 = obj1["pots"];');
+	expectSourceToContain(source, 'const z4 = obj1["subs"]();');
+
+	expectSourceToContain(source, 'const a = obj1["flip"].flap;');
+	expectSourceToContain(source, 'const b = obj1.zip["zap"];');
+	expectSourceToContain(source, 'const c = obj1["ding"].dong();');
+	expectSourceToContain(source, 'const d = obj1.sing["song"]();');
+
+	expectSourceToContain(source, 'const aa = obj1["zoom"];');
+
+	expectSourceToContain(source, 'const bb = obj1.up.down?.left.right;');
+
+	expectSourceToContain(source, 'data_namespaceObject.a.a["unknownProperty"].depth = "deep";');
+});
diff --git a/test/configCases/code-generation/re-export-namespace-concat/module1.js b/test/configCases/code-generation/re-export-namespace-concat/module1.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace-concat/module1.js
@@ -0,0 +1,3 @@
+export const obj1 = {};
+
+export default { obj2: {} };
diff --git a/test/configCases/code-generation/re-export-namespace-concat/module2.js b/test/configCases/code-generation/re-export-namespace-concat/module2.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace-concat/module2.js
@@ -0,0 +1,2 @@
+import * as m1 from './module1';
+export { m1 as m_1 };
diff --git a/test/configCases/code-generation/re-export-namespace-concat/module3.js b/test/configCases/code-generation/re-export-namespace-concat/module3.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace-concat/module3.js
@@ -0,0 +1,2 @@
+import * as m2 from './module2';
+export { m2 as m_2 };
diff --git a/test/configCases/code-generation/re-export-namespace-concat/test.filter.js b/test/configCases/code-generation/re-export-namespace-concat/test.filter.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace-concat/test.filter.js
@@ -0,0 +1,5 @@
+var supportsOptionalChaining = require("../../../helpers/supportsOptionalChaining");
+
+module.exports = function (config) {
+	return supportsOptionalChaining();
+};
diff --git a/test/configCases/code-generation/re-export-namespace-concat/webpack.config.js b/test/configCases/code-generation/re-export-namespace-concat/webpack.config.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace-concat/webpack.config.js
@@ -0,0 +1,11 @@
+/** @type {import("../../../../").Configuration} */
+module.exports = {
+	node: {
+		__dirname: false,
+		__filename: false
+	},
+	mode: "production",
+	optimization: {
+		mangleExports: "size"
+	}
+};
diff --git a/test/configCases/code-generation/re-export-namespace/data.json b/test/configCases/code-generation/re-export-namespace/data.json
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace/data.json
@@ -0,0 +1,5 @@
+{
+  "nested": {
+    "object3": {}
+  }
+}
diff --git a/test/configCases/code-generation/re-export-namespace/index.js b/test/configCases/code-generation/re-export-namespace/index.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace/index.js
@@ -0,0 +1,64 @@
+import { obj1 }  from './module1';
+import * as m_1 from './module1';
+import * as m_2 from './module2';
+import * as m_3 from './module3';
+import data from "./data";
+
+const { expectSourceToContain } = require("../../../helpers/expectSource");
+
+// It's important to preserve the same accessor syntax (quotes vs. dot notatation) after the actual export variable.
+// Else, minifiers such as Closure Compiler will not be able to minify correctly in ADVANCED mode.
+
+it("should use/preserve accessor form for import object and namespaces", function() {
+	var fs = require("fs");
+	var source = fs.readFileSync(__filename, "utf-8").toString();
+
+	// Reference the imports to generate uses in the source.
+
+	const f = false;
+	if (f) {
+		const x1 = m_1;
+		const x2 = obj1;
+
+		const z1 = obj1["plants"];
+		const z2 = obj1["funcs"]();
+		const z3 = m_1["obj1"]["pots"];
+		const z4 = m_1["obj1"]["subs"]();
+
+		const a = m_2["m_1"].obj1["flip"].flap;
+		const b = m_2["m_1"]["obj1"].zip["zap"];
+		const c = m_2.m_1.obj1["ding"].dong();
+		const d = m_2.m_1["obj1"].sing["song"]();
+
+		const aa = m_3["m_2"].m_1["obj1"]["zoom"];
+
+		const bb = obj1.up.down?.left.right;
+
+		data.nested.object3["unknownProperty"].depth = "deep";
+	}
+
+	/************ DO NOT MATCH BELOW THIS LINE ************/
+
+	// Imported objects and import namespaces should use dot notation.  Any references to the properties of exports
+	// should be preserved as either quotes or dot notation, depending on the original source.
+
+	expectSourceToContain(source, 'const x1 = _module1__WEBPACK_IMPORTED_MODULE_0__;');
+	expectSourceToContain(source, 'const x2 = _module1__WEBPACK_IMPORTED_MODULE_0__.obj1;');
+
+	expectSourceToContain(source, 'const z1 = _module1__WEBPACK_IMPORTED_MODULE_0__.obj1["plants"];');
+	expectSourceToContain(source, 'const z2 = _module1__WEBPACK_IMPORTED_MODULE_0__.obj1["funcs"]();');
+	expectSourceToContain(source, 'const z3 = _module1__WEBPACK_IMPORTED_MODULE_0__.obj1["pots"];');
+	expectSourceToContain(source, 'const z4 = _module1__WEBPACK_IMPORTED_MODULE_0__.obj1["subs"]();');
+
+	expectSourceToContain(source, 'const a = _module2__WEBPACK_IMPORTED_MODULE_1__.m_1.obj1["flip"].flap;');
+	expectSourceToContain(source, 'const b = _module2__WEBPACK_IMPORTED_MODULE_1__.m_1.obj1.zip["zap"];');
+	expectSourceToContain(source, 'const c = _module2__WEBPACK_IMPORTED_MODULE_1__.m_1.obj1["ding"].dong();');
+	expectSourceToContain(source, 'const d = _module2__WEBPACK_IMPORTED_MODULE_1__.m_1.obj1.sing["song"]();');
+
+	expectSourceToContain(source, 'const aa = _module3__WEBPACK_IMPORTED_MODULE_2__.m_2.m_1.obj1["zoom"];');
+
+	expectSourceToContain(source, 'const bb = _module1__WEBPACK_IMPORTED_MODULE_0__.obj1.up.down?.left.right;');
+
+	expectSourceToContain(source, '_data__WEBPACK_IMPORTED_MODULE_3__.nested.object3["unknownProperty"].depth = "deep";');
+
+});
diff --git a/test/configCases/code-generation/re-export-namespace/module1.js b/test/configCases/code-generation/re-export-namespace/module1.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace/module1.js
@@ -0,0 +1,3 @@
+export const obj1 = {};
+
+export default { obj2: {} };
diff --git a/test/configCases/code-generation/re-export-namespace/module2.js b/test/configCases/code-generation/re-export-namespace/module2.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace/module2.js
@@ -0,0 +1,2 @@
+import * as m1 from './module1';
+export { m1 as m_1 };
diff --git a/test/configCases/code-generation/re-export-namespace/module3.js b/test/configCases/code-generation/re-export-namespace/module3.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace/module3.js
@@ -0,0 +1,2 @@
+import * as m2 from './module2';
+export { m2 as m_2 };
diff --git a/test/configCases/code-generation/re-export-namespace/test.filter.js b/test/configCases/code-generation/re-export-namespace/test.filter.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace/test.filter.js
@@ -0,0 +1,5 @@
+var supportsOptionalChaining = require("../../../helpers/supportsOptionalChaining");
+
+module.exports = function (config) {
+	return supportsOptionalChaining();
+};
diff --git a/test/configCases/code-generation/re-export-namespace/webpack.config.js b/test/configCases/code-generation/re-export-namespace/webpack.config.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/re-export-namespace/webpack.config.js
@@ -0,0 +1,14 @@
+/** @type {import("../../../../").Configuration} */
+module.exports = {
+	node: {
+		__dirname: false,
+		__filename: false
+	},
+	optimization: {
+		concatenateModules: false,
+		usedExports: true,
+		providedExports: true,
+		minimize: false,
+		mangleExports: false
+	}
+};