UNPKG

@elbrus/eslint-plugin

Version:

ESLint plugin with custom rules that are used in Elbrus Bootcamp

185 lines (164 loc) 4.98 kB
export default { name: 'prefer-for-of', meta: { type: 'suggestion', docs: { description: 'Enforce the use of `for-of` loop over the standard `for` loop where possible', recommended: 'stylistic', }, messages: { preferForOf: 'Expected a `for-of` loop instead of a `for` loop with this simple iteration.', }, schema: [], }, create(context) { function isAssignee(node) { const { parent } = node; if (!parent) { return false; } // a[i] = 1, a[i] += 1, etc. if (parent.type === 'AssignmentExpression' && parent.left === node) { return true; } // delete a[i] if ( parent.type === 'UnaryExpression' && parent.operator === 'delete' && parent.argument === node ) { return true; } // a[i]++, --a[i], etc. if (parent.type === 'UpdateExpression' && parent.argument === node) { return true; } // [a[i]] = [0] if (parent.type === 'ArrayPattern') { return true; } // [...a[i]] = [0] if (parent.type === 'RestElement') { return true; } // ({ foo: a[i] }) = { foo: 0 } if ( parent.type === 'Property' && parent.value === node && parent.parent.type === 'ObjectExpression' && isAssignee(parent.parent) ) { return true; } return false; } function isSingleVariableDeclaration(node) { return ( node?.type === 'VariableDeclaration' && node.kind !== 'const' && node.declarations.length === 1 ); } function isLiteral(node, value) { return node.type === 'Literal' && node.value === value; } function isZeroInitialized(node) { return node.init != null && isLiteral(node.init, 0); } function isMatchingIdentifier(node, name) { return node.type === 'Identifier' && node.name === name; } function isLessThanLengthExpression(node, name) { if ( node?.type === 'BinaryExpression' && node.operator === '<' && isMatchingIdentifier(node.left, name) && node.right.type === 'MemberExpression' && isMatchingIdentifier(node.right.property, 'length') ) { return node.right.object; } return null; } function isIncrement(node, name) { if (!node) { return false; } switch (node.type) { case 'UpdateExpression': // x++ or ++x return node.operator === '++' && isMatchingIdentifier(node.argument, name); case 'AssignmentExpression': if (isMatchingIdentifier(node.left, name)) { if (node.operator === '+=') { // x += 1 return isLiteral(node.right, 1); } if (node.operator === '=') { // x = x + 1 or x = 1 + x const expr = node.right; return ( expr.type === 'BinaryExpression' && expr.operator === '+' && ((isMatchingIdentifier(expr.left, name) && isLiteral(expr.right, 1)) || (isLiteral(expr.left, 1) && isMatchingIdentifier(expr.right, name))) ); } } return false; default: return false; } } function contains(outer, inner) { return outer.range[0] <= inner.range[0] && outer.range[1] >= inner.range[1]; } function isIndexOnlyUsedWithArray(body, indexVar, arrayExpression) { const arrayText = context.sourceCode.getText(arrayExpression); return indexVar.references.every((reference) => { const id = reference.identifier; const node = id.parent; return ( !contains(body, id) || (node.type === 'MemberExpression' && node.object.type !== 'ThisExpression' && node.property === id && context.sourceCode.getText(node.object) === arrayText && !isAssignee(node)) ); }); } return { ForStatement(node) { if (!isSingleVariableDeclaration(node.init)) { return; } const declarator = node.init.declarations[0]; if ( !declarator || !isZeroInitialized(declarator) || declarator.id.type !== 'Identifier' ) { return; } const indexName = declarator.id.name; const arrayExpression = isLessThanLengthExpression(node.test, indexName); if (!arrayExpression) { return; } const [indexVar] = context.sourceCode.getDeclaredVariables(node.init); if ( isIncrement(node.update, indexName) && isIndexOnlyUsedWithArray(node.body, indexVar, arrayExpression) ) { context.report({ node, messageId: 'preferForOf', }); } }, }; }, };