fix edge case in scope analysis

fix double declaration problem in variable declarations
remove TrackingSet
rename StackedSetMap to StackedMap and remove add method
add more scope analysis test
This commit is contained in:
Tobias Koppers 2019-09-05 11:47:18 +02:00
parent acc7c3e1ea
commit ec518945f1
9 changed files with 381 additions and 173 deletions

View File

@ -9,19 +9,21 @@ const { Parser } = require("acorn");
const { SyncBailHook, HookMap } = require("tapable");
const vm = require("vm");
const BasicEvaluatedExpression = require("./BasicEvaluatedExpression");
const StackedSetMap = require("./util/StackedSetMap");
const StackedMap = require("./util/StackedMap");
// Syntax: https://developer.mozilla.org/en/SpiderMonkey/Parser_API
const parser = Parser.extend(require("./parsing/importAwaitAcornPlugin"));
/**
* @typedef {Object} VariableInfo
* @property {string | true} freeName
* @property {TagInfo} tagInfo
*/
class VariableInfo {
constructor(declaredScope, freeName, tagInfo) {
this.declaredScope = declaredScope;
this.freeName = freeName;
this.tagInfo = tagInfo;
}
}
/** @typedef {string | true | VariableInfo} ExportedVariableInfo */
/** @typedef {string | ScopeInfo | VariableInfo} ExportedVariableInfo */
/**
* @typedef {Object} TagInfo
@ -32,7 +34,7 @@ const parser = Parser.extend(require("./parsing/importAwaitAcornPlugin"));
/**
* @typedef {Object} ScopeInfo
* @property {StackedSetMap<string, VariableInfo | true>} definitions
* @property {StackedMap<string, VariableInfo | ScopeInfo>} definitions
* @property {boolean | "arrow"} topLevelScope
* @property {boolean} inShorthand
* @property {boolean} isStrict
@ -2132,16 +2134,16 @@ class JavascriptParser {
* @returns {any} resut of hook
*/
callHooksForInfoWithFallback(hookMap, info, fallback, defined, ...args) {
if (info === true) {
if (defined !== undefined) {
return defined();
}
return;
}
let name;
if (typeof info === "string") {
name = info;
} else {
if (!(info instanceof VariableInfo)) {
if (defined !== undefined) {
return defined();
}
return;
}
let tagInfo = info.tagInfo;
while (tagInfo !== undefined) {
const hook = hookMap.get(tagInfo.tag);
@ -2474,7 +2476,7 @@ class JavascriptParser {
inTry: false,
inShorthand: false,
isStrict: false,
definitions: new StackedSetMap()
definitions: new StackedMap()
};
const state = (this.state = initialState || {});
this.comments = comments;
@ -2529,7 +2531,7 @@ class JavascriptParser {
getTagData(name, tag) {
const info = this.scope.definitions.get(name);
if (typeof info === "object") {
if (info instanceof VariableInfo) {
let tagInfo = info.tagInfo;
while (tagInfo !== undefined) {
if (tagInfo.tag === tag) return tagInfo.data;
@ -2543,38 +2545,33 @@ class JavascriptParser {
/** @type {VariableInfo} */
let newInfo;
if (oldInfo === undefined) {
newInfo = {
freeName: name,
tagInfo: {
tag,
data,
next: undefined
}
};
} else if (oldInfo === true) {
newInfo = {
freeName: true,
tagInfo: {
tag,
data,
next: undefined
}
};
newInfo = new VariableInfo(this.scope, name, {
tag,
data,
next: undefined
});
} else if (oldInfo instanceof VariableInfo) {
newInfo = new VariableInfo(oldInfo.declaredScope, oldInfo.freeName, {
tag,
data,
next: oldInfo.tagInfo
});
} else {
newInfo = {
freeName: oldInfo.freeName,
tagInfo: {
tag,
data,
next: oldInfo.tagInfo
}
};
newInfo = new VariableInfo(oldInfo, true, {
tag,
data,
next: undefined
});
}
this.scope.definitions.set(name, newInfo);
}
defineVariable(name) {
this.scope.definitions.set(name, true);
const oldInfo = this.scope.definitions.get(name);
// Don't redefine variable in same scope to keep existing tags
if (oldInfo instanceof VariableInfo && oldInfo.declaredScope === this.scope)
return;
this.scope.definitions.set(name, this.scope);
}
undefineVariable(name) {
@ -2589,8 +2586,6 @@ class JavascriptParser {
const value = this.scope.definitions.get(name);
if (value === undefined) {
return name;
} else if (value === true) {
return true;
} else {
return value;
}
@ -2606,10 +2601,10 @@ class JavascriptParser {
if (variableInfo === name) {
this.scope.definitions.delete(name);
} else {
this.scope.definitions.set(name, {
freeName: variableInfo,
tagInfo: undefined
});
this.scope.definitions.set(
name,
new VariableInfo(this.scope, variableInfo, undefined)
);
}
} else {
this.scope.definitions.set(name, variableInfo);
@ -2664,14 +2659,14 @@ class JavascriptParser {
return undefined;
}
const rootInfo = this.getVariableInfo(rootName);
/** @type {string | true} */
/** @type {string | ScopeInfo} */
let resolvedRoot;
if (typeof rootInfo === "object") {
if (rootInfo instanceof VariableInfo) {
resolvedRoot = rootInfo.freeName;
} else {
resolvedRoot = rootInfo;
}
if (resolvedRoot === true) {
if (typeof resolvedRoot !== "string") {
return undefined;
}
let name = resolvedRoot;

View File

@ -15,6 +15,20 @@ const topLevelSymbolTag = Symbol("top level symbol");
/** @typedef {Map<TopLevelSymbol | Dependency, Set<string | TopLevelSymbol> | true>} InnerGraph */
const isPure = expr => {
switch (expr.type) {
case "Identifier":
return true;
case "Literal":
return true;
case "ConditionalExpression":
return (
isPure(expr.test) && isPure(expr.consequent) && isPure(expr.alternate)
);
}
return false;
};
class TopLevelSymbol {
/**
* @param {string} name name of the function
@ -65,7 +79,7 @@ class InnerGraphPlugin {
parser.state.harmonyAllExportDependentDependencies = new Set();
});
parser.hooks.finish.tap("HarmonyImportDependencyParserPlugin", () => {
parser.hooks.finish.tap("InnerGraphPlugin", () => {
const innerGraph =
/** @type {InnerGraph} */ (parser.state.harmonyInnerGraph);
if (!innerGraph) return;
@ -158,14 +172,26 @@ class InnerGraphPlugin {
) {
const innerGraph =
/** @type {InnerGraph} */ (parser.state.harmonyInnerGraph);
parser.defineVariable("*default*");
const fn = new TopLevelSymbol("*default*", innerGraph);
parser.tagVariable("*default*", topLevelSymbolTag, fn);
const name = "*default*";
parser.defineVariable(name);
const fn = new TopLevelSymbol(name, innerGraph);
parser.tagVariable(name, topLevelSymbolTag, fn);
statementWithTopLevelSymbol.set(statement, fn);
}
}
}
});
const tagVar = name => {
const innerGraph =
/** @type {InnerGraph} */ (parser.state.harmonyInnerGraph);
parser.defineVariable(name);
const existingTag = parser.getTagData(name, topLevelSymbolTag);
const fn = existingTag || new TopLevelSymbol(name, innerGraph);
if (!existingTag) {
parser.tagVariable(name, topLevelSymbolTag, fn);
}
return fn;
};
/** @type {WeakMap<{}, TopLevelSymbol>} */
const declWithTopLevelSymbol = new WeakMap();
parser.hooks.preDeclarator.tap(
@ -181,30 +207,26 @@ class InnerGraphPlugin {
decl.init.type === "ArrowFunctionExpression" ||
decl.init.type === "ClassExpression"
) {
const innerGraph =
/** @type {InnerGraph} */ (parser.state.harmonyInnerGraph);
parser.defineVariable(decl.id.name);
const fn = new TopLevelSymbol(decl.id.name, innerGraph);
parser.tagVariable(decl.id.name, topLevelSymbolTag, fn);
const name = decl.id.name;
const fn = tagVar(name);
declWithTopLevelSymbol.set(decl, fn);
return true;
}
if (
decl.init.range[0] - decl.id.range[1] > 9 &&
parser
.getComments([decl.id.range[1], decl.init.range[0]])
.some(
comment =>
comment.type === "Block" &&
/^\s*(#|@)__PURE__\s*$/.test(comment.value)
)
(decl.init.range[0] - decl.id.range[1] > 9 &&
parser
.getComments([decl.id.range[1], decl.init.range[0]])
.some(
comment =>
comment.type === "Block" &&
/^\s*(#|@)__PURE__\s*$/.test(comment.value)
)) ||
isPure(decl.init)
) {
const innerGraph =
/** @type {InnerGraph} */ (parser.state.harmonyInnerGraph);
const name = decl.id.name;
parser.defineVariable(name);
const fn = new TopLevelSymbol(name, innerGraph);
parser.tagVariable(name, topLevelSymbolTag, fn);
const fn = tagVar(name);
declWithTopLevelSymbol.set(decl, fn);
const dep = new PureExpressionDependency(decl.init.range);
dep.loc = decl.loc;

View File

@ -12,7 +12,7 @@ const HarmonyCompatibilityDependency = require("../dependencies/HarmonyCompatibi
const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
const ModuleHotAcceptDependency = require("../dependencies/ModuleHotAcceptDependency");
const ModuleHotDeclineDependency = require("../dependencies/ModuleHotDeclineDependency");
const StackedSetMap = require("../util/StackedSetMap");
const StackedMap = require("../util/StackedMap");
const ConcatenatedModule = require("./ConcatenatedModule");
/** @typedef {import("../Compilation")} Compilation */
@ -487,21 +487,21 @@ class ConcatConfiguration {
constructor(rootModule, cloneFrom) {
this.rootModule = rootModule;
if (cloneFrom) {
/** @type {StackedSetMap} */
/** @type {StackedMap<Module, true>} */
this.modules = cloneFrom.modules.createChild();
/** @type {StackedSetMap} */
/** @type {StackedMap<Module, Module>} */
this.warnings = cloneFrom.warnings.createChild();
} else {
/** @type {StackedSetMap} */
this.modules = new StackedSetMap();
this.modules.add(rootModule);
/** @type {StackedSetMap} */
this.warnings = new StackedSetMap();
/** @type {StackedMap<Module, true>} */
this.modules = new StackedMap();
this.modules.set(rootModule, true);
/** @type {StackedMap<Module, Module>} */
this.warnings = new StackedMap();
}
}
add(module) {
this.modules.add(module);
this.modules.set(module, true);
}
has(module) {

166
lib/util/StackedMap.js Normal file
View File

@ -0,0 +1,166 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const TOMBSTONE = Symbol("tombstone");
const UNDEFINED_MARKER = Symbol("undefined");
/**
* @template T
* @typedef {T | undefined} Cell<T>
*/
/**
* @template T
* @typedef {T | typeof TOMBSTONE | typeof UNDEFINED_MARKER} InternalCell<T>
*/
/**
* @template K
* @template V
* @param {[K, InternalCell<V>]} pair the internal cell
* @returns {[K, Cell<V>]} its safe representation
*/
const extractPair = pair => {
const key = pair[0];
const val = pair[1];
if (val === UNDEFINED_MARKER || val === TOMBSTONE) {
return [key, undefined];
} else {
return /** @type {[K, Cell<V>]} */ (pair);
}
};
/**
* @template K
* @template V
*/
class StackedMap {
/**
* @param {Map<K, InternalCell<V>>[]=} parentStack an optional parent
*/
constructor(parentStack) {
/** @type {Map<K, InternalCell<V>>} */
this.map = new Map();
/** @type {Map<K, InternalCell<V>>[]} */
this.stack = parentStack === undefined ? [] : parentStack.slice();
this.stack.push(this.map);
}
/**
* @param {K} item the key of the element to add
* @param {V} value the value of the element to add
* @returns {void}
*/
set(item, value) {
this.map.set(item, value === undefined ? UNDEFINED_MARKER : value);
}
/**
* @param {K} item the item to delete
* @returns {void}
*/
delete(item) {
if (this.stack.length > 1) {
this.map.set(item, TOMBSTONE);
} else {
this.map.delete(item);
}
}
/**
* @param {K} item the item to test
* @returns {boolean} true if the item exists in this set
*/
has(item) {
const topValue = this.map.get(item);
if (topValue !== undefined) {
return topValue !== TOMBSTONE;
}
if (this.stack.length > 1) {
for (let i = this.stack.length - 2; i >= 0; i--) {
const value = this.stack[i].get(item);
if (value !== undefined) {
this.map.set(item, value);
return value !== TOMBSTONE;
}
}
this.map.set(item, TOMBSTONE);
}
return false;
}
/**
* @param {K} item the key of the element to return
* @returns {Cell<V>} the value of the element
*/
get(item) {
const topValue = this.map.get(item);
if (topValue !== undefined) {
return topValue === TOMBSTONE || topValue === UNDEFINED_MARKER
? undefined
: topValue;
}
if (this.stack.length > 1) {
for (let i = this.stack.length - 2; i >= 0; i--) {
const value = this.stack[i].get(item);
if (value !== undefined) {
this.map.set(item, value);
return value === TOMBSTONE || value === UNDEFINED_MARKER
? undefined
: value;
}
}
this.map.set(item, TOMBSTONE);
}
return undefined;
}
_compress() {
if (this.stack.length === 1) return;
this.map = new Map();
for (const data of this.stack) {
for (const pair of data) {
if (pair[1] === TOMBSTONE) {
this.map.delete(pair[0]);
} else {
this.map.set(pair[0], pair[1]);
}
}
}
this.stack = [this.map];
}
asArray() {
this._compress();
return Array.from(this.map.keys());
}
asSet() {
this._compress();
return new Set(this.map.keys());
}
asPairArray() {
this._compress();
return Array.from(this.map.entries(), extractPair);
}
asMap() {
return new Map(this.asPairArray());
}
get size() {
this._compress();
return this.map.size;
}
createChild() {
return new StackedMap(this.stack);
}
}
module.exports = StackedMap;

View File

@ -10,12 +10,12 @@ const UNDEFINED_MARKER = Symbol("undefined");
/**
* @template T
* @typedef {T | true | undefined} Cell<T>
* @typedef {T | undefined} Cell<T>
*/
/**
* @template T
* @typedef {T | true | typeof TOMBSTONE | typeof UNDEFINED_MARKER} InternalCell<T>
* @typedef {T | typeof TOMBSTONE | typeof UNDEFINED_MARKER} InternalCell<T>
*/
/**
@ -38,7 +38,7 @@ const extractPair = pair => {
* @template K
* @template V
*/
class StackedSetMap {
class StackedMap {
/**
* @param {Map<K, InternalCell<V>>[]=} parentStack an optional parent
*/
@ -50,14 +50,6 @@ class StackedSetMap {
this.stack.push(this.map);
}
/**
* @param {K} item the item to add
* @returns {void}
*/
add(item) {
this.map.set(item, true);
}
/**
* @param {K} item the key of the element to add
* @param {V} value the value of the element to add
@ -167,8 +159,8 @@ class StackedSetMap {
}
createChild() {
return new StackedSetMap(this.stack);
return new StackedMap(this.stack);
}
}
module.exports = StackedSetMap;
module.exports = StackedMap;

View File

@ -1,65 +0,0 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
/**
* @template T
* @template U
* @typedef {import("./StackedSetMap")<T,U>} StackedSetMap<T,U>
*/
/**
* @template T
* @template U
*/
class TrackingSet {
/**
* @param {StackedSetMap<T,U>} set the set to track
*/
constructor(set) {
/** @type {StackedSetMap<T,U>} */
this.set = set;
/** @type {Set<T>} */
this.set2 = new Set();
this.stack = set.stack;
}
/**
* @param {T} item the item to add
* @returns {void}
*/
add(item) {
this.set2.add(item);
this.set.add(item);
}
/**
* @param {T} item the item to delete
* @returns {void}
*/
delete(item) {
this.set2.delete(item);
this.set.delete(item);
}
/**
* @param {T} item the item to test
* @returns {boolean} true if the item exists in this set
*/
has(item) {
return this.set.has(item);
}
createChild() {
return this.set.createChild();
}
getAddedItems() {
return this.set2;
}
}
module.exports = TrackingSet;

View File

@ -635,26 +635,26 @@ Entrypoint entry-1 = vendor-1.js entry-1.js
`;
exports[`StatsTestCases should print correct stats for commons-plugin-issue-4980 1`] = `
"Hash: 4de9d58c5e07bace3a862cc016e810ab8d72814f
"Hash: e8a1307ea3c8604531519325cea6a11512fc55f7
Child
Hash: 4de9d58c5e07bace3a86
Hash: e8a1307ea3c860453151
Time: Xms
Built at: 1970-04-20 12:42:42
Asset Size Chunks Chunk Names
app.20e7d920f7a5dd7ba86a.js 6.72 KiB {143} [emitted] app
vendor.44602d2bdfb280718742.js 606 bytes {736} [emitted] vendor
Entrypoint app = vendor.44602d2bdfb280718742.js app.20e7d920f7a5dd7ba86a.js
vendor.3ca106870164d059e3b7.js 606 bytes {736} [emitted] vendor
Entrypoint app = vendor.3ca106870164d059e3b7.js app.20e7d920f7a5dd7ba86a.js
[117] ./entry-1.js + 2 modules 190 bytes {143} [built]
[381] ./constants.js 87 bytes {736} [built]
+ 4 hidden modules
Child
Hash: 2cc016e810ab8d72814f
Hash: 9325cea6a11512fc55f7
Time: Xms
Built at: 1970-04-20 12:42:42
Asset Size Chunks Chunk Names
app.9ff34c93a677586ea3e1.js 6.74 KiB {143} [emitted] app
vendor.44602d2bdfb280718742.js 606 bytes {736} [emitted] vendor
Entrypoint app = vendor.44602d2bdfb280718742.js app.9ff34c93a677586ea3e1.js
vendor.3ca106870164d059e3b7.js 606 bytes {736} [emitted] vendor
Entrypoint app = vendor.3ca106870164d059e3b7.js app.9ff34c93a677586ea3e1.js
[381] ./constants.js 87 bytes {736} [built]
[655] ./entry-2.js + 2 modules 197 bytes {143} [built]
+ 4 hidden modules"
@ -1103,7 +1103,7 @@ chunk {trees} trees.js (trees) 71 bytes [rendered]
`;
exports[`StatsTestCases should print correct stats for import-context-filter 1`] = `
"Hash: ad66943c56c470a291e2
"Hash: 10cc1554ce1aa0fa0104
Time: Xms
Built at: 1970-04-20 12:42:42
Asset Size Chunks Chunk Names
@ -1525,11 +1525,11 @@ If you don't want to include a polyfill, you can use an empty module like this:
`;
exports[`StatsTestCases should print correct stats for module-reasons 1`] = `
"Hash: d467214b226e6b223582
"Hash: 7c9265c9403a708454f8
Time: Xms
Built at: 1970-04-20 12:42:42
Asset Size Chunks Chunk Names
main.js 2.8 KiB {179} [emitted] main
Asset Size Chunks Chunk Names
main.js 2.89 KiB {179} [emitted] main
Entrypoint main = main.js
[237] ./index.js + 2 modules 102 bytes {179} [built]
entry ./index main
@ -2467,7 +2467,7 @@ Entrypoint e2 = runtime.js e2.js"
`;
exports[`StatsTestCases should print correct stats for scope-hoisting-bailouts 1`] = `
"Hash: 8d50ea5a3930d4e325f5
"Hash: f517daabbf7f98de61bd
Time: Xms
Built at: 1970-04-20 12:42:42
Entrypoint index = index.js
@ -2593,7 +2593,7 @@ Entrypoint main = main.js
`;
exports[`StatsTestCases should print correct stats for side-effects-simple-unused 1`] = `
"Hash: 0dd96a6d2b50b602067c
"Hash: d07e55dc2ba899045482
Time: Xms
Built at: 1970-04-20 12:42:42
Asset Size Chunks Chunk Names
@ -3625,11 +3625,11 @@ chunk {794} default/async-a.js (async-a) 134 bytes <{179}> [rendered]
`;
exports[`StatsTestCases should print correct stats for tree-shaking 1`] = `
"Hash: d1c58bcd9027a12fc6a5
"Hash: 394ab56fdb998a8398e6
Time: Xms
Built at: 1970-04-20 12:42:42
Asset Size Chunks Chunk Names
bundle.js 7.78 KiB {179} [emitted] main
bundle.js 7.86 KiB {179} [emitted] main
Entrypoint main = bundle.js
[10] ./index.js 315 bytes {179} [built]
[no exports]

View File

@ -0,0 +1,47 @@
import { A, B, C1, C2, D1, D2, E1, E2, E3, F, G } from "./test";
export { a, b, c, d, e };
if (Math.random() > 0.5) {
var a = () => A;
let b = () => B;
}
let b;
var c = () => C1;
couldCallExportC();
var c = () => C2;
couldCallExportC();
while (Math.random() > 0.5) {
let d = () => D1;
}
while (Math.random() > 0.5) {
var d = () => D2;
}
while (Math.random() > 0.5) {
let d = () => D1;
}
if(false) {
E1();
}
export var e = true ? E2 : E3;
export { f, g }
if(true) {
let inner = () => F;
var f = () => inner();
}
if(true) {
const inner = () => G;
var g = () => inner();
}

View File

@ -0,0 +1,51 @@
const createTestCases = require("../_helpers/createTestCases");
module.exports = createTestCases({
nothing: {
usedExports: [],
expect: {
"./test": []
}
},
a: {
usedExports: ["a"],
expect: {
"./test": ["A"]
}
},
b: {
usedExports: ["b"],
expect: {
"./test": []
}
},
c: {
usedExports: ["c"],
expect: {
"./test": ["C1", "C2"]
}
},
d: {
usedExports: ["d"],
expect: {
"./test": ["D2"]
}
},
e: {
usedExports: ["e"],
expect: {
"./test": ["E2"]
}
},
f: {
usedExports: ["f"],
expect: {
"./test": ["F"]
}
},
g: {
usedExports: ["g"],
expect: {
"./test": ["G"]
}
}
});