You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

224 lines
6.9 KiB

/**
* @fileoverview A rule to suggest using of const declaration for variables that are never modified after declared.
* @author Toru Nagashima
* @copyright 2015 Toru Nagashima. All rights reserved.
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
var LOOP_TYPES = /^(?:While|DoWhile|For|ForIn|ForOf)Statement$/;
var FOR_IN_OF_TYPES = /^For(?:In|Of)Statement$/;
var SENTINEL_TYPES = /(?:Declaration|Statement)$/;
var END_POSITION_TYPES = /^(?:Assignment|Update)/;
/**
* Gets a write reference from a given variable if the variable is modified only
* once.
*
* @param {escope.Variable} variable - A variable to get.
* @returns {escope.Reference|null} A write reference or null.
*/
function getWriteReferenceIfOnce(variable) {
var retv = null;
var references = variable.references;
for (var i = 0; i < references.length; ++i) {
var reference = references[i];
if (reference.isWrite()) {
if (retv && !(retv.init && reference.init)) {
// This variable is modified two or more times.
return null;
}
retv = reference;
}
}
return retv;
}
/**
* Checks whether or not a given reference is in a loop condition or a
* for-loop's updater.
*
* @param {escope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the reference is in a loop condition or a
* for-loop's updater.
*/
function isInLoopHead(reference) {
var node = reference.identifier;
var parent = node.parent;
var assignment = false;
while (parent) {
if (LOOP_TYPES.test(parent.type)) {
return true;
}
// VariableDeclaration can be at ForInStatement.left
// This is catching the code like `for (const {b = ++a} of foo()) { ... }`
if (assignment &&
parent.type === "VariableDeclaration" &&
FOR_IN_OF_TYPES.test(parent.parent.type) &&
parent.parent.left === parent
) {
return true;
}
if (parent.type === "AssignmentPattern") {
assignment = true;
}
// If a declaration or a statement was found before a loop,
// it's not in the head of a loop.
if (SENTINEL_TYPES.test(parent.type)) {
break;
}
node = parent;
parent = parent.parent;
}
return false;
}
/**
* Gets the end position of a given reference.
* This position is used to check every ReadReferences exist after the given
* reference.
*
* If the reference is belonging to AssignmentExpression or UpdateExpression,
* this function returns the most rear position of those nodes.
* The range of those nodes are executed before the assignment.
*
* @param {escope.Reference} writer - A reference to get.
* @returns {number} The end position of the reference.
*/
function getEndPosition(writer) {
var node = writer.identifier;
var end = node.range[1];
// Detects the end position of the assignment expression the reference is
// belonging to.
while ((node = node.parent)) {
if (END_POSITION_TYPES.test(node.type)) {
end = node.range[1];
}
if (SENTINEL_TYPES.test(node.type)) {
break;
}
}
return end;
}
/**
* Gets a function which checks a given reference with the following condition:
*
* - The reference is a ReadReference.
* - The reference exists after a specific WriteReference.
* - The reference exists inside of the scope a specific WriteReference is
* belonging to.
*
* @param {escope.Reference} writer - A reference to check.
* @returns {function} A function which checks a given reference.
*/
function isInScope(writer) {
var start = getEndPosition(writer);
var end = writer.from.block.range[1];
return function(reference) {
if (!reference.isRead()) {
return true;
}
var range = reference.identifier.range;
return start <= range[0] && range[1] <= end;
};
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
/**
* Searches and reports variables that are never modified after declared.
* @param {Scope} scope - A scope of the search domain.
* @returns {void}
*/
function checkForVariables(scope) {
// Skip the TDZ type.
if (scope.type === "TDZ") {
return;
}
var variables = scope.variables;
for (var i = 0; i < variables.length; ++i) {
var variable = variables[i];
var def = variable.defs[0];
var declaration = def && def.parent;
var statement = declaration && declaration.parent;
var references = variable.references;
var identifier = variable.identifiers[0];
// Skips excludes `let`.
// And skips if it's at `ForStatement.init`.
if (!declaration ||
declaration.type !== "VariableDeclaration" ||
declaration.kind !== "let" ||
(statement.type === "ForStatement" && statement.init === declaration)
) {
continue;
}
// Checks references.
// - One WriteReference exists.
// - Two or more WriteReference don't exist.
// - Every ReadReference exists after the WriteReference.
// - The WriteReference doesn't exist in a loop condition.
// - If `eslintUsed` is true, we cannot know where it was used from.
// In this case, if the scope of the variable would change, it
// skips the variable.
var writer = getWriteReferenceIfOnce(variable);
if (writer &&
!(variable.eslintUsed && variable.scope !== writer.from) &&
!isInLoopHead(writer) &&
references.every(isInScope(writer))
) {
context.report({
node: identifier,
message: "'{{name}}' is never modified, use 'const' instead.",
data: identifier
});
}
}
}
/**
* Adds multiple items to the tail of an array.
* @param {any[]} array - A destination to add.
* @param {any[]} values - Items to be added.
* @returns {void}
*/
var pushAll = Function.apply.bind(Array.prototype.push);
return {
"Program:exit": function() {
var stack = [context.getScope()];
while (stack.length) {
var scope = stack.pop();
pushAll(stack, scope.childScopes);
checkForVariables(scope);
}
}
};
};
module.exports.schema = [];