Repository: webpack/webpack (JavaScript)

Issue:
CommonJS imports should preserve chained expressions
<!-- Please don't delete this template or we'll close your issue -->

## Feature request

Similar to Issue #17218, chained calls off of commonjs imports are completely rewritten by webpack.

e.g.,

```
const ww = require('./es6-module1').obj1["bing"].bang;
```

generates

```
const ww = (__webpack_require__(/*! ./es6-module1 */ 960).obj1.bing.bang);
```

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).

**What is the expected behavior?**

The expression after the imported object (obj1 in this case) should remain untouched. That is,

```
const ww = (__webpack_require__(/*! ./es6-module1 */ 960).obj1)["bing"].bang;
```

**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?**

Factor the code made in #17203 `HarmonyImportSpecifierDependency.js` into a shared location and call it from `CommonJsFullRequireDependency.js` as well.

**Are you willing to work on this yourself?**

Yes. See PR #17718.



Hints:

Patch:
diff --git a/lib/dependencies/CommonJsFullRequireDependency.js b/lib/dependencies/CommonJsFullRequireDependency.js
--- a/lib/dependencies/CommonJsFullRequireDependency.js
+++ b/lib/dependencies/CommonJsFullRequireDependency.js
@@ -7,6 +7,7 @@
 
 const Template = require("../Template");
 const { equals } = require("../util/ArrayHelpers");
+const { getTrimmedIdsAndRange } = require("../util/chainedImports");
 const makeSerializable = require("../util/makeSerializable");
 const propertyAccess = require("../util/propertyAccess");
 const ModuleDependency = require("./ModuleDependency");
