Repository: sindresorhus/eslint-plugin-unicorn (JavaScript)

Issue:
`no-array-for-each` invalid fix
Consider foreach calls some logic as return side-effect:
```js
function handleTwo() {}
function handleNotTwo() {}

[ 1, 2, 3, 4 ].forEach(item => {
    if (item === 2) return handleTwo();
    handleNotTwo();
});
```

after fixing by ```no-array-for-each``` next snippet is generated:

```js
function handleTwo() {}
function handleNotTwo() {}

for (const item of [ 1, 2, 3, 4 ]) {
    if (item === 2)  handleTwo(); continue;
    handleNotTwo();
}
```

here ```handleTwo``` is being called in case item equals 2 as expected, but then continue is called out of if scope.
as a result  ```handleNotTwo```  becomes  unreachable, and snippet **works in different way** then initial  forEach.

Consider addding scope here should work as expected:

```return handleTwo();``` => ```{ handleTwo(); continue; }```

so snippet becomes:
```js
function handleTwo() {}
function handleNotTwo() {}

for (const item of [ 1, 2, 3, 4 ]) {
    if (item === 2)  { handleTwo(); continue; }
    handleNotTwo();
}
```



Discussion:
Thanks for reporting this.



Patch:
diff --git a/rules/no-array-for-each.js b/rules/no-array-for-each.js
index a24f37e3cf..dde620a639 100644
--- a/rules/no-array-for-each.js
+++ b/rules/no-array-for-each.js
@@ -46,6 +46,27 @@ function isReturnStatementInContinueAbleNodes(returnStatement, callbackFunction)
 	return false;
 }
 
+function shouldSwitchReturnStatementToBlockStatement(returnStatement) {
+	const {parent} = returnStatement;
+
+	switch (parent.type) {
+		case 'IfStatement':
+			return parent.consequent === returnStatement || parent.alternate === returnStatement;
+
+		// These parent's body need switch to `BlockStatement` too, but since they are "continueAble", won't fix
+		// case 'ForStatement':
+		// case 'ForInStatement':
+		// case 'ForOfStatement':
+		// case 'WhileStatement':
+		// case 'DoWhileStatement':
+		case 'WithStatement':
+			return parent.body === returnStatement;
+
+		default:
+			return false;
+	}
+}
+
 function getFixFunction(callExpression, functionInfo, context) {
 	const sourceCode = context.getSourceCode();
 	const [callback] = callExpression.arguments;
@@ -118,12 +139,14 @@ function getFixFunction(callExpression, functionInfo, context) {
 			!isParenthesized(returnStatement.argument, sourceCode) &&
 			shouldAddParenthesesToExpressionStatementExpression(returnStatement.argument);
 		if (shouldAddParentheses) {
-			textBefore = '(';
-			textAfter = ')';
+			textBefore = `(${textBefore}`;
+			textAfter = `${textAfter})`;
 		}
 
-		const shouldAddSemicolonBefore = needsSemicolon(previousToken, sourceCode, shouldAddParentheses ? '(' : nextToken.value);
-		if (shouldAddSemicolonBefore) {
+		const insertBraces = shouldSwitchReturnStatementToBlockStatement(returnStatement);
+		if (insertBraces) {
+			textBefore = `{ ${textBefore}`;
+		} else if (needsSemicolon(previousToken, sourceCode, shouldAddParentheses ? '(' : nextToken.value)) {
 			textBefore = `;${textBefore}`;
 		}
 
@@ -135,12 +158,16 @@ function getFixFunction(callExpression, functionInfo, context) {
 			yield fixer.insertTextAfter(returnStatement.argument, textAfter);
 		}
 
-		// If `returnStatement` has no semi
-		const lastToken = sourceCode.getLastToken(returnStatement);
-		yield fixer.insertTextAfter(
-			returnStatement,
-			`${isSemicolonToken(lastToken) ? '' : ';'} continue;`
-		);
+		const returnStatementHasSemicolon = isSemicolonToken(sourceCode.getLastToken(returnStatement));
+		if (!returnStatementHasSemicolon) {
+			yield fixer.insertTextAfter(returnStatement, ';');
+		}
+
+		yield fixer.insertTextAfter(returnStatement, ' continue;');
+
+		if (insertBraces) {
+			yield fixer.insertTextAfter(returnStatement, ' }');
+		}
 	}
 
 	const shouldRemoveExpressionStatementLastToken = token => {
diff --git a/rules/utils/needs-semicolon.js b/rules/utils/needs-semicolon.js
index 98d674d1ff..8d1c08ed8d 100644
--- a/rules/utils/needs-semicolon.js
+++ b/rules/utils/needs-semicolon.js
@@ -43,12 +43,44 @@ function needsSemicolon(tokenBefore, sourceCode, code) {
 	}
 
 	const {type, value, range} = tokenBefore;
+	const lastBlockNode = sourceCode.getNodeByRangeIndex(range[0]);
 	if (type === 'Punctuator') {
 		if (value === ';') {
 			return false;
 		}
 
-		if (value === ']' || value === ')') {
+		if (value === ']') {
+			return true;
+		}
+
+		if (value === ')') {
+			switch (lastBlockNode.type) {
+				case 'IfStatement': {
+					if (
+						sourceCode.getTokenBefore(lastBlockNode.consequent) === tokenBefore ||
+						(lastBlockNode.alternate && sourceCode.getTokenBefore(lastBlockNode.alternate) === tokenBefore)
+					) {
+						return false;
+					}
+
+					break;
+				}
+
+				case 'ForStatement':
+				case 'ForInStatement':
+				case 'ForOfStatement':
+				case 'WhileStatement':
+				case 'DoWhileStatement':
+				case 'WithStatement': {
+					if (lastBlockNode.body && sourceCode.getTokenBefore(lastBlockNode.body) === tokenBefore) {
+						return false;
+					}
+
+					break;
+				}
+				// No default
+			}
+
 			return true;
 		}
 	}
@@ -61,19 +93,18 @@ function needsSemicolon(tokenBefore, sourceCode, code) {
 		return value.endsWith('`');
 	}
 
-	const lastBlockNode = sourceCode.getNodeByRangeIndex(range[0]);
-	if (lastBlockNode && lastBlockNode.type === 'ObjectExpression') {
+	if (lastBlockNode.type === 'ObjectExpression') {
 		return true;
 	}
 
 	if (type === 'Identifier') {
 		// `for...of`
-		if (value === 'of' && lastBlockNode && lastBlockNode.type === 'ForOfStatement') {
+		if (value === 'of' && lastBlockNode.type === 'ForOfStatement') {
 			return false;
 		}
 
 		// `await`
-		if (value === 'await' && lastBlockNode && lastBlockNode.type === 'AwaitExpression') {
+		if (value === 'await' && lastBlockNode.type === 'AwaitExpression') {
 			return false;
 		}
 


Test patch:
diff --git a/test/no-array-for-each.mjs b/test/no-array-for-each.mjs
index 21d683d436..73a682877e 100644
--- a/test/no-array-for-each.mjs
+++ b/test/no-array-for-each.mjs
@@ -392,6 +392,19 @@ test.snapshot({
 			foo.forEach(({element}, index) => {
 				element &&= 2;
 			});
+		`,
+
+		// Need switch to `BlockStatement`, #1318
+		outdent`
+			foo.forEach(_ => {
+				if (true) return {};
+			})
+		`,
+		outdent`
+			foo.forEach(_ => {
+				if (true);
+				else return {};
+			})
 		`
 	]
 });
@@ -532,6 +545,20 @@ test({
 			code: 'return foo.forEach(element => {bar(element)});',
 			errors: 1,
 			parserOptions: globalReturnOptions
+		},
+		{
+			code: outdent`
+				foo.forEach(_ => {
+					with (a) return {};
+				})
+			`,
+			output: outdent`
+				for (const _ of foo) {
+					with (a)  { ({}); continue; }
+				}
+			`,
+			errors: 1,
+			parserOptions: globalReturnOptions
 		}
 	]
 });
diff --git a/test/snapshots/no-array-for-each.mjs.md b/test/snapshots/no-array-for-each.mjs.md
index f677f7573c..44b2fa74e9 100644
--- a/test/snapshots/no-array-for-each.mjs.md
+++ b/test/snapshots/no-array-for-each.mjs.md
@@ -1763,92 +1763,49 @@ Generated by [AVA](https://avajs.dev).
       3 | });␊
     `
 
-## Invalid #13
-      1 | foo.forEach(element => {
-      2 | 	do {
-      3 | 		return
-      4 | 	} while (element)
-      5 | });
-
-> Error 1/1
-
-    `␊
-    > 1 | foo.forEach(element => {␊
-        |     ^^^^^^^ Use \`for…of\` instead of \`Array#forEach(…)\`.␊
-      2 | 	do {␊
-      3 | 		return␊
-      4 | 	} while (element)␊
-      5 | });␊
-    `
-
-## Invalid #14
-      1 | foo.forEach(element => {
-      2 | 	while (element) {
-      3 | 		return;
-      4 | 	}
-      5 | });
+## Invalid #98
+      1 | foo.forEach(_ => {
+      2 | 	if (true) return {};
+      3 | })
 
-> Error 1/1
+> Output
 
     `␊
-    > 1 | foo.forEach(element => {␊
-        |     ^^^^^^^ Use \`for…of\` instead of \`Array#forEach(…)\`.␊
-      2 | 	while (element) {␊
-      3 | 		return;␊
-      4 | 	}␊
-      5 | });␊
+      1 | for (const _ of foo) {␊
+      2 | 	if (true)  { ({}); continue; }␊
+      3 | }␊
     `
 
-## Invalid #15
-      1 | foo.forEach(element => {
-      2 | 	for (let i = 0; i < 2; i++) {
-      3 | 		return;
-      4 | 	}
-      5 | });
-
 > Error 1/1
 
     `␊
-    > 1 | foo.forEach(element => {␊
+    > 1 | foo.forEach(_ => {␊
         |     ^^^^^^^ Use \`for…of\` instead of \`Array#forEach(…)\`.␊
-      2 | 	for (let i = 0; i < 2; i++) {␊
-      3 | 		return;␊
-      4 | 	}␊
-      5 | });␊
+      2 | 	if (true) return {};␊
+      3 | })␊
     `
 
-## Invalid #16
-      1 | foo.forEach(element => {
-      2 | 	for (let i in element) {
-      3 | 		return;
-      4 | 	}
-      5 | });
+## Invalid #99
+      1 | foo.forEach(_ => {
+      2 | 	if (true);
+      3 | 	else return {};
+      4 | })
 
-> Error 1/1
+> Output
 
     `␊
-    > 1 | foo.forEach(element => {␊
-        |     ^^^^^^^ Use \`for…of\` instead of \`Array#forEach(…)\`.␊
-      2 | 	for (let i in element) {␊
-      3 | 		return;␊
-      4 | 	}␊
-      5 | });␊
+      1 | for (const _ of foo) {␊
+      2 | 	if (true);␊
+      3 | 	else  { ({}); continue; }␊
+      4 | }␊
     `
 
-## Invalid #17
-      1 | foo.forEach(element => {
-      2 | 	for (let i of element) {
-      3 | 		return;
-      4 | 	}
-      5 | });
-
 > Error 1/1
 
     `␊
-    > 1 | foo.forEach(element => {␊
+    > 1 | foo.forEach(_ => {␊
         |     ^^^^^^^ Use \`for…of\` instead of \`Array#forEach(…)\`.␊
-      2 | 	for (let i of element) {␊
-      3 | 		return;␊
-      4 | 	}␊
-      5 | });␊
+      2 | 	if (true);␊
+      3 | 	else return {};␊
+      4 | })␊
     `
diff --git a/test/snapshots/no-array-for-each.mjs.snap b/test/snapshots/no-array-for-each.mjs.snap
index 819e77c159..5c8edc208d 100644
Binary files a/test/snapshots/no-array-for-each.mjs.snap and b/test/snapshots/no-array-for-each.mjs.snap differ