Skip to content

Eliminate unreachable branches#6273

Merged
sokra merged 2 commits intowebpack:nextfrom
ooflorent:fix-4857/unreachable_branches
Jan 12, 2018
Merged

Eliminate unreachable branches#6273
sokra merged 2 commits intowebpack:nextfrom
ooflorent:fix-4857/unreachable_branches

Conversation

@ooflorent
Copy link
Member

@ooflorent ooflorent commented Jan 9, 2018

What kind of change does this PR introduce?

bugfix

Did you add tests for your changes?

yes

If relevant, link to documentation update:

n/a

Summary

Webpack does not transpile unreachable branches leaving import expressions
in the code. This PR modifies ConstPlugin to remove the unreachable branch.

Before:

if (true) { 1 } else { import("a") }
if (false) { import("a") } else { 1 }
true ? 1 : import("a");
false ? import("a") : 1;

After:

if (true) { 1 } else {}
if (false) {} else { 1 }
true ? 1 : null;
false ? null : 1;

Does this PR introduce a breaking change?

no

Other information

Fixes #4857.
Fixes #5663.

@sokra
Copy link
Member

sokra commented Jan 9, 2018

if(false) {
  function test() {}
}
test();

@ooflorent
Copy link
Member Author

ooflorent commented Jan 9, 2018

Hmm. You're right I totally forgot hoisting. Then I guess we cannot entirely skip node traversal in Parser. What's the right way then?

@sokra
Copy link
Member

sokra commented Jan 9, 2018

Not sure. You can walk each statement in unreachable branches and remove by statement type. Remove statements, keep and parse Declarations, enter and remove control statements (if, while, do).


While on this you can fix this bug:

if(false) {
  do { while(false) { for(;;) {
  function test() {
    require("./file"); // <- not processed by webpack
  }
  }}} while(false);
}
test();

@ooflorent
Copy link
Member Author

What about this:

if (false) {
  function f() {
    return import("./x")
  }
}
f()

Here is the problem, if import() appears somewhere in the output then it will fail. What should I do for this case?

@sokra
Copy link
Member

sokra commented Jan 9, 2018

walkDeadStatements walkDeadStatement walkDeadIfStatement walkDeadDoWhileStatement walkDeadWhileStatement walkDeadForStatement walkDeadForInStatement walkDeadForOfStatement walkDeadBlockStatement

@sokra
Copy link
Member

sokra commented Jan 9, 2018

What about this:

if (false) {
function f() {
return import("./x")
}
}
f()
Here is the problem, if import() appears somewhere in the output then it will fail. What should I do for this case?

Start walking FunctionDeclarations again. It's actually a bug that each are not walked. The code is available on runtime.

@ooflorent
Copy link
Member Author

ooflorent commented Jan 11, 2018

@sokra It's even more trickier than I initially though:

(function() {
  console.log("f", f) // "g" undefined
  console.log("g", g) // TypeError
  if (false) {
    function f() {}
  }
})()

(function() {
  "use strict"
  console.log("f", f) // TypeError
  if (false) {
    function f() {}
  }
})()

(function() {
  "use strict";
  (function() {
    console.log("f", f)  // TypeError
    if (false) {
      function f() {}
    }
  })()
})()

Since the result depends on strict mode, it is really hard to handle. Moreover, strict seems to propagate.

@ooflorent
Copy link
Member Author

If webpack automatically adds use strict then I think it is safe to remove the dead branches as initially implemented in this PR.

@sokra
Copy link
Member

sokra commented Jan 11, 2018

If webpack automatically adds use strict then I think it is safe to remove the dead branches as initially implemented in this PR.

Only for ESM, other module types stay in it's original mode.

@sokra
Copy link
Member

sokra commented Jan 11, 2018

Oh I tried this and it's actually more simple than expected. Functions doesn't hoist out of the block scope. Only the variable declaration hoists.

(function() { function g() {} return g; }()) => [Function]
(function() { if(true) {function g() {}} return g; }()) => [Function]
(function() { if(false) {function g() {}} return g; }()) => undefined
(function() { if(false) {} return g; }()) => g is not defined

So the solution is pretty simple. You don't need to parse any code instead of the dead scope. You can remove all code, but insert var xxx for FunctionDeclarations:

if(false) {
  while (true) {
    function foo() {}
  }
}
var x = foo;

=>

var foo;
var x = foo;

@ooflorent
Copy link
Member Author

@sokra I'm working on it using strict mode propagation. It's pretty simple. I'll update the PR in few minutes

@ooflorent
Copy link
Member Author

