Skip to content

Commit

Permalink
Merge pull request #95 from webpack/bugfix/stack-overflow
Browse files Browse the repository at this point in the history
Avoid stack overflow while series hook compilation
  • Loading branch information
sokra authored Apr 12, 2019
2 parents f12a34f + a83eda2 commit 32afdc5
Show file tree
Hide file tree
Showing 9 changed files with 1,286 additions and 93 deletions.
3 changes: 2 additions & 1 deletion lib/AsyncSeriesBailHook.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

class AsyncSeriesBailHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone }) {
content({ onError, onResult, resultReturns, onDone }) {
return this.callTapsSeries({
onError: (i, err, next, doneBreak) => onError(err) + doneBreak(true),
onResult: (i, result, next) =>
`if(${result} !== undefined) {\n${onResult(
result
)};\n} else {\n${next()}}\n`,
resultReturns,
onDone
});
}
Expand Down
74 changes: 48 additions & 26 deletions lib/HookCodeFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class HookCodeFactory {
this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
resultReturns: true,
onDone: () => "",
rethrowIfPossible: true
})
Expand All @@ -43,24 +44,32 @@ class HookCodeFactory {
);
break;
case "promise":
let code = "";
code += '"use strict";\n';
code += "return new Promise((_resolve, _reject) => {\n";
code += "var _sync = true;\n";
code += this.header();
code += this.content({
let errorHelperUsed = false;
const content = this.content({
onError: err => {
let code = "";
code += "if(_sync)\n";
code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));\n`;
code += "else\n";
code += `_reject(${err});\n`;
return code;
errorHelperUsed = true;
return `_error(${err});\n`;
},
onResult: result => `_resolve(${result});\n`,
onDone: () => "_resolve();\n"
});
code += "_sync = false;\n";
let code = "";
code += '"use strict";\n';
code += "return new Promise((_resolve, _reject) => {\n";
if (errorHelperUsed) {
code += "var _sync = true;\n";
code += "function _error(_err) {\n";
code += "if(_sync)\n";
code += "_resolve(Promise.resolve().then(() => { throw _err; }));\n";
code += "else\n";
code += "_reject(_err);\n";
code += "};\n";
}
code += this.header();
code += content;
if (errorHelperUsed) {
code += "_sync = false;\n";
}
code += "});\n";
fn = new Function(this.args(), code);
break;
Expand Down Expand Up @@ -207,35 +216,48 @@ class HookCodeFactory {
return code;
}

callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) {
callTapsSeries({
onError,
onResult,
resultReturns,
onDone,
doneReturns,
rethrowIfPossible
}) {
if (this.options.taps.length === 0) return onDone();
const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
const next = i => {
if (i >= this.options.taps.length) {
return onDone();
const somethingReturns = resultReturns || doneReturns || false;
let code = "";
let current = onDone;
for (let j = this.options.taps.length - 1; j >= 0; j--) {
const i = j;
const unroll = current !== onDone && this.options.taps[i].type !== "sync";
if (unroll) {
code += `function _next${i}() {\n`;
code += current();
code += `}\n`;
current = () => `${somethingReturns ? "return " : ""}_next${i}();\n`;
}
const done = () => next(i + 1);
const done = current;
const doneBreak = skipDone => {
if (skipDone) return "";
return onDone();
};
return this.callTap(i, {
const content = this.callTap(i, {
onError: error => onError(i, error, done, doneBreak),
onResult:
onResult &&
(result => {
return onResult(i, result, done, doneBreak);
}),
onDone:
!onResult &&
(() => {
return done();
}),
onDone: !onResult && done,
rethrowIfPossible:
rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
});
};
return next(0);
current = () => content;
}
code += current();
return code;
}

callTapsLooping({ onError, onDone, rethrowIfPossible }) {
Expand Down
3 changes: 2 additions & 1 deletion lib/SyncBailHook.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

class SyncBailHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
content({ onError, onResult, resultReturns, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onResult: (i, result, next) =>
`if(${result} !== undefined) {\n${onResult(
result
)};\n} else {\n${next()}}\n`,
resultReturns,
onDone,
rethrowIfPossible
});
Expand Down
2 changes: 1 addition & 1 deletion lib/SyncHook.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
Expand Down
2 changes: 1 addition & 1 deletion lib/SyncLoopHook.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

class SyncLoopHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsLooping({
onError: (i, err) => onError(err),
onDone,
Expand Down
3 changes: 2 additions & 1 deletion lib/SyncWaterfallHook.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

class SyncWaterfallHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
content({ onError, onResult, resultReturns, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onResult: (i, result, next) => {
Expand All @@ -20,6 +20,7 @@ class SyncWaterfallHookCodeFactory extends HookCodeFactory {
return code;
},
onDone: () => onResult(this._args[0]),
doneReturns: resultReturns,
rethrowIfPossible
});
}
Expand Down
13 changes: 13 additions & 0 deletions lib/__tests__/HookCodeFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ const expectNoSyntaxError = code => {
new Function("a, b, c", code);
};

const pretty = code => {
return require("prettier").format(code, { filepath: "code.js" });
};

describe("HookCodeFactory", () => {
describe("callTap", () => {
const factoryConfigurations = {
Expand Down Expand Up @@ -83,6 +87,7 @@ describe("HookCodeFactory", () => {
onDone: () => "onDone();\n"
});
expect(code).toMatchSnapshot();
expect(pretty(code)).toMatchSnapshot();
expectNoSyntaxError(code);
});
it("sync with onResult", () => {
Expand All @@ -91,6 +96,7 @@ describe("HookCodeFactory", () => {
onResult: result => `onResult(${result});\n`
});
expect(code).toMatchSnapshot();
expect(pretty(code)).toMatchSnapshot();
expectNoSyntaxError(code);
});
it("async without onResult", () => {
Expand All @@ -99,6 +105,7 @@ describe("HookCodeFactory", () => {
onDone: () => "onDone();\n"
});
expect(code).toMatchSnapshot();
expect(pretty(code)).toMatchSnapshot();
expectNoSyntaxError(code);
});
it("async with onResult", () => {
Expand All @@ -107,6 +114,7 @@ describe("HookCodeFactory", () => {
onResult: result => `onResult(${result});\n`
});
expect(code).toMatchSnapshot();
expect(pretty(code)).toMatchSnapshot();
expectNoSyntaxError(code);
});
it("promise without onResult", () => {
Expand All @@ -115,6 +123,7 @@ describe("HookCodeFactory", () => {
onDone: () => "onDone();\n"
});
expect(code).toMatchSnapshot();
expect(pretty(code)).toMatchSnapshot();
expectNoSyntaxError(code);
});
it("promise with onResult", () => {
Expand All @@ -123,6 +132,7 @@ describe("HookCodeFactory", () => {
onResult: result => `onResult(${result});\n`
});
expect(code).toMatchSnapshot();
expect(pretty(code)).toMatchSnapshot();
expectNoSyntaxError(code);
});
});
Expand Down Expand Up @@ -224,6 +234,7 @@ describe("HookCodeFactory", () => {
rethrowIfPossible: true
});
expect(code).toMatchSnapshot();
expect(pretty(code)).toMatchSnapshot();
expectNoSyntaxError(code);
});
it("callTapsParallel", () => {
Expand All @@ -235,6 +246,7 @@ describe("HookCodeFactory", () => {
rethrowIfPossible: true
});
expect(code).toMatchSnapshot();
expect(pretty(code)).toMatchSnapshot();
expectNoSyntaxError(code);
});
it("callTapsLooping", () => {
Expand All @@ -244,6 +256,7 @@ describe("HookCodeFactory", () => {
rethrowIfPossible: true
});
expect(code).toMatchSnapshot();
expect(pretty(code)).toMatchSnapshot();
expectNoSyntaxError(code);
});
});
Expand Down
15 changes: 15 additions & 0 deletions lib/__tests__/HookStackOverflow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const AsyncSeriesHook = require("../AsyncSeriesHook");

describe("HookStackOverflow", () => {
it("should not crash when compiling a large hook", () => {
const hook = new AsyncSeriesHook(["a", "b"]);

for (let i = 0; i < 10; i++) {
hook.tap("TestPlugin", (a, b) => {});
hook.tapAsync("TestPlugin", (a, b, callback) => callback());
hook.tapPromise("TestPlugin", (a, b) => Promise.resolve());
}

return hook.promise(1, 2);
});
});
Loading

0 comments on commit 32afdc5

Please sign in to comment.