improve side effects analysis to report imported and reexports symbols as side-effect-free

add bailout messages for side effects
This commit is contained in:
Tobias Koppers 2020-11-28 17:30:32 +01:00
parent fb7d09b315
commit c7ea63875c
8 changed files with 275 additions and 224 deletions

View File

@ -26,7 +26,7 @@ export default Dashboard;
```javascript
import { Button, Dialog } from "../components";
const Dashboard = () => {
const Login = () => {
return (
<>
<Button />
@ -34,7 +34,7 @@ const Dashboard = () => {
</>
);
};
export default Dashboard;
export default Login;
```
# components/index.js
@ -193,11 +193,11 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../components */ "./components/Dialog.js");
const Dashboard = () => {
const Login = () => {
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(_components__WEBPACK_IMPORTED_MODULE_0__.default, null), /*#__PURE__*/React.createElement(_components__WEBPACK_IMPORTED_MODULE_1__.default, null));
};
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Dashboard);
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Login);
/***/ })
@ -213,8 +213,8 @@ const Dashboard = () => {
## Unoptimized
```
asset output.js 10.7 KiB [emitted] (name: main)
asset pages_Login_js.output.js 2.85 KiB [emitted]
asset output.js 10.8 KiB [emitted] (name: main)
asset pages_Login_js.output.js 2.84 KiB [emitted]
asset pages_Dashboard_js.output.js 2.8 KiB [emitted]
chunk (runtime: main) output.js (main) 208 bytes (javascript) 5.42 KiB (runtime) [entry] [rendered]
> ./example.js main
@ -231,15 +231,15 @@ chunk (runtime: main) pages_Dashboard_js.output.js 513 bytes [rendered]
[exports: default]
context element ./Dashboard ./pages/ lazy ^\.\/.*$ namespace object ./Dashboard
context element ./Dashboard.js ./pages/ lazy ^\.\/.*$ namespace object ./Dashboard.js
chunk (runtime: main) pages_Login_js.output.js 512 bytes [rendered]
chunk (runtime: main) pages_Login_js.output.js 504 bytes [rendered]
> ./Login ./pages/ lazy ^\.\/.*$ namespace object ./Login
> ./Login.js ./pages/ lazy ^\.\/.*$ namespace object ./Login.js
dependent modules 247 bytes [dependent] 2 modules
./pages/Login.js 265 bytes [optional] [built] [code generated]
./pages/Login.js 257 bytes [optional] [built] [code generated]
[exports: default]
context element ./Login ./pages/ lazy ^\.\/.*$ namespace object ./Login
context element ./Login.js ./pages/ lazy ^\.\/.*$ namespace object ./Login.js
webpack 5.7.0 compiled successfully
webpack 5.8.0 compiled successfully
```
## Production mode
@ -263,13 +263,13 @@ chunk (runtime: main) pages_Dashboard_js.output.js 513 bytes [rendered]
[exports: default]
context element ./Dashboard ./pages/ lazy ^\.\/.*$ namespace object ./Dashboard
context element ./Dashboard.js ./pages/ lazy ^\.\/.*$ namespace object ./Dashboard.js
chunk (runtime: main) pages_Login_js.output.js 512 bytes [rendered]
chunk (runtime: main) pages_Login_js.output.js 504 bytes [rendered]
> ./Login ./pages/ lazy ^\.\/.*$ namespace object ./Login
> ./Login.js ./pages/ lazy ^\.\/.*$ namespace object ./Login.js
dependent modules 115 bytes [dependent] 1 module
./pages/Login.js + 1 modules 397 bytes [optional] [built] [code generated]
./pages/Login.js + 1 modules 389 bytes [optional] [built] [code generated]
[exports: default]
context element ./Login ./pages/ lazy ^\.\/.*$ namespace object ./Login
context element ./Login.js ./pages/ lazy ^\.\/.*$ namespace object ./Login.js
webpack 5.7.0 compiled successfully
webpack 5.8.0 compiled successfully
```

View File

@ -1,3 +0,0 @@
{
"sideEffects": false
}

View File

@ -1,6 +1,6 @@
import { Button, Dialog } from "../components";
const Dashboard = () => {
const Login = () => {
return (
<>
<Button />
@ -8,4 +8,4 @@ const Dashboard = () => {
</>
);
};
export default Dashboard;
export default Login;

View File

@ -26,6 +26,7 @@ const ModuleWarning = require("./ModuleWarning");
const RuntimeGlobals = require("./RuntimeGlobals");
const UnhandledSchemeError = require("./UnhandledSchemeError");
const WebpackError = require("./WebpackError");
const formatLocation = require("./formatLocation");
const LazySet = require("./util/LazySet");
const { getScheme } = require("./util/URLAbsoluteSpecifier");
const {
@ -248,6 +249,7 @@ class NormalModule extends Module {
this._lastSuccessfulBuildMeta = {};
this._forceBuild = true;
this._isEvaluatingSideEffects = false;
this._addedSideEffectsBailout = new WeakSet();
}
/**
@ -877,6 +879,17 @@ class NormalModule extends Module {
for (const dep of this.dependencies) {
const state = dep.getModuleEvaluationSideEffectsState(moduleGraph);
if (state === true) {
if (!this._addedSideEffectsBailout.has(moduleGraph)) {
this._addedSideEffectsBailout.add(moduleGraph);
moduleGraph
.getOptimizationBailout(this)
.push(
() =>
`Dependency (${
dep.type
}) with side effects at ${formatLocation(dep.loc)}`
);
}
this._isEvaluatingSideEffects = false;
return true;
} else if (state !== ModuleGraphConnection.CIRCULAR_CONNECTION) {

View File

@ -370,6 +370,14 @@ class HarmonyExportImportedSpecifierDependency extends HarmonyImportDependency {
};
}
/**
* @param {ModuleGraph} moduleGraph the module graph
* @returns {ConnectionState} how this dependency connects the module to referencing modules
*/
getModuleEvaluationSideEffectsState(moduleGraph) {
return false;
}
/**
* Returns list of exports referenced by this dependency
* @param {ModuleGraph} moduleGraph module graph

View File

@ -88,6 +88,14 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency {
this.checkUsedByExports(moduleGraph, runtime);
}
/**
* @param {ModuleGraph} moduleGraph the module graph
* @returns {ConnectionState} how this dependency connects the module to referencing modules
*/
getModuleEvaluationSideEffectsState(moduleGraph) {
return false;
}
checkUsedByExports(moduleGraph, runtime) {
if (this.usedByExports === false) return false;
if (this.usedByExports !== true && this.usedByExports !== undefined) {

View File

@ -9,6 +9,7 @@ const glob2regexp = require("glob-to-regexp");
const { STAGE_DEFAULT } = require("../OptimizationStages");
const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency");
const HarmonyImportSpecifierDependency = require("../dependencies/HarmonyImportSpecifierDependency");
const formatLocation = require("../formatLocation");
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../Dependency")} Dependency */
@ -67,223 +68,242 @@ class SideEffectsFlagPlugin {
cache = new Map();
globToRegexpCache.set(compiler.root, cache);
}
compiler.hooks.normalModuleFactory.tap("SideEffectsFlagPlugin", nmf => {
nmf.hooks.module.tap("SideEffectsFlagPlugin", (module, data) => {
const resolveData = data.resourceResolveData;
if (
resolveData &&
resolveData.descriptionFileData &&
resolveData.relativePath
) {
const sideEffects = resolveData.descriptionFileData.sideEffects;
const hasSideEffects = SideEffectsFlagPlugin.moduleHasSideEffects(
resolveData.relativePath,
sideEffects,
cache
);
if (!hasSideEffects) {
if (module.factoryMeta === undefined) {
module.factoryMeta = {};
}
module.factoryMeta.sideEffectFree = true;
}
}
return module;
});
nmf.hooks.module.tap("SideEffectsFlagPlugin", (module, data) => {
if (data.settings.sideEffects === false) {
if (module.factoryMeta === undefined) {
module.factoryMeta = {};
}
module.factoryMeta.sideEffectFree = true;
} else if (data.settings.sideEffects === true) {
if (module.factoryMeta !== undefined) {
module.factoryMeta.sideEffectFree = false;
}
}
return module;
});
if (this._analyseSource) {
/**
* @param {JavascriptParser} parser the parser
* @returns {void}
*/
const parserHandler = parser => {
let hasSideEffects = false;
parser.hooks.program.tap("SideEffectsFlagPlugin", () => {
hasSideEffects = false;
});
parser.hooks.statement.tap(
{ name: "SideEffectsFlagPlugin", stage: -100 },
statement => {
if (hasSideEffects) return;
if (parser.scope.topLevelScope !== true) return;
switch (statement.type) {
case "ExpressionStatement":
if (
!parser.isPure(statement.expression, statement.range[0])
) {
hasSideEffects = true;
}
break;
case "IfStatement":
case "WhileStatement":
case "DoWhileStatement":
if (!parser.isPure(statement.test, statement.range[0])) {
hasSideEffects = true;
}
// statement hook will be called for child statements too
break;
case "ForStatement":
if (
!parser.isPure(statement.init, statement.range[0]) ||
!parser.isPure(
statement.test,
statement.init
? statement.init.range[1]
: statement.range[0]
) ||
!parser.isPure(
statement.update,
statement.test
? statement.test.range[1]
: statement.init
? statement.init.range[1]
: statement.range[0]
)
) {
hasSideEffects = true;
}
// statement hook will be called for child statements too
break;
case "SwitchStatement":
if (
!parser.isPure(statement.discriminant, statement.range[0])
) {
hasSideEffects = true;
}
// statement hook will be called for child statements too
break;
case "VariableDeclaration":
case "ClassDeclaration":
case "FunctionDeclaration":
if (!parser.isPure(statement, statement.range[0])) {
hasSideEffects = true;
}
break;
case "ExportDefaultDeclaration":
if (
!parser.isPure(statement.declaration, statement.range[0])
) {
hasSideEffects = true;
}
break;
case "ExportNamedDeclaration":
if (statement.source) {
hasSideEffects = true;
}
break;
case "LabeledStatement":
case "BlockStatement":
// statement hook will be called for child statements too
break;
case "EmptyStatement":
break;
case "ImportDeclaration":
// imports will be handled by the dependencies
break;
default:
hasSideEffects = true;
break;
compiler.hooks.compilation.tap(
"SideEffectsFlagPlugin",
(compilation, { normalModuleFactory }) => {
const moduleGraph = compilation.moduleGraph;
normalModuleFactory.hooks.module.tap(
"SideEffectsFlagPlugin",
(module, data) => {
const resolveData = data.resourceResolveData;
if (
resolveData &&
resolveData.descriptionFileData &&
resolveData.relativePath
) {
const sideEffects = resolveData.descriptionFileData.sideEffects;
const hasSideEffects = SideEffectsFlagPlugin.moduleHasSideEffects(
resolveData.relativePath,
sideEffects,
cache
);
if (!hasSideEffects) {
if (module.factoryMeta === undefined) {
module.factoryMeta = {};
}
module.factoryMeta.sideEffectFree = true;
}
}
);
parser.hooks.finish.tap("SideEffectsFlagPlugin", () => {
if (!hasSideEffects) {
parser.state.module.buildMeta.sideEffectFree = true;
return module;
}
);
normalModuleFactory.hooks.module.tap(
"SideEffectsFlagPlugin",
(module, data) => {
if (data.settings.sideEffects === false) {
if (module.factoryMeta === undefined) {
module.factoryMeta = {};
}
module.factoryMeta.sideEffectFree = true;
} else if (data.settings.sideEffects === true) {
if (module.factoryMeta !== undefined) {
module.factoryMeta.sideEffectFree = false;
}
}
});
};
for (const key of [
"javascript/auto",
"javascript/esm",
"javascript/dynamic"
]) {
nmf.hooks.parser.for(key).tap("SideEffectsFlagPlugin", parserHandler);
return module;
}
);
if (this._analyseSource) {
/**
* @param {JavascriptParser} parser the parser
* @returns {void}
*/
const parserHandler = parser => {
let sideEffectsStatement;
parser.hooks.program.tap("SideEffectsFlagPlugin", () => {
sideEffectsStatement = undefined;
});
parser.hooks.statement.tap(
{ name: "SideEffectsFlagPlugin", stage: -100 },
statement => {
if (sideEffectsStatement) return;
if (parser.scope.topLevelScope !== true) return;
switch (statement.type) {
case "ExpressionStatement":
if (
!parser.isPure(statement.expression, statement.range[0])
) {
sideEffectsStatement = statement;
}
break;
case "IfStatement":
case "WhileStatement":
case "DoWhileStatement":
if (!parser.isPure(statement.test, statement.range[0])) {
sideEffectsStatement = statement;
}
// statement hook will be called for child statements too
break;
case "ForStatement":
if (
!parser.isPure(statement.init, statement.range[0]) ||
!parser.isPure(
statement.test,
statement.init
? statement.init.range[1]
: statement.range[0]
) ||
!parser.isPure(
statement.update,
statement.test
? statement.test.range[1]
: statement.init
? statement.init.range[1]
: statement.range[0]
)
) {
sideEffectsStatement = statement;
}
// statement hook will be called for child statements too
break;
case "SwitchStatement":
if (
!parser.isPure(statement.discriminant, statement.range[0])
) {
sideEffectsStatement = statement;
}
// statement hook will be called for child statements too
break;
case "VariableDeclaration":
case "ClassDeclaration":
case "FunctionDeclaration":
if (!parser.isPure(statement, statement.range[0])) {
sideEffectsStatement = statement;
}
break;
case "ExportNamedDeclaration":
case "ExportDefaultDeclaration":
if (
!parser.isPure(statement.declaration, statement.range[0])
) {
sideEffectsStatement = statement;
}
break;
case "LabeledStatement":
case "BlockStatement":
// statement hook will be called for child statements too
break;
case "EmptyStatement":
break;
case "ExportAllDeclaration":
case "ImportDeclaration":
// imports will be handled by the dependencies
break;
default:
sideEffectsStatement = statement;
break;
}
}
);
parser.hooks.finish.tap("SideEffectsFlagPlugin", () => {
if (sideEffectsStatement === undefined) {
parser.state.module.buildMeta.sideEffectFree = true;
} else {
const { loc, type } = sideEffectsStatement;
moduleGraph
.getOptimizationBailout(parser.state.module)
.push(
() =>
`Statement (${type}) with side effects in source code at ${formatLocation(
loc
)}`
);
}
});
};
for (const key of [
"javascript/auto",
"javascript/esm",
"javascript/dynamic"
]) {
normalModuleFactory.hooks.parser
.for(key)
.tap("SideEffectsFlagPlugin", parserHandler);
}
}
}
});
compiler.hooks.compilation.tap("SideEffectsFlagPlugin", compilation => {
const moduleGraph = compilation.moduleGraph;
compilation.hooks.optimizeDependencies.tap(
{
name: "SideEffectsFlagPlugin",
stage: STAGE_DEFAULT
},
modules => {
const logger = compilation.getLogger("webpack.SideEffectsFlagPlugin");
compilation.hooks.optimizeDependencies.tap(
{
name: "SideEffectsFlagPlugin",
stage: STAGE_DEFAULT
},
modules => {
const logger = compilation.getLogger(
"webpack.SideEffectsFlagPlugin"
);
logger.time("update dependencies");
for (const module of modules) {
if (module.getSideEffectsConnectionState(moduleGraph) === false) {
const exportsInfo = moduleGraph.getExportsInfo(module);
for (const connection of moduleGraph.getIncomingConnections(
module
)) {
const dep = connection.dependency;
let isReexport;
if (
(isReexport =
dep instanceof HarmonyExportImportedSpecifierDependency) ||
(dep instanceof HarmonyImportSpecifierDependency &&
!dep.namespaceObjectAsContext)
) {
// TODO improve for export *
if (isReexport && dep.name) {
const exportInfo = moduleGraph.getExportInfo(
connection.originModule,
dep.name
);
exportInfo.moveTarget(
moduleGraph,
({ module }) =>
module.getSideEffectsConnectionState(moduleGraph) ===
false
);
}
// TODO improve for nested imports
const ids = dep.getIds(moduleGraph);
if (ids.length > 0) {
const exportInfo = exportsInfo.getExportInfo(ids[0]);
const target = exportInfo.moveTarget(
moduleGraph,
({ module }) =>
module.getSideEffectsConnectionState(moduleGraph) ===
false
);
if (!target) continue;
logger.time("update dependencies");
for (const module of modules) {
if (module.getSideEffectsConnectionState(moduleGraph) === false) {
const exportsInfo = moduleGraph.getExportsInfo(module);
for (const connection of moduleGraph.getIncomingConnections(
module
)) {
const dep = connection.dependency;
let isReexport;
if (
(isReexport =
dep instanceof
HarmonyExportImportedSpecifierDependency) ||
(dep instanceof HarmonyImportSpecifierDependency &&
!dep.namespaceObjectAsContext)
) {
// TODO improve for export *
if (isReexport && dep.name) {
const exportInfo = moduleGraph.getExportInfo(
connection.originModule,
dep.name
);
exportInfo.moveTarget(
moduleGraph,
({ module }) =>
module.getSideEffectsConnectionState(moduleGraph) ===
false
);
}
// TODO improve for nested imports
const ids = dep.getIds(moduleGraph);
if (ids.length > 0) {
const exportInfo = exportsInfo.getExportInfo(ids[0]);
const target = exportInfo.moveTarget(
moduleGraph,
({ module }) =>
module.getSideEffectsConnectionState(moduleGraph) ===
false
);
if (!target) continue;
moduleGraph.updateModule(dep, target.module);
moduleGraph.addExplanation(
dep,
"(skipped side-effect-free modules)"
);
dep.setIds(
moduleGraph,
target.export
? [...target.export, ...ids.slice(1)]
: ids.slice(1)
);
moduleGraph.updateModule(dep, target.module);
moduleGraph.addExplanation(
dep,
"(skipped side-effect-free modules)"
);
dep.setIds(
moduleGraph,
target.export
? [...target.export, ...ids.slice(1)]
: ids.slice(1)
);
}
}
}
}
}
logger.timeEnd("update dependencies");
}
logger.timeEnd("update dependencies");
}
);
});
);
}
);
}
static moduleHasSideEffects(moduleName, flagValue, cache) {

View File

@ -1,3 +1,8 @@
import "./module";
import "./cjs";
import "./pure";
import { unusedExport } from "./pure";
export { unusedExport } from "./pure";
export function unused() {
return unusedExport;
}