ooflorent commented Jan 11, 2018

@sokra It will be a little longer. Here is the algorithm I'm implementing:

  • Track isStrict in scope while walking the AST
  • On a dead branch:
    • If isStrict = true then remove the branch
    • Otherwise walk the child nodes
      • Store VariableDeclaration and FunctionDeclaration ids
      • Loop through ids and generate a VariableDeclation string representation
      • Replace the branch with it

Here below is a rough implementation:

x

@sokra
Copy link
Member

sokra commented Jan 11, 2018

If isStrict = true then remove the branch

(function() { "use strict"; if(false) { var foo; } return foo; }()) => undefined

So you need to walk the block in strict mode too to keep VarDeclarations... Only FunctionDeclarations change their behavior in strict mode... (JS is weird)

@ooflorent ooflorent force-pushed the fix-4857/unreachable_branches branch 2 times, most recently from 6b9d34d to 48a1d44 Compare January 11, 2018 14:53
@ooflorent
Copy link
Member Author

@sokra Finally I'm done with this. I hope.

Copy link
Member

@sokra sokra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

parserInstance.state.module.buildInfo.strict = true;
parser.state.current.addDependency(dep);
parser.state.module.buildInfo.strict = true;
parser.scope.isStrict = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seem to be a bit missplaced here. All logic for detecting parser.scope.isStrict should be in Parser. Better remove this and add a call to this.detectStrictMode(statement.body.body); after if(this.hooks.program.call(ast, comments) === undefined) { in Parser.parse.

isStrict: false,
definitions: new StackedSetMap(),
renames: new StackedSetMap()
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In HarmonyDetectionsPlugin add a parser.scope.isStrict = true, because all ESM modules are in strict mode automatically.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Webpack does not transpile unreachable branches leaving `import` expressions
in the code. This PR modifies `ConstPlugin` to remove the unreachable branch.

Before:

```js
if (true) { 1 } else { import("a") }
if (false) { import("a") } else { 1 }
true ? 1 : import("a");
false ? import("a") : 1;
```

After:

```js
if (true) { 1 } else {}
if (false) {} else { 1 }
true ? 1 : null;
false ? null : 1;
```
@ooflorent ooflorent force-pushed the fix-4857/unreachable_branches branch from 48a1d44 to c9ff97f Compare January 11, 2018 18:35
@webpack-bot
Copy link
Contributor

@ooflorent Thanks for your update.

I labeled the Pull Request so reviewers will review it again.

@sokra Please review the new changes.

@codecov
Copy link

codecov bot commented Jan 11, 2018

Codecov Report

Merging #6273 into next will decrease coverage by <.01%.
The diff coverage is 91.08%.

Impacted file tree graph

@@            Coverage Diff             @@
##             next    #6273      +/-   ##
==========================================
- Coverage   92.75%   92.74%   -0.01%     
==========================================
  Files         303      303              
  Lines       14400    14498      +98     
  Branches     3095     3126      +31     
==========================================
+ Hits        13356    13446      +90     
- Misses       1044     1052       +8
Flag Coverage Δ
#integration 91.4% <91.08%> (ø) ⬆️
#unit 43.66% <80%> (+0.07%) ⬆️
Impacted Files Coverage Δ
lib/Parser.js 94.68% <100%> (+0.04%) ⬆️
lib/UseStrictPlugin.js 100% <100%> (ø) ⬆️
lib/ConstPlugin.js 92.96% <89.77%> (-7.04%) ⬇️
lib/ContextModule.js 87.93% <0%> (+0.34%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e3c2f73...c9ff97f. Read the comment docs.

Copy link
Member

@sokra sokra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look good 👍

If you want to go for 100% test coverage, there are 3 items left.

if(element) stack.push(element);
break;
case "AssignmentPattern":
stack.push(node.left);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Untested

stack.push(node.body);
break;
case "SwitchStatement":
for(const cs of node.cases)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Untested

stack.push(node.left);
stack.push(node.body);
break;
case "DoWhileStatement":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DoWhile -> Untested

@ooflorent
Copy link
Member Author

@sokra I've added more tests. Now everything should be covered.

@sokra
Copy link
Member

sokra commented Jan 12, 2018

@sokra I've added more tests. Now everything should be covered.

👍

@webpack-bot
Copy link
Contributor

Thank you for your pull request! The most important CI builds succeeded, we’ll review the pull request soon.

@sokra sokra merged commit e6fb58d into webpack:next Jan 12, 2018
@sokra
Copy link
Member

sokra commented Jan 12, 2018

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants