babel / babel/babel

[Bug]: Function declarations not hoisted to upper scope in sloppy mode code

Open
#13,549 2 comments 0 reactions 0 assignees View on GitHub
pkg: traverse (scope)
Dominant language
TypeScript
Stars
44k
Forks
6k
Avg merge
5d 15h
Merged PRs (30d)
23

Description

### 💻

- [X] Would you like to work on a fix?

### How are you using Babel?

Programmatic API (`babel.transform`, `babel.parse`)

### Input code

```js
{
function f() {}
}
f();
```

### Configuration file name

babel.config.json

### Configuration

n/a

### Current and expected behavior

In strict mode, function declarations are scoped to the statement block in which they're defined (i.e. behave like `let`). In sloppy mode they are scoped to the next enclosing function / top-level (i.e. behave like `var`).

This code is valid in sloppy mode and will execute without an error:

```js
{
function f() {}
}
f();
```

Babel appears not to take strict/sloppy mode into account, and applies the strict mode behavior regardless.

```js
const srcCode = `
{
function f() {}
}
f();
`;

const { code } = transformSync( srcCode, {
sourceType: 'script',
plugins: [
() => ( {
visitor: {
Identifier( path ) {
const binding = path.scope.getBinding( path.node.name );
if ( !binding ) path.node.name = `unreferenced_${ path.node.name }`;
}
}
} )
]
} );
console.log(code);
```

This outputs:

```js
{
function f() {}
}
unreferenced_f();
```

i.e. `path.scope.getBinding('f')` did not identify that `function f` is in scope of `f()`.

NB As far as I can see from testing in NodeJS, the function declaration's scope is hoisted up to enclosing function's scope/top-level scope if its *surrounding environment* is sloppy mode, regardless of whether the function itself is strict. i.e. `f` is still defined in top level scope in this case:

```js
// Sloppy mode
{
function f() {
'use strict';
}
}
f();
```

### Environment

```
System:
OS: macOS 10.15.7
Binaries:
Node: 16.4.2 - ~/.nvm/versions/node/v16.4.2/bin/node
Yarn: 1.22.10 - ~/.nvm/versions/node/v16.4.2/bin/yarn
npm: 7.18.1 - ~/.nvm/versions/node/v16.4.2/bin/npm
npmPackages:
@babel/core: ^7.14.6 => 7.14.6
@babel/generator: ^7.14.5 => 7.14.5
@babel/helper-module-transforms: ^7.14.5 => 7.14.5
@babel/parser: ^7.14.7 => 7.14.7
@babel/plugin-transform-arrow-functions: ^7.14.5 => 7.14.5
@babel/plugin-transform-modules-commonjs: ^7.14.5 => 7.14.5
@babel/plugin-transform-react-jsx: ^7.14.5 => 7.14.5
@babel/plugin-transform-strict-mode: ^7.14.5 => 7.14.5
@babel/register: ^7.14.5 => 7.14.5
@babel/traverse: ^7.14.7 => 7.14.7
@babel/types: ^7.14.5 => 7.14.5
babel-jest: ^27.0.6 => 27.0.6
babel-plugin-dynamic-import-node: ^2.3.3 => 2.3.3
eslint: ^7.30.0 => 7.30.0
jest: ^27.0.6 => 27.0.6
```

### Possible solution

_No response_

### Additional context

I'm happy to work on a fix but would appreciate it if someone can point me in right direction. Last time I looked at the scope logic in Babel, I had trouble getting my head around it.

Contributor guide

Open the contributing guide

Research direction

Reproduce the behavior with the issue's transformSync example using sourceType: 'script', focusing on path.scope.getBinding('f') for the block function and later call. Read Babel's scope logic and compare binding resolution in sloppy and strict contexts; done means the outer f() resolves to the block declaration only in sloppy mode, while strict mode remains block-scoped.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.