-
-
Notifications
You must be signed in to change notification settings - Fork 354
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add no-array-for-each
rule
#1017
Merged
Merged
Changes from 11 commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
ea0b64c
Add `no-array-for-each`
fisker 2a638a3
Merge branch 'master' into no-array-for-each
fisker aa5786e
Refactor
fisker 298e2e1
Tests
fisker 59c70a7
Tests
fisker 5c18e07
Tests
fisker 507863f
fix
fisker 4782010
Update snapshot
fisker b17181b
Fix
fisker f1e723b
Rename
fisker b57da17
Style
fisker f871353
Support check `this`
fisker 3bc64aa
Support check `function.id` and `arguments`
fisker d9f6141
Fix wrongly fix
fisker 6b0b592
More tests
fisker 109ab2b
Merge branch 'master' into no-array-for-each
fisker a747d93
Fix logic
fisker 4f374e5
Style
fisker dcafc69
Style
fisker 1a7e6d4
More tests
fisker 3f2c9ef
Clean
fisker bfa8ea0
Simplify array parentheses check
fisker a9c1713
Fix ASI problem
fisker 71cd4eb
Fix parameter check
fisker 2bcc256
Add `return` in `switch`
fisker 8dac1cd
Fix parenthesized callback
fisker 2599ae5
Keep `semi` for arrow functions
fisker edd49ba
Ignore unreachable
fisker 80cde64
Fix style
fisker 0917e76
Fix `arguments` check
fisker c33b426
Test possible conflicts
fisker 078e93b
Extend fix range
fisker f035aca
Add docs
fisker 4941a08
Fix `lint`
fisker e32cb9f
Rename a function
fisker 34e5a58
Improve `=>` token search, fix crash on `typescript` parser
fisker 6a91f93
Fix code style
fisker bc47538
One more test
fisker d8447b6
Merge branch 'master' into no-array-for-each
fisker de64278
Update no-array-for-each.js
fisker 1f1dbb8
Rename file
fisker 6982ad4
Update no-array-for-each.md
sindresorhus c767fe0
Update no-array-for-each.js
sindresorhus File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# Prefer `for…of` over `Array#forEach(…)`. | ||
|
||
<!-- More detailed description. Remove this comment. --> | ||
|
||
This rule is partly fixable. | ||
|
||
## Fail | ||
|
||
```js | ||
const foo = 'unicorn'; | ||
``` | ||
|
||
## Pass | ||
|
||
```js | ||
const foo = '🦄'; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,284 @@ | ||
'use strict'; | ||
const {isParenthesized, isArrowToken, isCommaToken, isSemicolonToken} = require('eslint-utils'); | ||
const getDocumentationUrl = require('./utils/get-documentation-url'); | ||
const methodSelector = require('./utils/method-selector'); | ||
const needsSemicolon = require('./utils/needs-semicolon'); | ||
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object'); | ||
const shouldAddParenthesesToExpressionStatementExpression = require('./utils/should-add-parentheses-to-expression-statement-expression'); | ||
|
||
const MESSAGE_ID = 'no-array-for-each'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Do not use `Array#forEach(…)`.' | ||
}; | ||
|
||
const arrayForEachCallSelector = methodSelector({ | ||
name: 'forEach', | ||
includeOptional: true | ||
}); | ||
|
||
const continueAbleNodeTypes = new Set([ | ||
'WhileStatement', | ||
'DoWhileStatement', | ||
'ForStatement', | ||
'ForOfStatement', | ||
'ForInStatement' | ||
]); | ||
|
||
function isReturnStatementInContinueAbleNodes(returnStatement, callbackFunction) { | ||
for (let node = returnStatement; node && node !== callbackFunction; node = node.parent) { | ||
if (continueAbleNodeTypes.has(node.type)) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
function getFixFunction(callExpression, sourceCode, functionReturnStatements) { | ||
const [callback] = callExpression.arguments; | ||
const parameters = callback.params; | ||
const array = callExpression.callee.object; | ||
const returnStatements = functionReturnStatements.get(callback); | ||
|
||
const getForOfLoopHeadText = () => { | ||
const parametersText = parameters.map(parameter => sourceCode.getText(parameter)); | ||
const useEntries = parameters.length === 2; | ||
|
||
let text = 'for (const '; | ||
text += useEntries ? `[${parametersText.join(', ')}]` : parametersText[0]; | ||
|
||
text += ' of '; | ||
|
||
let arrayText = sourceCode.getText(array); | ||
if ( | ||
isParenthesized(callExpression, sourceCode) || | ||
(useEntries && shouldAddParenthesesToMemberExpressionObject(array, sourceCode)) | ||
) { | ||
arrayText = `(${arrayText})`; | ||
} | ||
|
||
text += arrayText; | ||
|
||
if (useEntries) { | ||
text += '.entries()'; | ||
} | ||
|
||
text += ') '; | ||
|
||
return text; | ||
}; | ||
|
||
const getForOfLoopHeadRange = () => { | ||
const [start] = callExpression.range; | ||
let end; | ||
if (callback.body.type === 'BlockStatement') { | ||
end = callback.body.range[0]; | ||
} else { | ||
const arrowToken = sourceCode.getFirstToken(callback, isArrowToken); | ||
end = arrowToken.range[1]; | ||
} | ||
|
||
return [start, end]; | ||
}; | ||
|
||
function * replaceReturnStatement(returnStatement, fixer) { | ||
const returnToken = sourceCode.getFirstToken(returnStatement); | ||
|
||
/* istanbul ignore next: `ReturnStatement` firstToken should be `return` */ | ||
if (returnToken.value !== 'return') { | ||
throw new Error(`Unexpected token ${returnToken.value}.`); | ||
} | ||
|
||
if (!returnStatement.argument) { | ||
yield fixer.replaceText(returnToken, 'continue'); | ||
return; | ||
} | ||
|
||
// Remove `return` | ||
yield fixer.remove(returnToken); | ||
|
||
const previousToken = sourceCode.getTokenBefore(returnToken); | ||
const nextToken = sourceCode.getTokenAfter(returnToken); | ||
let textBefore = ''; | ||
let textAfter = ''; | ||
const shouldAddParentheses = | ||
!isParenthesized(returnStatement.argument, sourceCode) && | ||
shouldAddParenthesesToExpressionStatementExpression(returnStatement.argument); | ||
if (shouldAddParentheses) { | ||
textBefore = '('; | ||
textAfter = ')'; | ||
} | ||
|
||
const shouldAddSemicolonBefore = needsSemicolon(previousToken, sourceCode, shouldAddParentheses ? '(' : nextToken.value); | ||
if (shouldAddSemicolonBefore) { | ||
textBefore = `;${textBefore}`; | ||
} | ||
|
||
if (textBefore) { | ||
yield fixer.insertTextBefore(nextToken, textBefore); | ||
} | ||
|
||
if (textAfter) { | ||
yield fixer.insertTextAfter(returnStatement.argument, textAfter); | ||
} | ||
|
||
// If `returnStatement` has no semi | ||
const lastToken = sourceCode.getLastToken(returnStatement); | ||
yield fixer.insertTextAfter( | ||
returnStatement, | ||
`${isSemicolonToken(lastToken) ? '' : ';'} continue;` | ||
); | ||
} | ||
|
||
const shouldRemoveExpressionStatementLastToken = (token) => { | ||
if (!isSemicolonToken(token)) { | ||
return false; | ||
} | ||
|
||
if (callback.body.type === 'BlockStatement') { | ||
return true; | ||
} | ||
|
||
const nextToken = sourceCode.getTokenAfter(token); | ||
if (nextToken && needsSemicolon(token, sourceCode, nextToken.value)) { | ||
return false; | ||
} | ||
|
||
return true; | ||
}; | ||
|
||
return function * (fixer) { | ||
yield fixer.replaceTextRange(getForOfLoopHeadRange(), getForOfLoopHeadText()); | ||
|
||
// Remove call expression trailing comma | ||
const [penultimateToken, lastToken] = sourceCode.getLastTokens(callExpression, 2); | ||
if (isCommaToken(penultimateToken)) { | ||
yield fixer.remove(penultimateToken); | ||
} | ||
|
||
yield fixer.remove(lastToken); | ||
|
||
for (const returnStatement of returnStatements) { | ||
yield * replaceReturnStatement(returnStatement, fixer); | ||
} | ||
|
||
const expressionStatementLastToken = sourceCode.getLastToken(callExpression.parent); | ||
if (shouldRemoveExpressionStatementLastToken(expressionStatementLastToken)) { | ||
yield fixer.remove(expressionStatementLastToken, fixer); | ||
} | ||
}; | ||
} | ||
|
||
function isFixable(callExpression, sourceCode, functionReturnStatements) { | ||
// Check `CallExpression` | ||
if ( | ||
callExpression.optional || | ||
isParenthesized(callExpression, sourceCode) || | ||
callExpression.arguments.length !== 1 | ||
) { | ||
return false; | ||
} | ||
|
||
// Check `CallExpression.parent` | ||
if (callExpression.parent.type !== 'ExpressionStatement') { | ||
return false; | ||
} | ||
|
||
// Check `CallExpression.callee` | ||
if (callExpression.callee.optional) { | ||
return false; | ||
} | ||
|
||
// Check `CallExpression.arguments[0]`; | ||
const [callback] = callExpression.arguments; | ||
if ( | ||
// Leave non-function type to `no-array-callback-reference` rule | ||
(callback.type !== 'FunctionExpression' && callback.type !== 'ArrowFunctionExpression') || | ||
callback.async || | ||
callback.generator | ||
) { | ||
return false; | ||
} | ||
|
||
// Check `callback.params` | ||
const parameters = callback.params; | ||
if ( | ||
!(parameters.length === 1 || parameters.length === 2) || | ||
parameters.some(parameter => parameter.type !== 'Identifier') | ||
) { | ||
return false; | ||
} | ||
|
||
// TODO: check parameters conflicts | ||
|
||
// Check `ReturnStatement`s in `callback` | ||
const returnStatements = functionReturnStatements.get(callback); | ||
if (returnStatements.some(returnStatement => isReturnStatementInContinueAbleNodes(returnStatement, callback))) { | ||
return false; | ||
} | ||
|
||
// Check `callback` self | ||
if (callback.type === 'FunctionExpression') { | ||
// TODO: check `.id` `arguments` `this` of `FunctionExpression` | ||
} | ||
|
||
return true; | ||
} | ||
|
||
const create = context => { | ||
const functionStacks = []; | ||
const functionReturnStatements = new Map(); | ||
const callExpressions = []; | ||
|
||
const sourceCode = context.getSourceCode(); | ||
|
||
return { | ||
':function'(node) { | ||
functionStacks.push(node); | ||
functionReturnStatements.set(node, []); | ||
}, | ||
':function:exit'() { | ||
functionStacks.pop(); | ||
}, | ||
ReturnStatement(node) { | ||
const currentFunction = functionStacks[functionStacks.length - 1]; | ||
// `globalReturn ` | ||
/* istanbul ignore next: ESLint deprecated `ecmaFeatures`, can't test */ | ||
if (!currentFunction) { | ||
return; | ||
} | ||
|
||
const returnStatements = functionReturnStatements.get(currentFunction); | ||
returnStatements.push(node); | ||
}, | ||
[arrayForEachCallSelector](node) { | ||
callExpressions.push(node); | ||
}, | ||
'Program:exit'() { | ||
for (const callExpression of callExpressions) { | ||
const problem = { | ||
node: callExpression.callee.property, | ||
messageId: MESSAGE_ID | ||
}; | ||
|
||
if (isFixable(callExpression, sourceCode, functionReturnStatements)) { | ||
problem.fix = getFixFunction(callExpression, sourceCode, functionReturnStatements); | ||
} | ||
|
||
context.report(problem); | ||
} | ||
} | ||
}; | ||
}; | ||
|
||
module.exports = { | ||
create, | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
url: getDocumentationUrl(__filename) | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
rules/utils/should-add-parentheses-to-expression-statement-expression.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
'use strict'; | ||
|
||
/** | ||
Check if parentheses should to be added to a `node` when it's used as an `expression` of `ExpressionStatement`. | ||
|
||
@param {Node} node - The AST node to check. | ||
@param {SourceCode} sourceCode - The source code object. | ||
@returns {boolean} | ||
*/ | ||
function shouldAddParenthesesToExpressionStatementExpression(node) { | ||
switch (node.type) { | ||
case 'ObjectExpression': | ||
return true; | ||
case 'AssignmentExpression': | ||
return node.left.type === 'ObjectPattern' || node.left.type === 'ArrayPattern'; | ||
default: | ||
return false; | ||
} | ||
} | ||
|
||
module.exports = shouldAddParenthesesToExpressionStatementExpression; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really sure how to check this,
Should not fix
->
->
Should fix
->
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Figured a way to detect this, but not very efficient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A similar logic maybe possible to auto fix destructuring parameters, but it's much complicated, I'm going to try in a seperate PR.