Source code

Revision control

Copy as Markdown

Other Tools

/*
Author Sergey Melyukov @smelukov
*/
"use strict";
const { UsageState } = require("../ExportsInfo");
const JavascriptParser = require("../javascript/JavascriptParser");
/** @typedef {import("../Compilation")} Compilation */
/** @typedef {import("../Dependency")} Dependency */
/** @typedef {import("../Dependency").GetConditionFn} GetConditionFn */
/** @typedef {import("../Module")} Module */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../Parser").ParserState} ParserState */
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
/** @typedef {boolean | ((compilation: Compilation, module: Module) => boolean)} PureCondition */
/** @typedef {Set<string | TopLevelSymbol>} InnerGraphValueSet */
/** @typedef {InnerGraphValueSet | true} InnerGraphValue */
/** @typedef {TopLevelSymbol | null} InnerGraphKey */
/** @typedef {Map<InnerGraphKey, InnerGraphValue | undefined>} InnerGraph */
/** @typedef {(value: boolean | Set<string> | undefined, module: Module) => void} UsageCallback */
/**
* Defines the state object type used by this module.
* @typedef {object} StateObject
* @property {InnerGraph} innerGraph
* @property {TopLevelSymbol=} currentTopLevelSymbol
* @property {Map<TopLevelSymbol, Set<UsageCallback>>} usageCallbackMap
*/
/** @typedef {false | StateObject} State */
/** @typedef {string | TopLevelSymbol | true} Usage */
/** @typedef {Set<string> | boolean} UsedByExports */
/** @typedef {Dependency & { usedByExports: UsedByExports | undefined }} DependencyWithUsedByExports */
class TopLevelSymbol {
/**
* Creates an instance of TopLevelSymbol.
* @param {string} name name of the variable
* @param {PureCondition=} pure pure condition
*/
constructor(name, pure = false) {
/** @type {string} */
this.name = name;
/** @type {boolean} */
this.conditional = false;
/** @type {boolean} */
this._pure = false;
/** @type {((compilation: Compilation, module: Module) => boolean) | undefined} */
this.pureFn = undefined;
this.setPure(pure);
}
/**
* Sets the pure condition
* @param {PureCondition} pure pure condition
* @returns {void}
*/
setPure(pure) {
this.conditional = typeof pure === "function";
this._pure = pure === true;
this.pureFn = typeof pure === "function" ? pure : undefined;
}
/**
* @param {Compilation} compilation compilation
* @param {Module} module module
* @returns {boolean} if the symbol is pure
*/
isPure(compilation, module) {
if (!this.conditional) return this._pure;
const pureFn =
/** @type {(compilation: Compilation, module: Module) => boolean} */ (
this.pureFn
);
return pureFn(compilation, module);
}
}
module.exports.TopLevelSymbol = TopLevelSymbol;
const topLevelSymbolTag = Symbol("top level symbol");
/** @type {WeakMap<Compilation, InnerGraphUtils>} */
const innerGraphByCompilation = new WeakMap();
/**
* @typedef {object} InnerGraphUtils
* @property {(parserState: ParserState) => void} enable
* @property {(parserState: ParserState) => void} bailout
* @property {(parserState: ParserState) => boolean} isEnabled
* @property {(parserState: ParserState, symbol: TopLevelSymbol | null, usage: Usage) => void} addUsage
* @property {(parserState: ParserState, onUsageCallback: UsageCallback) => void} onUsage
* @property {(parserState: ParserState, symbol: TopLevelSymbol | undefined) => void} setTopLevelSymbol
* @property {(parserState: ParserState) => TopLevelSymbol | void} getTopLevelSymbol
* @property {(parser: JavascriptParser, name: string, pure?: PureCondition) => TopLevelSymbol | undefined} tagTopLevelSymbol
* @property {(parser: JavascriptParser, name: string, usage: Usage) => void} addVariableUsage
* @property {(module: Module) => void} inferDependencyUsage
* @property {(module: Module) => void} release
*/
/**
* Returns false, when unused. Otherwise true.
* @param {Dependency} dependency the dependency
* @param {UsedByExports | undefined} usedByExports usedByExports info
* @param {ModuleGraph} moduleGraph moduleGraph
* @param {RuntimeSpec} runtime runtime
* @returns {boolean} false, when unused. Otherwise true
*/
function isDependencyUsedByExports(
dependency,
usedByExports,
moduleGraph,
runtime
) {
if (usedByExports === false) return false;
if (usedByExports !== true && usedByExports !== undefined) {
const selfModule =
/** @type {Module} */
(moduleGraph.getParentModule(dependency));
const exportsInfo = moduleGraph.getExportsInfo(selfModule);
for (const exportName of usedByExports) {
if (exportsInfo.getUsed(exportName, runtime) !== UsageState.Unused) {
return true;
}
}
return false;
}
return true;
}
/**
* Returns dependency condition
* @param {Dependency} dependency the dependency
* @param {ModuleGraph} moduleGraph moduleGraph
* @returns {null | false | GetConditionFn} function to determine if the connection is active
*/
module.exports.getDependencyUsedByExportsCondition = (
dependency,
moduleGraph
) => {
switch (
/** @type {DependencyWithUsedByExports} */ (dependency).usedByExports
) {
case false:
return false;
case true:
return null;
default:
return (_connections, runtime) =>
isDependencyUsedByExports(
dependency,
/** @type {DependencyWithUsedByExports} */ (dependency).usedByExports,
moduleGraph,
runtime
);
}
};
/**
* Returns the InnerGraph utils scoped to a single compilation.
* @param {Compilation} compilation the compilation
* @returns {InnerGraphUtils} utils
*/
module.exports.getInnerGraphUtils = (compilation) => {
let utils = innerGraphByCompilation.get(compilation);
if (utils) return utils;
/** @type {WeakMap<Module, State>} */
const states = new WeakMap();
/**
* @param {ParserState} parserState parser state
* @returns {State | undefined} state
*/
function getState(parserState) {
return states.get(parserState.module);
}
/**
* @param {ParserState} parserState parser state
* @returns {void}
*/
function enable(parserState) {
const state = states.get(parserState.module);
if (state === false) {
return;
}
states.set(parserState.module, {
innerGraph: new Map(),
currentTopLevelSymbol: undefined,
usageCallbackMap: new Map()
});
}
/**
* @param {ParserState} parserState parser state
* @returns {void}
*/
function bailout(parserState) {
states.set(parserState.module, false);
}
/**
* @param {ParserState} parserState parser state
* @returns {boolean} true, when enabled
*/
function isEnabled(parserState) {
return Boolean(states.get(parserState.module));
}
/**
* @param {ParserState} state parser state
* @param {TopLevelSymbol | null} symbol the symbol, or null for all symbols
* @param {Usage} usage usage data
* @returns {void}
*/
function addUsage(state, symbol, usage) {
const innerGraphState = getState(state);
if (innerGraphState) {
const { innerGraph } = innerGraphState;
const info = innerGraph.get(symbol);
if (usage === true) {
innerGraph.set(symbol, true);
} else if (info === undefined) {
innerGraph.set(symbol, new Set([usage]));
} else if (info !== true) {
info.add(usage);
}
}
}
/**
* @param {JavascriptParser} parser the parser
* @param {string} name name of variable
* @param {Usage} usage usage data
* @returns {void}
*/
function addVariableUsage(parser, name, usage) {
const symbol =
/** @type {TopLevelSymbol} */ (
parser.getTagData(name, topLevelSymbolTag)
) || tagTopLevelSymbol(parser, name);
if (symbol) {
addUsage(parser.state, symbol, usage);
}
}
/**
* @param {ParserState} state parser state
* @returns {TopLevelSymbol | void} usage data
*/
function getTopLevelSymbol(state) {
const innerGraphState = getState(state);
if (innerGraphState) {
return innerGraphState.currentTopLevelSymbol;
}
}
/**
* @param {ParserState} state parser state
* @param {TopLevelSymbol | undefined} symbol the symbol
*/
function setTopLevelSymbol(state, symbol) {
const innerGraphState = getState(state);
if (innerGraphState) {
innerGraphState.currentTopLevelSymbol = symbol;
}
}
/**
* @param {ParserState} state parser state
* @param {UsageCallback} onUsageCallback on usage callback
*/
function onUsage(state, onUsageCallback) {
const innerGraphState = getState(state);
if (innerGraphState) {
const { usageCallbackMap, currentTopLevelSymbol } = innerGraphState;
if (currentTopLevelSymbol) {
let callbacks = usageCallbackMap.get(currentTopLevelSymbol);
if (callbacks === undefined) {
/** @type {Set<UsageCallback>} */
callbacks = new Set();
usageCallbackMap.set(currentTopLevelSymbol, callbacks);
}
callbacks.add(onUsageCallback);
} else {
onUsageCallback(true, state.module);
}
} else {
onUsageCallback(undefined, state.module);
}
}
/**
* @param {JavascriptParser} parser parser
* @param {string} name name of variable
* @param {PureCondition=} pure pure condition
* @returns {TopLevelSymbol=} symbol
*/
function tagTopLevelSymbol(parser, name, pure) {
const innerGraphState = getState(parser.state);
if (!innerGraphState) return;
parser.defineVariable(name);
const existingTag = /** @type {TopLevelSymbol} */ (
parser.getTagData(name, topLevelSymbolTag)
);
if (existingTag) {
if (pure !== undefined) {
existingTag.setPure(pure);
}
return existingTag;
}
const symbol = new TopLevelSymbol(name, pure);
parser.tagVariable(
name,
topLevelSymbolTag,
symbol,
JavascriptParser.VariableInfoFlags.Normal
);
return symbol;
}
/**
* @param {Module} module module
* @returns {void}
*/
function inferDependencyUsage(module) {
const innerGraphState = states.get(module);
if (!innerGraphState) {
return;
}
const { innerGraph, usageCallbackMap } = innerGraphState;
/** @type {Map<InnerGraphKey, InnerGraphValueSet | undefined>} */
const processed = new Map();
// flatten graph to terminal nodes (string, undefined or true)
const nonTerminal = new Set(innerGraph.keys());
while (nonTerminal.size > 0) {
for (const key of nonTerminal) {
if (key !== null && !key.isPure(compilation, module)) {
innerGraph.set(key, true);
nonTerminal.delete(key);
continue;
}
/** @type {InnerGraphValue} */
let newSet = new Set();
let isTerminal = true;
const value = innerGraph.get(key);
let alreadyProcessed = processed.get(key);
if (alreadyProcessed === undefined) {
/** @type {InnerGraphValueSet} */
alreadyProcessed = new Set();
processed.set(key, alreadyProcessed);
}
if (value !== true && value !== undefined) {
for (const item of value) {
alreadyProcessed.add(item);
}
for (const item of value) {
if (typeof item === "string") {
newSet.add(item);
} else if (!item.isPure(compilation, module)) {
newSet = true;
break;
} else {
const itemValue = innerGraph.get(item);
if (itemValue === true) {
newSet = true;
break;
}
if (itemValue !== undefined) {
for (const i of itemValue) {
if (i === key) continue;
if (alreadyProcessed.has(i)) continue;
newSet.add(i);
if (typeof i !== "string") {
isTerminal = false;
}
}
}
}
}
if (newSet === true) {
innerGraph.set(key, true);
} else if (newSet.size === 0) {
innerGraph.set(key, undefined);
} else {
innerGraph.set(key, newSet);
}
}
if (isTerminal) {
nonTerminal.delete(key);
// For the global key, merge with all other keys
if (key === null) {
const globalValue = innerGraph.get(null);
if (globalValue) {
for (const [key, value] of innerGraph) {
if (key !== null && value !== true) {
if (globalValue === true) {
innerGraph.set(key, true);
} else {
const newSet = new Set(value);
for (const item of globalValue) {
newSet.add(item);
}
innerGraph.set(key, newSet);
}
}
}
}
}
}
}
}
/** @type {Map<Dependency, true | Set<string>>} */
for (const [symbol, callbacks] of usageCallbackMap) {
const usage = symbol.isPure(compilation, module)
? /** @type {true | Set<string> | undefined} */ (innerGraph.get(symbol))
: true;
for (const callback of callbacks) {
callback(usage === undefined ? false : usage, module);
}
}
}
/**
* @param {Module} module module
* @returns {void}
*/
function release(module) {
states.delete(module);
}
utils = {
enable,
bailout,
isEnabled,
addUsage,
addVariableUsage,
getTopLevelSymbol,
setTopLevelSymbol,
onUsage,
tagTopLevelSymbol,
inferDependencyUsage,
release
};
innerGraphByCompilation.set(compilation, utils);
return utils;
};
/**
* Usage callback map.
* @param {Dependency} dependency the dependency
* @param {UsedByExports | undefined} usedByExports usedByExports info
* @param {ModuleGraph} moduleGraph moduleGraph
* @returns {null | false | GetConditionFn} function to determine if the connection is active
*/
module.exports.topLevelSymbolTag = topLevelSymbolTag;