@@ -26,11 +27,18 @@ class CommonJsFullRequireDependency extends ModuleDependency {
 	 * @param {string} request the request string
 	 * @param {Range} range location in source code
 	 * @param {string[]} names accessed properties on module
+	 * @param {Range[]=} idRanges ranges for members of ids; the two arrays are right-aligned
 	 */
-	constructor(request, range, names) {
+	constructor(
+		request,
+		range,
+		names,
+		idRanges /* TODO webpack 6 make this non-optional. It must always be set to properly trim ids. */
+	) {
 		super(request);
 		this.range = range;
 		this.names = names;
+		this.idRanges = idRanges;
 		this.call = false;
 		this.asiSafe = undefined;
 	}
@@ -60,6 +68,7 @@ class CommonJsFullRequireDependency extends ModuleDependency {
 	serialize(context) {
 		const { write } = context;
 		write(this.names);
+		write(this.idRanges);
 		write(this.call);
 		write(this.asiSafe);
 		super.serialize(context);
@@ -71,6 +80,7 @@ class CommonJsFullRequireDependency extends ModuleDependency {
 	deserialize(context) {
 		const { read } = context;
 		this.names = read();
+		this.idRanges = read();
 		this.call = read();
 		this.asiSafe = read();
 		super.deserialize(context);
@@ -117,15 +127,26 @@ CommonJsFullRequireDependency.Template = class CommonJsFullRequireDependencyTemp
 			weak: dep.weak,
 			runtimeRequirements
 		});
+
+		const {
+			trimmedRange: [trimmedRangeStart, trimmedRangeEnd],
+			trimmedIds
+		} = getTrimmedIdsAndRange(
+			dep.names,
+			dep.range,
+			dep.idRanges,
+			moduleGraph,
+			dep
+		);
+
 		if (importedModule) {
-			const ids = dep.names;
 			const usedImported = moduleGraph
 				.getExportsInfo(importedModule)
-				.getUsedName(ids, runtime);
+				.getUsedName(trimmedIds, runtime);
 			if (usedImported) {
-				const comment = equals(usedImported, ids)
+				const comment = equals(usedImported, trimmedIds)
 					? ""
-					: Template.toNormalComment(propertyAccess(ids)) + " ";
+					: Template.toNormalComment(propertyAccess(trimmedIds)) + " ";
 				const access = `${comment}${propertyAccess(usedImported)}`;
 				requireExpr =
 					dep.asiSafe === true
@@ -133,7 +154,7 @@ CommonJsFullRequireDependency.Template = class CommonJsFullRequireDependencyTemp
 						: `${requireExpr}${access}`;
 			}
 		}
-		source.replace(dep.range[0], dep.range[1] - 1, requireExpr);
+		source.replace(trimmedRangeStart, trimmedRangeEnd - 1, requireExpr);
 	}
 };
 
diff --git a/lib/dependencies/CommonJsImportsParserPlugin.js b/lib/dependencies/CommonJsImportsParserPlugin.js
--- a/lib/dependencies/CommonJsImportsParserPlugin.js
+++ b/lib/dependencies/CommonJsImportsParserPlugin.js
@@ -379,9 +379,16 @@ class CommonJsImportsParserPlugin {
 		 * @param {string[]} calleeMembers callee members
 		 * @param {CallExpression} callExpr call expression
 		 * @param {string[]} members members
+		 * @param {Range[]} memberRanges member ranges
 		 * @returns {boolean | void} true when handled
 		 */
-		const chainHandler = (expr, calleeMembers, callExpr, members) => {
+		const chainHandler = (
+			expr,
+			calleeMembers,
+			callExpr,
+			members,
+			memberRanges
+		) => {
 			if (callExpr.arguments.length !== 1) return;
 			const param = parser.evaluateExpression(callExpr.arguments[0]);
 			if (
@@ -391,7 +398,8 @@ class CommonJsImportsParserPlugin {
 				const dep = new CommonJsFullRequireDependency(
 					/** @type {string} */ (param.string),
 					/** @type {Range} */ (expr.range),
-					members
+					members,
+					/** @type {Range[]} */ memberRanges
 				);
 				dep.asiSafe = !parser.isAsiPosition(
 					/** @type {Range} */ (expr.range)[0]
@@ -407,9 +415,16 @@ class CommonJsImportsParserPlugin {
 		 * @param {string[]} calleeMembers callee members
 		 * @param {CallExpression} callExpr call expression
 		 * @param {string[]} members members
+		 * @param {Range[]} memberRanges member ranges
 		 * @returns {boolean | void} true when handled
 		 */
-		const callChainHandler = (expr, calleeMembers, callExpr, members) => {
+		const callChainHandler = (
+			expr,
+			calleeMembers,
+			callExpr,
+			members,
+			memberRanges
+		) => {
 			if (callExpr.arguments.length !== 1) return;
 			const param = parser.evaluateExpression(callExpr.arguments[0]);
 			if (
@@ -419,7 +434,8 @@ class CommonJsImportsParserPlugin {
 				const dep = new CommonJsFullRequireDependency(
 					/** @type {string} */ (param.string),
 					/** @type {Range} */ (expr.callee.range),
-					members
+					members,
+					/** @type {Range[]} */ memberRanges
 				);
 				dep.call = true;
 				dep.asiSafe = !parser.isAsiPosition(
diff --git a/lib/dependencies/HarmonyImportSpecifierDependency.js b/lib/dependencies/HarmonyImportSpecifierDependency.js
--- a/lib/dependencies/HarmonyImportSpecifierDependency.js
+++ b/lib/dependencies/HarmonyImportSpecifierDependency.js
@@ -9,6 +9,7 @@ const Dependency = require("../Dependency");
 const {
 	getDependencyUsedByExportsCondition
 } = require("../optimize/InnerGraph");
+const { getTrimmedIdsAndRange } = require("../util/chainedImports");
 const makeSerializable = require("../util/makeSerializable");
 const propertyAccess = require("../util/propertyAccess");
 const HarmonyImportDependency = require("./HarmonyImportDependency");
@@ -324,30 +325,16 @@ HarmonyImportSpecifierDependency.Template = class HarmonyImportSpecifierDependen
 		// Skip rendering depending when dependency is conditional
 		if (connection && !connection.isTargetActive(runtime)) return;
 
-		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.idRanges 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.idRanges corresponds to [the expression fragment to the left of] the last element of dep.getIds.
-			// Use this to find the correct replacement range based on the number of ids that were trimmed.
-			const idx =
-				dep.idRanges === undefined
-					? -1 /* trigger failure case below */
-					: dep.idRanges.length + (trimmedIds.length - ids.length);
-			if (idx < 0 || idx >= dep.idRanges.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 {
-				[rangeStart, rangeEnd] = dep.idRanges[idx];
-			}
-		}
+		const {
+			trimmedRange: [trimmedRangeStart, trimmedRangeEnd],
+			trimmedIds
+		} = getTrimmedIdsAndRange(
+			dep.getIds(moduleGraph),
+			dep.range,
+			dep.idRanges,
+			moduleGraph,
+			dep
+		);
 
 		const exportExpr = this._getCodeForIds(
 			dep,
@@ -356,47 +343,10 @@ HarmonyImportSpecifierDependency.Template = class HarmonyImportSpecifierDependen
 			trimmedIds
 		);
 		if (dep.shorthand) {
-			source.insert(rangeEnd, `: ${exportExpr}`);
+			source.insert(trimmedRangeEnd, `: ${exportExpr}`);
 		} else {
-			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) {
-		/** @type {string[]} */
-		let trimmedIds = [];
-		const exportsInfo = moduleGraph.getExportsInfo(
-			/** @type {Module} */ (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;
+			source.replace(trimmedRangeStart, trimmedRangeEnd - 1, exportExpr);
 		}
-		// 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/JavascriptParser.js b/lib/javascript/JavascriptParser.js
--- a/lib/javascript/JavascriptParser.js
+++ b/lib/javascript/JavascriptParser.js
@@ -363,25 +363,27 @@ class JavascriptParser extends Parser {
 					])
 			),
 			/** Something like "a.b().c.d" */
-			/** @type {HookMap<SyncBailHook<[Expression, string[], CallExpression, string[]], boolean | void>>} */
+			/** @type {HookMap<SyncBailHook<[Expression, string[], CallExpression, string[], Range[]], boolean | void>>} */
 			memberChainOfCallMemberChain: new HookMap(
 				() =>
 					new SyncBailHook([
 						"expression",
 						"calleeMembers",
 						"callExpression",
-						"members"
+						"members",
+						"memberRanges"
 					])
 			),
 			/** Something like "a.b().c.d()"" */
-			/** @type {HookMap<SyncBailHook<[CallExpression, string[], CallExpression, string[]], boolean | void>>} */
+			/** @type {HookMap<SyncBailHook<[CallExpression, string[], CallExpression, string[], Range[]], boolean | void>>} */
 			callMemberChainOfCallMemberChain: new HookMap(
 				() =>
 					new SyncBailHook([
 						"expression",
 						"calleeMembers",
 						"innerCallExpression",
-						"members"
+						"members",
+						"memberRanges"
 					])
 			),
 			/** @type {SyncBailHook<[ChainExpression], boolean | void>} */
@@ -3274,7 +3276,8 @@ class JavascriptParser extends Parser {
 						expression,
 						exprInfo.getCalleeMembers(),
 						exprInfo.call,
-						exprInfo.getMembers()
+						exprInfo.getMembers(),
+						exprInfo.getMemberRanges()
 					);
 					if (result === true) return;
 				}
@@ -3365,7 +3368,8 @@ class JavascriptParser extends Parser {
 						expression,
 						exprInfo.getCalleeMembers(),
 						exprInfo.call,
-						exprInfo.getMembers()
+						exprInfo.getMembers(),
+						exprInfo.getMemberRanges()
 					);
 					if (result === true) return;
 					// Fast skip over the member chain as we already called memberChainOfCallMemberChain
diff --git a/lib/util/chainedImports.js b/lib/util/chainedImports.js
new file mode 100644
--- /dev/null
+++ b/lib/util/chainedImports.js
@@ -0,0 +1,96 @@
+/*
+	MIT License http://www.opensource.org/licenses/mit-license.php
+	Author Tobias Koppers @sokra
+*/
+
+"use strict";
+
+/** @typedef {import("../Dependency")} Dependency */
+/** @typedef {import("../ModuleGraph")} ModuleGraph */
+/** @typedef {import("../javascript/JavascriptParser").Range} Range */
+
+/**
+ * @summary Get the subset of ids and their corresponding range in an id chain that should be re-rendered by webpack.
+ * Only those in the chain that are actually referring to namespaces or imports should be re-rendered.
+ * Deeper member accessors on the imported object should not be re-rendered.  If deeper member accessors are re-rendered,
+ * there is a potential loss of meaning with rendering a quoted accessor as an unquoted accessor, or vice versa,
+ * because minifiers treat quoted accessors differently.  e.g. import { a } from "./module"; a["b"] vs a.b
+ * @param {string[]} untrimmedIds chained ids
+ * @param {Range} untrimmedRange range encompassing allIds
+ * @param {Range[]} ranges cumulative range of ids for each of allIds
+ * @param {ModuleGraph} moduleGraph moduleGraph
+ * @param {Dependency} dependency dependency
+ * @returns {{trimmedIds: string[], trimmedRange: Range}} computed trimmed ids and cumulative range of those ids
+ */
+exports.getTrimmedIdsAndRange = (
+	untrimmedIds,
+	untrimmedRange,
+	ranges,
+	moduleGraph,
+	dependency
+) => {
+	let trimmedIds = trimIdsToThoseImported(
+		untrimmedIds,
+		moduleGraph,
+		dependency
+	);
+	let trimmedRange = untrimmedRange;
+	if (trimmedIds.length !== untrimmedIds.length) {
+		// The array returned from dep.idRanges is right-aligned with the array returned from dep.names.
+		// Meaning, the two arrays may not always have the same number of elements, but the last element of
+		// dep.idRanges corresponds to [the expression fragment to the left of] the last element of dep.names.
+		// Use this to find the correct replacement range based on the number of ids that were trimmed.
+		const idx =
+			ranges === undefined
+				? -1 /* trigger failure case below */
+				: ranges.length + (trimmedIds.length - untrimmedIds.length);
+		if (idx < 0 || idx >= ranges.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 = untrimmedIds;
+			// 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 {
+			trimmedRange = ranges[idx];
+		}
+	}
+
+	return { trimmedIds, trimmedRange };
+};
+
+/**
+ * @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.
+ * @param {string[]} ids untrimmed ids
+ * @param {ModuleGraph} moduleGraph moduleGraph
+ * @param {Dependency} dependency dependency
+ * @returns {string[]} trimmed ids
+ */
+function 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/types.d.ts b/types.d.ts
--- a/types.d.ts
+++ b/types.d.ts
@@ -5521,13 +5521,19 @@ declare class JavascriptParser extends Parser {
 		>;
 		memberChainOfCallMemberChain: HookMap<
 			SyncBailHook<
-				[Expression, string[], CallExpression, string[]],
+				[Expression, string[], CallExpression, string[], [number, number][]],
 				boolean | void
 			>
 		>;
 		callMemberChainOfCallMemberChain: HookMap<
 			SyncBailHook<
-				[CallExpression, string[], CallExpression, string[]],
+				[
+					CallExpression,
+					string[],
+					CallExpression,
+					string[],
+					[number, number][]
+				],
 				boolean | void
 			>
 		>;


Test patch:
diff --git a/test/configCases/code-generation/re-export-namespace-concat/index.js b/test/configCases/code-generation/re-export-namespace-concat/index.js
--- a/test/configCases/code-generation/re-export-namespace-concat/index.js
+++ b/test/configCases/code-generation/re-export-namespace-concat/index.js
@@ -34,6 +34,10 @@ it("should use/preserve accessor form for import object and namespaces", functio
 
 		const bb = obj1.up.down?.left.right;
 
+		const ww = require('./module1').obj1["bing"]?.bang;
+		const xx = require('./module1').obj1["pip"].pop();
+		const yy = require('./module3')["m_2"]["m_1"]["obj1"]["tip"].top();
+
 		data.nested.object3["unknownProperty"].depth = "deep";
 
 		(obj1)["aaa"].bbb;
@@ -48,28 +52,32 @@ it("should use/preserve accessor form for import object and namespaces", functio
 	// 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 x1 = module1;');
+	expectSourceToContain(source, 'const x2 = module1.obj1;');
+
+	expectSourceToContain(source, 'const z1 = module1.obj1["plants"];');
+	expectSourceToContain(source, 'const z2 = module1.obj1["funcs"]();');
+	expectSourceToContain(source, 'const z3 = module1.obj1["pots"];');
+	expectSourceToContain(source, 'const z4 = module1.obj1["subs"]();');
 
-	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 = module2/* m_1.obj1 */.a.obj1["flip"].flap;');
+	expectSourceToContain(source, 'const b = module2/* m_1.obj1 */.a.obj1.zip["zap"];');
+	expectSourceToContain(source, 'const c = module2/* m_1.obj1 */.a.obj1["ding"].dong();');
+	expectSourceToContain(source, 'const d = module2/* m_1.obj1 */.a.obj1.sing["song"]();');
 
-	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 = module3/* m_2.m_1.obj1 */.a.a.obj1["zoom"];');
 
-	expectSourceToContain(source, 'const aa = obj1["zoom"];');
+	expectSourceToContain(source, 'const bb = module1.obj1.up.down?.left.right;');
 
-	expectSourceToContain(source, 'const bb = obj1.up.down?.left.right;');
+	expectSourceToContain(source, 'const ww = (__webpack_require__(/*! ./module1 */ 960).obj1)["bing"]?.bang;');
+	expectSourceToContain(source, 'const xx = (__webpack_require__(/*! ./module1 */ 960).obj1)["pip"].pop();');
+	expectSourceToContain(source, 'const yy = (__webpack_require__(/*! ./module3 */ 834)/* .m_2.m_1.obj1 */ .a.a.obj1)["tip"].top();');
 
 	expectSourceToContain(source, 'data_namespaceObject.a.a["unknownProperty"].depth = "deep";');
 
-	expectSourceToContain(source, '(obj1)["aaa"].bbb;');
-	expectSourceToContain(source, '(obj1)["ccc"].ddd;');
-	expectSourceToContain(source, '(obj1["eee"]).fff;');
-	expectSourceToContain(source, '(obj1["ggg"]).hhh;');
-	expectSourceToContain(source, '((obj1)["iii"]).jjj;');
+	expectSourceToContain(source, '(module1.obj1)["aaa"].bbb;');
+	expectSourceToContain(source, '(module1.obj1)["ccc"].ddd;');
+	expectSourceToContain(source, '(module1.obj1["eee"]).fff;');
+	expectSourceToContain(source, '(module1.obj1["ggg"]).hhh;');
+	expectSourceToContain(source, '((module1.obj1)["iii"]).jjj;');
 });
diff --git a/test/configCases/code-generation/re-export-namespace/index.js b/test/configCases/code-generation/re-export-namespace/index.js
--- a/test/configCases/code-generation/re-export-namespace/index.js
+++ b/test/configCases/code-generation/re-export-namespace/index.js
@@ -34,6 +34,10 @@ it("should use/preserve accessor form for import object and namespaces", functio
 
 		const bb = obj1.up.down?.left.right;
 
+		const ww = require('./module1').obj1["bing"]?.bang;
+		const xx = require('./module1').obj1["pip"].pop();
+		const yy = require('./module3')["m_2"]["m_1"]["obj1"]["tip"].top();
+
 		data.nested.object3["unknownProperty"].depth = "deep";
 
 		(obj1)["aaa"].bbb;
@@ -65,6 +69,10 @@ it("should use/preserve accessor form for import object and namespaces", functio
 
 	expectSourceToContain(source, 'const bb = _module1__WEBPACK_IMPORTED_MODULE_0__.obj1.up.down?.left.right;');
 
+	expectSourceToContain(source, 'const ww = (__webpack_require__(/*! ./module1 */ 960).obj1)["bing"]?.bang;');
+	expectSourceToContain(source, 'const xx = (__webpack_require__(/*! ./module1 */ 960).obj1)["pip"].pop();');
+	expectSourceToContain(source, 'const yy = (__webpack_require__(/*! ./module3 */ 834).m_2.m_1.obj1)["tip"].top();');
+
 	expectSourceToContain(source, '_data__WEBPACK_IMPORTED_MODULE_3__.nested.object3["unknownProperty"].depth = "deep";');
 
 	expectSourceToContain(source, '(_module1__WEBPACK_IMPORTED_MODULE_0__.obj1)["aaa"].bbb;');