Repository: eslint/eslint (JavaScript)

Issue:
curly 'multi' autofix produces syntax errors with lexical declarations 
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).

    This template is for bug reports. If you are here for another reason, please see below:

    1. To propose a new rule: https://eslint.org/docs/developer-guide/contributing/new-rules
    2. To request a rule change: https://eslint.org/docs/developer-guide/contributing/rule-changes
    3. To request a change that is not a bug fix, rule change, or new rule: https://eslint.org/docs/developer-guide/contributing/changes
    4. If you have any questions, please stop by our chatroom: https://gitter.im/eslint/eslint

    Note that leaving sections blank will make it difficult for us to troubleshoot and we may have to close the issue.
-->

**Tell us about your environment**

* **ESLint Version:**  6.0.1 (same with 5.16.0)
* **Node Version:** 10.16.0
* **npm Version:** 6.9.0

**What parser (default, Babel-ESLint, etc.) are you using?**

default

**Please show your full configuration:**

<details>
<summary>Configuration</summary>

<!-- Paste your configuration below: -->
```js
module.exports = {
  parserOptions: {
    ecmaVersion: 6,
  },
  rules: {
    "curly": ["error", "multi"]
  }
};
```

</details>

**What did you do? Please include the actual source code causing the issue, as well as the command that you used to run ESLint.**

<!-- Paste the source code below: -->
```js
if (foo) {
  let bar;
}
```

<!-- Paste the command you used to run ESLint: -->
```bash
eslint index.js --fix
```

**What did you expect to happen?**

No errors, similar to how `multi-or-nest` works.

**What actually happened? Please include the actual, raw output from ESLint.**

```js
if (foo) 
  let bar; // SyntaxError: Lexical declaration cannot appear in a single-statement context
```

**Are you willing to submit a pull request to fix this bug?**

Yes.



