Merge pull request #11395 from webpack/refactor/asi

refactor how asi handled
This commit is contained in:
Tobias Koppers 2020-09-01 15:47:41 +02:00 committed by GitHub
commit bdeea6ec2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 129 additions and 49 deletions

View File

@ -15,6 +15,7 @@ const {
} = require("./javascript/JavascriptParserHelpers"); } = require("./javascript/JavascriptParserHelpers");
/** @typedef {import("./Compiler")} Compiler */ /** @typedef {import("./Compiler")} Compiler */
/** @typedef {import("estree").Expression} Expression */
/** @typedef {import("./javascript/JavascriptParser")} JavascriptParser */ /** @typedef {import("./javascript/JavascriptParser")} JavascriptParser */
/** @typedef {null|undefined|RegExp|Function|string|number|boolean|bigint|undefined} CodeValuePrimitive */ /** @typedef {null|undefined|RegExp|Function|string|number|boolean|bigint|undefined} CodeValuePrimitive */
/** @typedef {RecursiveArrayOrRecord<CodeValuePrimitive|RuntimeValue>} CodeValue */ /** @typedef {RecursiveArrayOrRecord<CodeValuePrimitive|RuntimeValue>} CodeValue */
@ -39,25 +40,41 @@ class RuntimeValue {
} }
} }
const stringifyObj = (obj, parser, ecmaVersion) => { /**
if (Array.isArray(obj)) { * @param {any[]|{[k: string]: any}} obj obj
return ( * @param {JavascriptParser} parser Parser
"Object([" + * @param {number} ecmaVersion EcmaScript version
obj.map(code => toCode(code, parser, ecmaVersion)).join(",") + * @param {boolean|undefined|null=} asiSafe asi safe (undefined: unknown, null: unneeded)
"])" * @returns {string} code converted to string that evaluates
); */
} const stringifyObj = (obj, parser, ecmaVersion, asiSafe) => {
let code;
return ( let arr = Array.isArray(obj);
"Object({" + if (arr) {
Object.keys(obj) code = `[${obj
.map(code => toCode(code, parser, ecmaVersion, null))
.join(",")}]`;
} else {
code = `{${Object.keys(obj)
.map(key => { .map(key => {
const code = obj[key]; const code = obj[key];
return JSON.stringify(key) + ":" + toCode(code, parser, ecmaVersion); return (
JSON.stringify(key) + ":" + toCode(code, parser, ecmaVersion, null)
);
}) })
.join(",") + .join(",")}}`;
"})" }
);
switch (asiSafe) {
case null:
return code;
case true:
return arr ? code : `(${code})`;
case false:
return arr ? `;${code}` : `;(${code})`;
default:
return `Object(${code})`;
}
}; };
/** /**
@ -65,9 +82,10 @@ const stringifyObj = (obj, parser, ecmaVersion) => {
* @param {CodeValue} code Code to evaluate * @param {CodeValue} code Code to evaluate
* @param {JavascriptParser} parser Parser * @param {JavascriptParser} parser Parser
* @param {number} ecmaVersion EcmaScript version * @param {number} ecmaVersion EcmaScript version
* @param {boolean|undefined|null=} asiSafe asi safe (undefined: unknown, null: unneeded)
* @returns {string} code converted to string that evaluates * @returns {string} code converted to string that evaluates
*/ */
const toCode = (code, parser, ecmaVersion) => { const toCode = (code, parser, ecmaVersion, asiSafe) => {
if (code === null) { if (code === null) {
return "null"; return "null";
} }
@ -78,7 +96,7 @@ const toCode = (code, parser, ecmaVersion) => {
return "-0"; return "-0";
} }
if (code instanceof RuntimeValue) { if (code instanceof RuntimeValue) {
return toCode(code.exec(parser), parser, ecmaVersion); return toCode(code.exec(parser), parser, ecmaVersion, asiSafe);
} }
if (code instanceof RegExp && code.toString) { if (code instanceof RegExp && code.toString) {
return code.toString(); return code.toString();
@ -87,7 +105,7 @@ const toCode = (code, parser, ecmaVersion) => {
return "(" + code.toString() + ")"; return "(" + code.toString() + ")";
} }
if (typeof code === "object") { if (typeof code === "object") {
return stringifyObj(code, parser, ecmaVersion); return stringifyObj(code, parser, ecmaVersion, asiSafe);
} }
if (typeof code === "bigint") { if (typeof code === "bigint") {
return ecmaVersion >= 11 ? `${code}n` : `BigInt("${code}")`; return ecmaVersion >= 11 ? `${code}n` : `BigInt("${code}")`;
@ -195,14 +213,19 @@ class DefinePlugin {
if (recurse) return; if (recurse) return;
recurse = true; recurse = true;
const res = parser.evaluate( const res = parser.evaluate(
toCode(code, parser, ecmaVersion) toCode(code, parser, ecmaVersion, null)
); );
recurse = false; recurse = false;
res.setRange(expr.range); res.setRange(expr.range);
return res; return res;
}); });
parser.hooks.expression.for(key).tap("DefinePlugin", expr => { parser.hooks.expression.for(key).tap("DefinePlugin", expr => {
const strCode = toCode(code, parser, ecmaVersion); const strCode = toCode(
code,
parser,
ecmaVersion,
!parser.isAsiPosition(expr.range[0])
);
if (/__webpack_require__\s*(!?\.)/.test(strCode)) { if (/__webpack_require__\s*(!?\.)/.test(strCode)) {
return toConstantDependency(parser, strCode, [ return toConstantDependency(parser, strCode, [
RuntimeGlobals.require RuntimeGlobals.require
@ -228,8 +251,8 @@ class DefinePlugin {
if (recurseTypeof) return; if (recurseTypeof) return;
recurseTypeof = true; recurseTypeof = true;
const typeofCode = isTypeof const typeofCode = isTypeof
? toCode(code, parser, ecmaVersion) ? toCode(code, parser, ecmaVersion, null)
: "typeof (" + toCode(code, parser, ecmaVersion) + ")"; : "typeof (" + toCode(code, parser, ecmaVersion, null) + ")";
const res = parser.evaluate(typeofCode); const res = parser.evaluate(typeofCode);
recurseTypeof = false; recurseTypeof = false;
res.setRange(expr.range); res.setRange(expr.range);
@ -237,8 +260,8 @@ class DefinePlugin {
}); });
parser.hooks.typeof.for(key).tap("DefinePlugin", expr => { parser.hooks.typeof.for(key).tap("DefinePlugin", expr => {
const typeofCode = isTypeof const typeofCode = isTypeof
? toCode(code, parser, ecmaVersion) ? toCode(code, parser, ecmaVersion, null)
: "typeof (" + toCode(code, parser, ecmaVersion) + ")"; : "typeof (" + toCode(code, parser, ecmaVersion, null) + ")";
const res = parser.evaluate(typeofCode); const res = parser.evaluate(typeofCode);
if (!res.isString()) return; if (!res.isString()) return;
return toConstantDependency( return toConstantDependency(
@ -268,7 +291,12 @@ class DefinePlugin {
.for(key) .for(key)
.tap("DefinePlugin", evaluateToString("object")); .tap("DefinePlugin", evaluateToString("object"));
parser.hooks.expression.for(key).tap("DefinePlugin", expr => { parser.hooks.expression.for(key).tap("DefinePlugin", expr => {
const strCode = stringifyObj(obj, parser, ecmaVersion); const strCode = stringifyObj(
obj,
parser,
ecmaVersion,
!parser.isAsiPosition(expr.range[0])
);
if (/__webpack_require__\s*(!?\.)/.test(strCode)) { if (/__webpack_require__\s*(!?\.)/.test(strCode)) {
return toConstantDependency(parser, strCode, [ return toConstantDependency(parser, strCode, [

View File

@ -625,7 +625,7 @@ class RuntimeTemplate {
* @param {string} options.request the request * @param {string} options.request the request
* @param {string | string[]} options.exportName the export name * @param {string | string[]} options.exportName the export name
* @param {Module} options.originModule the origin module * @param {Module} options.originModule the origin module
* @param {boolean} options.asiSafe true, if location is safe for ASI, a bracket can be emitted * @param {boolean|undefined} options.asiSafe true, if location is safe for ASI, a bracket can be emitted
* @param {boolean} options.isCall true, if expression will be called * @param {boolean} options.isCall true, if expression will be called
* @param {boolean} options.callContext when false, call context will not be preserved * @param {boolean} options.callContext when false, call context will not be preserved
* @param {boolean} options.defaultInterop when true and accessing the default exports, interop code will be generated * @param {boolean} options.defaultInterop when true and accessing the default exports, interop code will be generated
@ -669,10 +669,12 @@ class RuntimeTemplate {
case "dynamic": case "dynamic":
if (isCall) { if (isCall) {
return `${importVar}_default()${propertyAccess(exportName, 1)}`; return `${importVar}_default()${propertyAccess(exportName, 1)}`;
} else if (asiSafe) {
return `(${importVar}_default()${propertyAccess(exportName, 1)})`;
} else { } else {
return `${importVar}_default.a${propertyAccess(exportName, 1)}`; return asiSafe
? `(${importVar}_default()${propertyAccess(exportName, 1)})`
: asiSafe === false
? `;(${importVar}_default()${propertyAccess(exportName, 1)})`
: `${importVar}_default.a${propertyAccess(exportName, 1)}`;
} }
case "default-only": case "default-only":
case "default-with-named": case "default-with-named":
@ -700,7 +702,7 @@ class RuntimeTemplate {
) )
); );
return `/*#__PURE__*/ ${ return `/*#__PURE__*/ ${
asiSafe ? "" : "Object" asiSafe ? "" : asiSafe === false ? ";" : "Object"
}(${importVar}_namespace_cache || (${importVar}_namespace_cache = ${ }(${importVar}_namespace_cache || (${importVar}_namespace_cache = ${
RuntimeGlobals.createFakeNamespaceObject RuntimeGlobals.createFakeNamespaceObject
}(${importVar}${exportsType === "default-only" ? "" : ", 2"})))`; }(${importVar}${exportsType === "default-only" ? "" : ", 2"})))`;
@ -721,11 +723,11 @@ class RuntimeTemplate {
: Template.toNormalComment(propertyAccess(exportName)) + " "; : Template.toNormalComment(propertyAccess(exportName)) + " ";
const access = `${importVar}${comment}${propertyAccess(used)}`; const access = `${importVar}${comment}${propertyAccess(used)}`;
if (isCall && callContext === false) { if (isCall && callContext === false) {
if (asiSafe) { return asiSafe
return `(0,${access})`; ? `(0,${access})`
} else { : asiSafe === false
return `Object(${access})`; ? `;(0,${access})`
} : `Object(${access})`;
} }
return access; return access;
} else { } else {

View File

@ -37,7 +37,7 @@ class CommonJsExportRequireDependency extends ModuleDependency {
this.names = names; this.names = names;
this.ids = ids; this.ids = ids;
this.resultUsed = resultUsed; this.resultUsed = resultUsed;
this.asiSafe = false; this.asiSafe = undefined;
} }
get type() { get type() {
@ -260,6 +260,7 @@ class CommonJsExportRequireDependency extends ModuleDependency {
serialize(context) { serialize(context) {
const { write } = context; const { write } = context;
write(this.asiSafe);
write(this.range); write(this.range);
write(this.valueRange); write(this.valueRange);
write(this.base); write(this.base);
@ -271,6 +272,7 @@ class CommonJsExportRequireDependency extends ModuleDependency {
deserialize(context) { deserialize(context) {
const { read } = context; const { read } = context;
this.asiSafe = read();
this.range = read(); this.range = read();
this.valueRange = read(); this.valueRange = read();
this.base = read(); this.base = read();

View File

@ -29,7 +29,7 @@ class CommonJsFullRequireDependency extends ModuleDependency {
this.range = range; this.range = range;
this.names = names; this.names = names;
this.call = false; this.call = false;
this.asiSafe = false; this.asiSafe = undefined;
} }
/** /**

View File

@ -36,7 +36,7 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency {
this.call = undefined; this.call = undefined;
this.directImport = undefined; this.directImport = undefined;
this.shorthand = undefined; this.shorthand = undefined;
this.asiSafe = false; this.asiSafe = undefined;
/** @type {Set<string> | boolean} */ /** @type {Set<string> | boolean} */
this.usedByExports = undefined; this.usedByExports = undefined;
} }
@ -272,7 +272,7 @@ HarmonyImportSpecifierDependency.Template = class HarmonyImportSpecifierDependen
request: dep.request, request: dep.request,
exportName: ids, exportName: ids,
originModule: module, originModule: module,
asiSafe: dep.asiSafe || dep.shorthand, asiSafe: dep.shorthand ? true : dep.asiSafe,
isCall: dep.call, isCall: dep.call,
callContext: !dep.directImport, callContext: !dep.directImport,
defaultInterop: true, defaultInterop: true,

View File

@ -69,7 +69,10 @@ class ImportMetaPlugin {
metaProperty.loc metaProperty.loc
) )
); );
const dep = new ConstDependency("Object()", metaProperty.range); const dep = new ConstDependency(
`${parser.isAsiPosition(metaProperty.range[0]) ? ";" : ""}({})`,
metaProperty.range
);
dep.loc = metaProperty.loc; dep.loc = metaProperty.loc;
parser.state.module.addPresentationalDependency(dep); parser.state.module.addPresentationalDependency(dep);
return true; return true;

View File

@ -3256,6 +3256,10 @@ class JavascriptParser extends Parser {
); );
} }
/**
* @param {number} pos source code position
* @returns {boolean} true when a semicolon has been inserted before this position, false if not
*/
isAsiPosition(pos) { isAsiPosition(pos) {
if (this.prevStatement === undefined) return false; if (this.prevStatement === undefined) return false;
const currentStatement = this.statementPath[this.statementPath.length - 1]; const currentStatement = this.statementPath[this.statementPath.length - 1];

View File

@ -10,6 +10,7 @@ const ConstDependency = require("../dependencies/ConstDependency");
const BasicEvaluatedExpression = require("./BasicEvaluatedExpression"); const BasicEvaluatedExpression = require("./BasicEvaluatedExpression");
/** @typedef {import("estree").Expression} ExpressionNode */ /** @typedef {import("estree").Expression} ExpressionNode */
/** @typedef {import("estree").Node} Node */
/** @typedef {import("./JavascriptParser")} JavascriptParser */ /** @typedef {import("./JavascriptParser")} JavascriptParser */
/** /**

View File

@ -241,7 +241,7 @@ const ensureNsObjSource = (
* @param {boolean} asCall asCall * @param {boolean} asCall asCall
* @param {boolean} callContext callContext * @param {boolean} callContext callContext
* @param {boolean} strictHarmonyModule strictHarmonyModule * @param {boolean} strictHarmonyModule strictHarmonyModule
* @param {boolean} asiSafe asiSafe * @param {boolean | undefined} asiSafe asiSafe
* @returns {string} expression to get value of external module * @returns {string} expression to get value of external module
*/ */
const getExternalImport = ( const getExternalImport = (
@ -299,6 +299,8 @@ const getExternalImport = (
? `${info.interopDefaultAccessName}()` ? `${info.interopDefaultAccessName}()`
: asiSafe : asiSafe
? `(${info.interopDefaultAccessName}())` ? `(${info.interopDefaultAccessName}())`
: asiSafe === false
? `;(${info.interopDefaultAccessName}())`
: `${info.interopDefaultAccessName}.a`; : `${info.interopDefaultAccessName}.a`;
exportName = exportName.slice(1); exportName = exportName.slice(1);
} }
@ -316,7 +318,11 @@ const getExternalImport = (
: Template.toNormalComment(`${exportName.join(".")}`); : Template.toNormalComment(`${exportName.join(".")}`);
const reference = `${exprStart}${comment}${propertyAccess(used)}`; const reference = `${exprStart}${comment}${propertyAccess(used)}`;
if (asCall && callContext === false) { if (asCall && callContext === false) {
return asiSafe ? `(0,${reference})` : `Object(${reference})`; return asiSafe
? `(0,${reference})`
: asiSafe === false
? `;(0,${reference})`
: `Object(${reference})`;
} }
return reference; return reference;
}; };
@ -407,7 +413,7 @@ const getFinalBinding = (
* @param {boolean} asCall asCall * @param {boolean} asCall asCall
* @param {boolean} callContext callContext * @param {boolean} callContext callContext
* @param {boolean} strictHarmonyModule strictHarmonyModule * @param {boolean} strictHarmonyModule strictHarmonyModule
* @param {boolean} asiSafe asiSafe * @param {boolean | undefined} asiSafe asiSafe
* @returns {string} the final name * @returns {string} the final name
*/ */
const getFinalName = ( const getFinalName = (
@ -581,14 +587,18 @@ const createModuleReference = ({
const callFlag = call ? "_call" : ""; const callFlag = call ? "_call" : "";
const directImportFlag = directImport ? "_directImport" : ""; const directImportFlag = directImport ? "_directImport" : "";
const strictFlag = strict ? "_strict" : ""; const strictFlag = strict ? "_strict" : "";
const asiSafeFlag = asiSafe ? "_asiSafe" : ""; const asiSafeFlag = asiSafe
? "_asiSafe1"
: asiSafe === false
? "_asiSafe0"
: "";
const exportData = ids const exportData = ids
? Buffer.from(JSON.stringify(ids), "utf-8").toString("hex") ? Buffer.from(JSON.stringify(ids), "utf-8").toString("hex")
: "ns"; : "ns";
return `__WEBPACK_MODULE_REFERENCE__${info.index}_${exportData}${callFlag}${directImportFlag}${strictFlag}${asiSafeFlag}__`; return `__WEBPACK_MODULE_REFERENCE__${info.index}_${exportData}${callFlag}${directImportFlag}${strictFlag}${asiSafeFlag}__`;
}; };
const MODULE_REFERENCE_REGEXP = /^__WEBPACK_MODULE_REFERENCE__(\d+)_([\da-f]+|ns)(_call)?(_directImport)?(_strict)?(_asiSafe)?__$/; const MODULE_REFERENCE_REGEXP = /^__WEBPACK_MODULE_REFERENCE__(\d+)_([\da-f]+|ns)(_call)?(_directImport)?(_strict)?(?:_asiSafe(\d))?__$/;
const isModuleReference = name => { const isModuleReference = name => {
return MODULE_REFERENCE_REGEXP.test(name); return MODULE_REFERENCE_REGEXP.test(name);
@ -598,6 +608,7 @@ const matchModuleReference = (name, modulesWithInfo) => {
const match = MODULE_REFERENCE_REGEXP.exec(name); const match = MODULE_REFERENCE_REGEXP.exec(name);
if (!match) return null; if (!match) return null;
const index = +match[1]; const index = +match[1];
const asiSafe = match[6];
return { return {
index, index,
info: modulesWithInfo[index], info: modulesWithInfo[index],
@ -608,7 +619,7 @@ const matchModuleReference = (name, modulesWithInfo) => {
call: !!match[3], call: !!match[3],
directImport: !!match[4], directImport: !!match[4],
strict: !!match[5], strict: !!match[5],
asiSafe: !!match[6] asiSafe: asiSafe ? asiSafe === "1" : undefined
}; };
}; };

View File

@ -0,0 +1 @@
export function a() {}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,15 @@
import {a as b} from "./a";
import * as c from "./b";
function donotcallme() {
expect("asi unsafe call happened").toBe(false);
}
it("should respect asi flag", () => {
(donotcallme)
import.meta;
(donotcallme)
b();
(donotcallme)
c;
});

View File

@ -0,0 +1,3 @@
module.exports = [
[/Critical dependency: Accessing import\.meta/]
];

View File

@ -1,4 +1,7 @@
/* globals it, should */ function donotcallme() {
expect("asi unsafe call happened").toBe(false);
}
it("should define FALSE", function() { it("should define FALSE", function() {
expect(FALSE).toBe(false); expect(FALSE).toBe(false);
expect(typeof FALSE).toBe("boolean"); expect(typeof FALSE).toBe("boolean");
@ -126,6 +129,10 @@ it("should define OBJECT", function() {
expect(o.SUB.FUNCTION(10)).toBe(11); expect(o.SUB.FUNCTION(10)).toBe(11);
}); });
it("should define OBJECT.SUB.CODE", function() { it("should define OBJECT.SUB.CODE", function() {
(donotcallme)
OBJECT;
(donotcallme)
OBJECT.SUB;
expect(typeof OBJECT.SUB.CODE).toBe("number"); expect(typeof OBJECT.SUB.CODE).toBe("number");
expect(OBJECT.SUB.CODE).toBe(3); expect(OBJECT.SUB.CODE).toBe(3);
if (OBJECT.SUB.CODE !== 3) require("fail"); if (OBJECT.SUB.CODE !== 3) require("fail");
@ -148,6 +155,8 @@ it("should define OBJECT.SUB.STRING", function() {
})(OBJECT.SUB); })(OBJECT.SUB);
}); });
it("should define ARRAY", function() { it("should define ARRAY", function() {
(donotcallme)
ARRAY;
expect(Array.isArray(ARRAY)).toBeTruthy(); expect(Array.isArray(ARRAY)).toBeTruthy();
expect(ARRAY).toHaveLength(2); expect(ARRAY).toHaveLength(2);
}); });

2
types.d.ts vendored
View File

@ -3824,7 +3824,7 @@ declare class JavascriptParser extends Parser {
parseCalculatedString(expression?: any): any; parseCalculatedString(expression?: any): any;
evaluate(source?: any): BasicEvaluatedExpression; evaluate(source?: any): BasicEvaluatedExpression;
getComments(range?: any): any; getComments(range?: any): any;
isAsiPosition(pos?: any): any; isAsiPosition(pos: number): boolean;
isStatementLevelExpression(expr?: any): boolean; isStatementLevelExpression(expr?: any): boolean;
getTagData(name?: any, tag?: any): any; getTagData(name?: any, tag?: any): any;
tagVariable(name?: any, tag?: any, data?: any): void; tagVariable(name?: any, tag?: any, data?: any): void;