mirror of https://github.com/lukechilds/node.git
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
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 = [];
|
|
|