Discussion:
This was recently fixed for `multi-or-nest` (#11663) but not for `multi`, which is another option that can remove braces.

I guess the solution should be the same - don't report.
That makes sense to me. I was able to verify this in our demo. Just for clarity, this would also apply to 
```js
if (foo) {
  const bar = true;
}
```
correct?
Yes, and `class` and `function` shouldn't be fixed as well.

So: `const`, `let`, `class` and `function`

First three are syntax errors. 

`function` might depend on the parser, I'll check it more, but it certainly isn't a good idea to move it up from braces.
I'm not sure how the spec treats these two cases:

* `function foo () { if (bar) function a() {} }`
* `function foo () { if (bar) { function a() {} } }`

In Chrome it seems that the names `a` are hoisted, but values not, Value is  `undefined` until the execution reaches the declaration.

```js
const a = 1;
function f() { return a; if (false) { function a() {}  } }
f(); // undefined, 'a' is shadowed
```

```js
const a = 1;
function f() { if (true) { function a() {}  }; return a;  }
f(); // 'ƒ a() {}'
```
When I try the same examples without braces after `if`, the results are the same. 

But perhaps it wouldn't be like that in other engines, and maybe it still isn't a good idea to remove or put braces around function declarations in the fix.

By the way, ESLint's scope manager treats nested function declarations as block scoped like `let`, which doesn't match this behavior in Chrome.
This might be also a bug: with the default `all` option, `curly` would put braces around the declaration:

```js
if (foo) function bar() {}
```
Sorry for the long thread. 

To summarize, these are the only blocking questions related directly to this issue and the `curly` rule:

1. Should the rule ever transform this:

```js
if (foo) {
    function f() {}
}
```

into this:

```js
if (foo) 
    function f() {}
```

2. Should the rule ever transform this:

```js
if (foo) 
    function f() {}
```

into this:

```js
if (foo) {
    function f() {}
}
```
Function scopes aren't lexical, right? They're hoisted to the top of the function scope, right? And having a function declaration as part of an if statement's blockless consequent is not a syntax error?

If I'm understanding that correctly, I would say the rule could safely autofix if there are functions inside the if statement.
To add another wrinkle to this, it looks like strict mode can affect whether this is a syntax error or not:

This [yields a syntax error](https://eslint.org/demo/#eyJ0ZXh0IjoiJ3VzZSBzdHJpY3QnXG5cbmlmIChmb28pIFxuICAgIGZ1bmN0aW9uIGYoKSB7fSIsIm9wdGlvbnMiOnsicGFyc2VyT3B0aW9ucyI6eyJlY21hVmVyc2lvbiI6MTAsInNvdXJjZVR5cGUiOiJzY3JpcHQiLCJlY21hRmVhdHVyZXMiOnt9fSwicnVsZXMiOnsiY29uc3RydWN0b3Itc3VwZXIiOjIsImZvci1kaXJlY3Rpb24iOjIsImdldHRlci1yZXR1cm4iOjIsIm5vLWFzeW5jLXByb21pc2UtZXhlY3V0b3IiOjIsIm5vLWNhc2UtZGVjbGFyYXRpb25zIjoyLCJuby1jbGFzcy1hc3NpZ24iOjIsIm5vLWNvbXBhcmUtbmVnLXplcm8iOjIsIm5vLWNvbmQtYXNzaWduIjoyLCJuby1jb25zdC1hc3NpZ24iOjIsIm5vLWNvbnN0YW50LWNvbmRpdGlvbiI6Miwibm8tY29udHJvbC1yZWdleCI6Miwibm8tZGVidWdnZXIiOjIsIm5vLWRlbGV0ZS12YXIiOjIsIm5vLWR1cGUtYXJncyI6Miwibm8tZHVwZS1jbGFzcy1tZW1iZXJzIjoyLCJuby1kdXBlLWtleXMiOjIsIm5vLWR1cGxpY2F0ZS1jYXNlIjoyLCJuby1lbXB0eSI6Miwibm8tZW1wdHktY2hhcmFjdGVyLWNsYXNzIjoyLCJuby1lbXB0eS1wYXR0ZXJuIjoyLCJuby1leC1hc3NpZ24iOjIsIm5vLWV4dHJhLWJvb2xlYW4tY2FzdCI6Miwibm8tZXh0cmEtc2VtaSI6Miwibm8tZmFsbHRocm91Z2giOjIsIm5vLWZ1bmMtYXNzaWduIjoyLCJuby1nbG9iYWwtYXNzaWduIjoyLCJuby1pbm5lci1kZWNsYXJhdGlvbnMiOjIsIm5vLWludmFsaWQtcmVnZXhwIjoyLCJuby1pcnJlZ3VsYXItd2hpdGVzcGFjZSI6Miwibm8tbWlzbGVhZGluZy1jaGFyYWN0ZXItY2xhc3MiOjIsIm5vLW1peGVkLXNwYWNlcy1hbmQtdGFicyI6Miwibm8tbmV3LXN5bWJvbCI6Miwibm8tb2JqLWNhbGxzIjoyLCJuby1vY3RhbCI6Miwibm8tcHJvdG90eXBlLWJ1aWx0aW5zIjoyLCJuby1yZWRlY2xhcmUiOjIsIm5vLXJlZ2V4LXNwYWNlcyI6Miwibm8tc2VsZi1hc3NpZ24iOjIsIm5vLXNoYWRvdy1yZXN0cmljdGVkLW5hbWVzIjoyLCJuby1zcGFyc2UtYXJyYXlzIjoyLCJuby10aGlzLWJlZm9yZS1zdXBlciI6Miwibm8tdW5kZWYiOjIsIm5vLXVuZXhwZWN0ZWQtbXVsdGlsaW5lIjoyLCJuby11bnJlYWNoYWJsZSI6Miwibm8tdW5zYWZlLWZpbmFsbHkiOjIsIm5vLXVuc2FmZS1uZWdhdGlvbiI6Miwibm8tdW51c2VkLWxhYmVscyI6Miwibm8tdW51c2VkLXZhcnMiOjIsIm5vLXVzZWxlc3MtY2F0Y2giOjIsIm5vLXVzZWxlc3MtZXNjYXBlIjoyLCJuby13aXRoIjoyLCJyZXF1aXJlLWF0b21pYy11cGRhdGVzIjoyLCJyZXF1aXJlLXlpZWxkIjoyLCJ1c2UtaXNuYW4iOjIsInZhbGlkLXR5cGVvZiI6Mn0sImVudiI6e319fQ==):
```js
'use strict'

if (foo) 
    function f() {}
```

This [does not](https://eslint.org/demo/#eyJ0ZXh0IjoiaWYgKGZvbykgXG4gICAgZnVuY3Rpb24gZigpIHt9Iiwib3B0aW9ucyI6eyJwYXJzZXJPcHRpb25zIjp7ImVjbWFWZXJzaW9uIjoxMCwic291cmNlVHlwZSI6InNjcmlwdCIsImVjbWFGZWF0dXJlcyI6e319LCJydWxlcyI6eyJjb25zdHJ1Y3Rvci1zdXBlciI6MiwiZm9yLWRpcmVjdGlvbiI6MiwiZ2V0dGVyLXJldHVybiI6Miwibm8tYXN5bmMtcHJvbWlzZS1leGVjdXRvciI6Miwibm8tY2FzZS1kZWNsYXJhdGlvbnMiOjIsIm5vLWNsYXNzLWFzc2lnbiI6Miwibm8tY29tcGFyZS1uZWctemVybyI6Miwibm8tY29uZC1hc3NpZ24iOjIsIm5vLWNvbnN0LWFzc2lnbiI6Miwibm8tY29uc3RhbnQtY29uZGl0aW9uIjoyLCJuby1jb250cm9sLXJlZ2V4IjoyLCJuby1kZWJ1Z2dlciI6Miwibm8tZGVsZXRlLXZhciI6Miwibm8tZHVwZS1hcmdzIjoyLCJuby1kdXBlLWNsYXNzLW1lbWJlcnMiOjIsIm5vLWR1cGUta2V5cyI6Miwibm8tZHVwbGljYXRlLWNhc2UiOjIsIm5vLWVtcHR5IjoyLCJuby1lbXB0eS1jaGFyYWN0ZXItY2xhc3MiOjIsIm5vLWVtcHR5LXBhdHRlcm4iOjIsIm5vLWV4LWFzc2lnbiI6Miwibm8tZXh0cmEtYm9vbGVhbi1jYXN0IjoyLCJuby1leHRyYS1zZW1pIjoyLCJuby1mYWxsdGhyb3VnaCI6Miwibm8tZnVuYy1hc3NpZ24iOjIsIm5vLWdsb2JhbC1hc3NpZ24iOjIsIm5vLWlubmVyLWRlY2xhcmF0aW9ucyI6Miwibm8taW52YWxpZC1yZWdleHAiOjIsIm5vLWlycmVndWxhci13aGl0ZXNwYWNlIjoyLCJuby1taXNsZWFkaW5nLWNoYXJhY3Rlci1jbGFzcyI6Miwibm8tbWl4ZWQtc3BhY2VzLWFuZC10YWJzIjoyLCJuby1uZXctc3ltYm9sIjoyLCJuby1vYmotY2FsbHMiOjIsIm5vLW9jdGFsIjoyLCJuby1wcm90b3R5cGUtYnVpbHRpbnMiOjIsIm5vLXJlZGVjbGFyZSI6Miwibm8tcmVnZXgtc3BhY2VzIjoyLCJuby1zZWxmLWFzc2lnbiI6Miwibm8tc2hhZG93LXJlc3RyaWN0ZWQtbmFtZXMiOjIsIm5vLXNwYXJzZS1hcnJheXMiOjIsIm5vLXRoaXMtYmVmb3JlLXN1cGVyIjoyLCJuby11bmRlZiI6Miwibm8tdW5leHBlY3RlZC1tdWx0aWxpbmUiOjIsIm5vLXVucmVhY2hhYmxlIjoyLCJuby11bnNhZmUtZmluYWxseSI6Miwibm8tdW5zYWZlLW5lZ2F0aW9uIjoyLCJuby11bnVzZWQtbGFiZWxzIjoyLCJuby11bnVzZWQtdmFycyI6Miwibm8tdXNlbGVzcy1jYXRjaCI6Miwibm8tdXNlbGVzcy1lc2NhcGUiOjIsIm5vLXdpdGgiOjIsInJlcXVpcmUtYXRvbWljLXVwZGF0ZXMiOjIsInJlcXVpcmUteWllbGQiOjIsInVzZS1pc25hbiI6MiwidmFsaWQtdHlwZW9mIjoyfSwiZW52Ijp7fX19):
```js
if (foo) 
    function f() {}
```

I'm trying to find where this is defined in the spec without much luck (hopefully someone else can find it!), but some relevant resources I found:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode#Paving_the_way_for_future_ECMAScript_versions
* http://whereswalden.com/2011/01/24/new-es5-strict-mode-requirement-function-statements-not-at-top-level-of-a-program-or-function-are-prohibited/

The above two articles make it seem like different browser engines might treat them differently, as well.
Another resource:

* [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function#Conditionally_created_functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function#Conditionally_created_functions)
It's most likely a bad practice to have a function declaration anywhere other than directly in the body.

Perhaps ESLint should never modify such code, i.e. never move function declarations to `{}` or out from its existing `{}`, because it can produce some unpredictable differences even if it isn't a syntax error.
Agreed - better to play it safe than potentially do something unsafe.
Ok, I'm working on this. 

It might take some time, all options have to be checked and the `consistent` modifier adds some complexity.

Unfortunately, it looks like there wasn't enough interest from the team
or community to implement this change. While we wish we'd be able to
accommodate everyone's requests, we do need to prioritize. We've found
that accepted issues failing to be implemented after 90 days tend to
never be implemented, and as such, we [close those issues](https://eslint.org/docs/maintainer-guide/issues#when-to-close-an-issue).
This doesn't mean the idea isn't interesting or useful, just that it's
not something the team can commit to.

Thanks for contributing to ESLint and we appreciate your understanding.

[//]: # (auto-close)

Reopening this since we have a PR for it!

Unfortunately, it looks like there wasn't enough interest from the team
or community to implement this change. While we wish we'd be able to
accommodate everyone's requests, we do need to prioritize. We've found
that accepted issues failing to be implemented after 90 days tend to
never be implemented, and as such, we [close those issues](https://eslint.org/docs/maintainer-guide/issues#when-to-close-an-issue).
This doesn't mean the idea isn't interesting or useful, just that it's
not something the team can commit to.

Thanks for contributing to ESLint and we appreciate your understanding.

[//]: # (auto-close)

@mdjermanovic would you like to assign this to yourself so it doesn't get autoclosed?
Yes, and I think it might be better to just fix the error from the original post at the moment.

The assumption that it isn't safe to convert `if (foo) function f(){}` to `if (foo) { function f(){} }` (which makes problems in PR #11912) might be wrong.

From the specification [B.3.4 FunctionDeclarations in IfStatement Statement Clauses](https://tc39.es/ecma262/#sec-functiondeclarations-in-ifstatement-statement-clauses) it seems that a function as a single-statement in `if` is treated as if it was in a block statement:

> Code matching this production is processed as if each matching occurrence of FunctionDeclaration was the sole StatementListItem of a BlockStatement occupying that position in the source code.

So I plan to prepare another PR instead of #11912, which would only prevent removing braces around `function`, `class`, `let` and `const`, with a min. diff.



Patch:
diff --git a/lib/rules/curly.js b/lib/rules/curly.js
index 93c74d11fcf3..21a5d4444cc3 100644
--- a/lib/rules/curly.js
+++ b/lib/rules/curly.js
@@ -240,7 +240,7 @@ module.exports = {
             if (node.type === "IfStatement" && node.consequent === body && requiresBraceOfConsequent(node)) {
                 expected = true;
             } else if (multiOnly) {
-                if (hasBlock && body.body.length === 1) {
+                if (hasBlock && body.body.length === 1 && !isLexicalDeclaration(body.body[0])) {
                     expected = false;
                 }
             } else if (multiLine) {


Test patch:
diff --git a/tests/lib/rules/curly.js b/tests/lib/rules/curly.js
index b9f0f0890f34..c21e528f81c0 100644
--- a/tests/lib/rules/curly.js
+++ b/tests/lib/rules/curly.js
@@ -164,6 +164,35 @@ ruleTester.run("curly", rule, {
             options: ["multi-or-nest"],
             parserOptions: { ecmaVersion: 6 }
         },
+        {
+            code: "if (foo) { const bar = 'baz'; }",
+            options: ["multi"],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: "while (foo) { let bar = 'baz'; }",
+            options: ["multi"],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: "for(;;) { function foo() {} }",
+            options: ["multi"]
+        },
+        {
+            code: "for (foo in bar) { class Baz {} }",
+            options: ["multi"],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: "if (foo) { let bar; } else { baz(); }",
+            options: ["multi", "consistent"],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: "if (foo) { bar(); } else { const baz = 'quux'; }",
+            options: ["multi", "consistent"],
+            parserOptions: { ecmaVersion: 6 }
+        },
         {
             code: "if (foo) { \n const bar = 'baz'; \n }",
             options: ["multi-or-nest"],
@@ -616,6 +645,44 @@ ruleTester.run("curly", rule, {
                 }
             ]
         },
+        {
+            code: "if (foo) { var bar = 'baz'; }",
+            output: "if (foo)  var bar = 'baz'; ",
+            options: ["multi"],
+            errors: [
+                {
+                    messageId: "unexpectedCurlyAfterCondition",
+                    data: { name: "if" },
+                    type: "IfStatement"
+                }
+            ]
+        },
+        {
+            code: "if (foo) { let bar; } else baz();",
+            output: "if (foo) { let bar; } else {baz();}",
+            options: ["multi", "consistent"],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                {
+                    messageId: "missingCurlyAfter",
+                    data: { name: "else" },
+                    type: "IfStatement"
+                }
+            ]
+        },
+        {
+            code: "if (foo) bar(); else { const baz = 'quux' }",
+            output: "if (foo) {bar();} else { const baz = 'quux' }",
+            options: ["multi", "consistent"],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                {
+                    messageId: "missingCurlyAfterCondition",
+                    data: { name: "if" },
+                    type: "IfStatement"
+                }
+            ]
+        },
         {
             code: "if (foo) { \n var bar = 'baz'; \n }",
             output: "if (foo)  \n var bar = 'baz'; \n ",