-
Notifications
You must be signed in to change notification settings - Fork 889
Clean up prefer-for-of rule #2348
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,168 +36,177 @@ export class Rule extends Lint.Rules.AbstractRule { | |
public static FAILURE_STRING = "Expected a 'for-of' loop instead of a 'for' loop with this simple iteration"; | ||
|
||
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { | ||
return this.applyWithWalker(new PreferForOfWalker(sourceFile, this.getOptions())); | ||
return this.applyWithFunction(sourceFile, walk); | ||
} | ||
} | ||
|
||
interface IIncrementorState { | ||
interface IncrementorState { | ||
indexVariableName: string; | ||
arrayToken: ts.Identifier; | ||
forLoopEndPosition: number; | ||
onlyArrayReadAccess: boolean; | ||
} | ||
|
||
// a map of incrementors and whether or not they are only used to index into an array reference in the for loop | ||
type IncrementorMap = Map<string, IIncrementorState>; | ||
function walk(ctx: Lint.WalkContext<void>): void { | ||
const scopes: IncrementorState[] = []; | ||
|
||
class PreferForOfWalker extends Lint.BlockScopeAwareRuleWalker<void, IncrementorMap> { | ||
public createScope() {} // tslint:disable-line:no-empty | ||
return ts.forEachChild(ctx.sourceFile, cb); | ||
|
||
public createBlockScope() { | ||
return new Map(); | ||
function cb(node: ts.Node): void { | ||
switch (node.kind) { | ||
case ts.SyntaxKind.ForStatement: | ||
return visitForStatement(node as ts.ForStatement); | ||
case ts.SyntaxKind.Identifier: | ||
return visitIdentifier(node as ts.Identifier); | ||
default: | ||
return ts.forEachChild(node, cb); | ||
} | ||
} | ||
|
||
protected visitForStatement(node: ts.ForStatement) { | ||
const arrayNodeInfo = this.getForLoopHeaderInfo(node); | ||
const currentBlockScope = this.getCurrentBlockScope(); | ||
let indexVariableName: string | undefined; | ||
if (node.incrementor != null && arrayNodeInfo != null) { | ||
const { indexVariable, arrayToken } = arrayNodeInfo; | ||
indexVariableName = indexVariable.getText(); | ||
|
||
// store `for` loop state | ||
currentBlockScope.set(indexVariableName, { | ||
arrayToken: arrayToken as ts.Identifier, | ||
forLoopEndPosition: node.incrementor.end + 1, | ||
onlyArrayReadAccess: true, | ||
}); | ||
function visitForStatement(node: ts.ForStatement): void { | ||
const arrayNodeInfo = getForLoopHeaderInfo(node); | ||
if (!node.incrementor || !arrayNodeInfo) { | ||
return ts.forEachChild(node, cb); | ||
} | ||
|
||
super.visitForStatement(node); | ||
|
||
if (indexVariableName != null) { | ||
const incrementorState = currentBlockScope.get(indexVariableName)!; | ||
if (incrementorState.onlyArrayReadAccess) { | ||
this.addFailureFromStartToEnd(node.getStart(), incrementorState.forLoopEndPosition, Rule.FAILURE_STRING); | ||
} | ||
const { indexVariable, arrayToken } = arrayNodeInfo; | ||
const indexVariableName = indexVariable.text; | ||
|
||
// store `for` loop state | ||
const state: IncrementorState = { | ||
indexVariableName, | ||
arrayToken: arrayToken as ts.Identifier, | ||
onlyArrayReadAccess: true, | ||
}; | ||
scopes.push(state); | ||
ts.forEachChild(node.statement, cb); | ||
scopes.pop(); | ||
|
||
if (state.onlyArrayReadAccess) { | ||
ctx.addFailure(node.getStart(), node.statement.getFullStart(), Rule.FAILURE_STRING); | ||
} | ||
} | ||
|
||
// remove current `for` loop state | ||
currentBlockScope.delete(indexVariableName); | ||
function visitIdentifier(node: ts.Identifier): void { | ||
const state = getStateForVariable(node.text); | ||
if (state) { | ||
updateIncrementorState(node, state); | ||
} | ||
} | ||
|
||
protected visitIdentifier(node: ts.Identifier) { | ||
const incrementorScope = this.findBlockScope((scope) => scope.has(node.text)); | ||
|
||
if (incrementorScope != null) { | ||
const incrementorState = incrementorScope.get(node.text); | ||
|
||
// check if the identifier is an iterator and is currently in the `for` loop body | ||
if (incrementorState != null && incrementorState.arrayToken != null && incrementorState.forLoopEndPosition < node.getStart()) { | ||
// check if iterator is used for something other than reading data from array | ||
if (node.parent!.kind === ts.SyntaxKind.ElementAccessExpression) { | ||
const elementAccess = node.parent as ts.ElementAccessExpression; | ||
const arrayIdentifier = unwrapParentheses(elementAccess.expression) as ts.Identifier; | ||
if (incrementorState.arrayToken.getText() !== arrayIdentifier.getText()) { | ||
// iterator used in array other than one iterated over | ||
incrementorState.onlyArrayReadAccess = false; | ||
} else if (elementAccess.parent != null && isAssignment(elementAccess.parent)) { | ||
// array position is assigned a new value | ||
incrementorState.onlyArrayReadAccess = false; | ||
} | ||
} else { | ||
incrementorState.onlyArrayReadAccess = false; | ||
} | ||
function getStateForVariable(name: string): IncrementorState | undefined { | ||
for (let i = scopes.length - 1; i >= 0; i--) { | ||
const scope = scopes[i]; | ||
if (scope.indexVariableName === name) { | ||
return scope; | ||
} | ||
super.visitIdentifier(node); | ||
} | ||
return undefined; | ||
} | ||
} | ||
|
||
// returns the iterator and array of a `for` loop if the `for` loop is basic. Otherwise, `null` | ||
private getForLoopHeaderInfo(forLoop: ts.ForStatement) { | ||
let indexVariableName: string | undefined; | ||
let indexVariable: ts.Identifier | undefined; | ||
|
||
// assign `indexVariableName` if initializer is simple and starts at 0 | ||
if (forLoop.initializer != null && forLoop.initializer.kind === ts.SyntaxKind.VariableDeclarationList) { | ||
const syntaxList = forLoop.initializer.getChildAt(1); | ||
if (syntaxList.kind === ts.SyntaxKind.SyntaxList && syntaxList.getChildCount() === 1) { | ||
const assignment = syntaxList.getChildAt(0); | ||
if (assignment.kind === ts.SyntaxKind.VariableDeclaration && assignment.getChildCount() === 3) { | ||
const value = assignment.getChildAt(2).getText(); | ||
if (value === "0") { | ||
indexVariable = assignment.getChildAt(0) as ts.Identifier; | ||
indexVariableName = indexVariable.getText(); | ||
} | ||
function updateIncrementorState(node: ts.Identifier, state: IncrementorState): void { | ||
// check if iterator is used for something other than reading data from array | ||
if (node.parent!.kind === ts.SyntaxKind.ElementAccessExpression) { | ||
const elementAccess = node.parent as ts.ElementAccessExpression; | ||
const arrayIdentifier = unwrapParentheses(elementAccess.expression) as ts.Identifier; | ||
if (state.arrayToken.getText() !== arrayIdentifier.getText()) { | ||
// iterator used in array other than one iterated over | ||
state.onlyArrayReadAccess = false; | ||
} else if (elementAccess.parent && isAssignment(elementAccess.parent)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
// array position is assigned a new value | ||
state.onlyArrayReadAccess = false; | ||
} | ||
} else { | ||
state.onlyArrayReadAccess = false; | ||
} | ||
|
||
} | ||
|
||
// returns the iterator and array of a `for` loop if the `for` loop is basic. | ||
function getForLoopHeaderInfo(forLoop: ts.ForStatement): { indexVariable: ts.Identifier, arrayToken: ts.Expression } | undefined { | ||
let indexVariableName: string | undefined; | ||
let indexVariable: ts.Identifier | undefined; | ||
|
||
// assign `indexVariableName` if initializer is simple and starts at 0 | ||
if (forLoop.initializer && forLoop.initializer.kind === ts.SyntaxKind.VariableDeclarationList) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if (forLoop.initializer && isVariableDeclarationList(forLoop.initializer) && forLoop.initializer.declarations.length === 1) {
const declaration = forLoop.initializer.declarations[0];
if (declaration.name.kind === ts.SyntaxKind.Identifier &&
declaration.initializer !== undefined && isNumericLiteral(declaration.initializer) && declaration.initializer.text === "0") {
indexVariable = declaration.name;
indexVariableName = indexVariable.text;
}
} |
||
const syntaxList = forLoop.initializer.getChildAt(1); | ||
if (syntaxList.kind === ts.SyntaxKind.SyntaxList && syntaxList.getChildCount() === 1) { | ||
const assignment = syntaxList.getChildAt(0); | ||
if (assignment.kind === ts.SyntaxKind.VariableDeclaration && assignment.getChildCount() === 3) { | ||
const value = assignment.getChildAt(2).getText(); | ||
if (value === "0") { | ||
indexVariable = assignment.getChildAt(0) as ts.Identifier; | ||
indexVariableName = indexVariable.getText(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// ensure `for` condition | ||
if (indexVariableName == null | ||
|| forLoop.condition == null | ||
|| forLoop.condition.kind !== ts.SyntaxKind.BinaryExpression | ||
|| forLoop.condition.getChildAt(0).getText() !== indexVariableName | ||
|| forLoop.condition.getChildAt(1).getText() !== "<") { | ||
// ensure `for` condition | ||
if (!indexVariableName | ||
|| !forLoop.condition | ||
|| forLoop.condition.kind !== ts.SyntaxKind.BinaryExpression | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. !isBinaryExpression(forLoop.condition) ||
!isVariable(forLoop.condition.left, indexVariableName) || // see my other comments regarding indexVariableName
forLoop.condition.operatorToken.kind !== ts.SyntaxKind.LessThanToken ||
// condition from line 159
!isPropertyAccessExpression(forLoop.condition.right) ||
forLoop.condition.right.name.text !== "length" |
||
|| forLoop.condition.getChildAt(0).getText() !== indexVariableName | ||
|| forLoop.condition.getChildAt(1).getText() !== "<") { | ||
|
||
return null; | ||
} | ||
return undefined; | ||
} | ||
|
||
if (forLoop.incrementor == null || !this.isIncremented(forLoop.incrementor, indexVariableName)) { | ||
return null; | ||
} | ||
if (!forLoop.incrementor || !isIncremented(forLoop.incrementor, indexVariableName)) { | ||
return undefined; | ||
} | ||
|
||
// ensure that the condition checks a `length` property | ||
const conditionRight = forLoop.condition.getChildAt(2); | ||
if (conditionRight.kind === ts.SyntaxKind.PropertyAccessExpression) { | ||
const propertyAccess = conditionRight as ts.PropertyAccessExpression; | ||
if (indexVariable != null && propertyAccess.name.getText() === "length") { | ||
return { indexVariable: indexVariable!, arrayToken: unwrapParentheses(propertyAccess.expression) }; | ||
} | ||
// ensure that the condition checks a `length` property | ||
const conditionRight = forLoop.condition.getChildAt(2); | ||
if (conditionRight.kind === ts.SyntaxKind.PropertyAccessExpression) { | ||
const propertyAccess = conditionRight as ts.PropertyAccessExpression; | ||
if (indexVariable && propertyAccess.name.getText() === "length") { | ||
return { indexVariable: indexVariable!, arrayToken: unwrapParentheses(propertyAccess.expression) }; | ||
} | ||
} | ||
|
||
return null; | ||
return undefined; | ||
} | ||
|
||
function isIncremented(node: ts.Node, indexVariableName: string): boolean { | ||
if (!node) { | ||
return false; | ||
} | ||
|
||
private isIncremented(node: ts.Node, indexVariableName: string) { | ||
if (node == null) { | ||
return false; | ||
// ensure variable is incremented | ||
if (node.kind === ts.SyntaxKind.PrefixUnaryExpression) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use typeguards? |
||
const incrementor = node as ts.PrefixUnaryExpression; | ||
if (incrementor.operator === ts.SyntaxKind.PlusPlusToken && incrementor.operand.getText() === indexVariableName) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you could replace maybe make this a function because you also need it below |
||
// x++ | ||
return true; | ||
} | ||
|
||
// ensure variable is incremented | ||
if (node.kind === ts.SyntaxKind.PrefixUnaryExpression) { | ||
const incrementor = node as ts.PrefixUnaryExpression; | ||
if (incrementor.operator === ts.SyntaxKind.PlusPlusToken && incrementor.operand.getText() === indexVariableName) { | ||
// x++ | ||
return true; | ||
} | ||
} else if (node.kind === ts.SyntaxKind.PostfixUnaryExpression) { | ||
const incrementor = node as ts.PostfixUnaryExpression; | ||
if (incrementor.operator === ts.SyntaxKind.PlusPlusToken && incrementor.operand.getText() === indexVariableName) { | ||
// ++x | ||
return true; | ||
} | ||
} else if (node.kind === ts.SyntaxKind.BinaryExpression) { | ||
const binaryExpression = node as ts.BinaryExpression; | ||
if (binaryExpression.operatorToken.getText() === "+=" | ||
&& binaryExpression.left.getText() === indexVariableName | ||
&& binaryExpression.right.getText() === "1") { | ||
// x += 1 | ||
return true; | ||
} | ||
if (binaryExpression.operatorToken.getText() === "=" | ||
&& binaryExpression.left.getText() === indexVariableName) { | ||
const addExpression = binaryExpression.right as ts.BinaryExpression; | ||
if (addExpression.operatorToken.getText() === "+") { | ||
if (addExpression.right.getText() === indexVariableName && addExpression.left.getText() === "1") { | ||
// x = 1 + x | ||
return true; | ||
} else if (addExpression.left.getText() === indexVariableName && addExpression.right.getText() === "1") { | ||
// x = x + 1 | ||
return true; | ||
} | ||
} else if (node.kind === ts.SyntaxKind.PostfixUnaryExpression) { | ||
const incrementor = node as ts.PostfixUnaryExpression; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could be joined with the if block above as they do exactly the same |
||
if (incrementor.operator === ts.SyntaxKind.PlusPlusToken && incrementor.operand.getText() === indexVariableName) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just return, no need for an if |
||
// ++x | ||
return true; | ||
} | ||
} else if (node.kind === ts.SyntaxKind.BinaryExpression) { | ||
const binaryExpression = node as ts.BinaryExpression; | ||
if (binaryExpression.operatorToken.getText() === "+=" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
&& binaryExpression.left.getText() === indexVariableName | ||
&& binaryExpression.right.getText() === "1") { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
make this a function a reuse it for the 2 conditions below, too |
||
// x += 1 | ||
return true; | ||
} | ||
if (binaryExpression.operatorToken.getText() === "=" | ||
&& binaryExpression.left.getText() === indexVariableName) { | ||
const addExpression = binaryExpression.right as ts.BinaryExpression; | ||
if (addExpression.operatorToken.getText() === "+") { | ||
if (addExpression.right.getText() === indexVariableName && addExpression.left.getText() === "1") { | ||
// x = 1 + x | ||
return true; | ||
} else if (addExpression.left.getText() === indexVariableName && addExpression.right.getText() === "1") { | ||
// x = x + 1 | ||
return true; | ||
} | ||
} | ||
} | ||
return false; | ||
} | ||
return false; | ||
} |
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.
arrayToken
is not guaranteed to bets.Identifier