We have been stalled on ESLint 3.8.0 for some time. Current ESLint is 3.13.0. We have been unable to upgrade because of more aggressive reporting on some rules, including indentation. ESLint configuration options and bugfixes are now such that we can reasonably upgrade. PR-URL: https://github.com/nodejs/node/pull/10561 Reviewed-By: Teddy Katz <teddy.katz@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
367 lines
14 KiB
JavaScript
367 lines
14 KiB
JavaScript
/**
|
|
* @fileoverview A rule to control the use of single variable declarations.
|
|
* @author Ian Christian Myers
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Rule Definition
|
|
//------------------------------------------------------------------------------
|
|
|
|
module.exports = {
|
|
meta: {
|
|
docs: {
|
|
description: "enforce variables to be declared either together or separately in functions",
|
|
category: "Stylistic Issues",
|
|
recommended: false
|
|
},
|
|
|
|
schema: [
|
|
{
|
|
oneOf: [
|
|
{
|
|
enum: ["always", "never"]
|
|
},
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
var: {
|
|
enum: ["always", "never"]
|
|
},
|
|
let: {
|
|
enum: ["always", "never"]
|
|
},
|
|
const: {
|
|
enum: ["always", "never"]
|
|
}
|
|
},
|
|
additionalProperties: false
|
|
},
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
initialized: {
|
|
enum: ["always", "never"]
|
|
},
|
|
uninitialized: {
|
|
enum: ["always", "never"]
|
|
}
|
|
},
|
|
additionalProperties: false
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
|
|
create(context) {
|
|
|
|
const MODE_ALWAYS = "always",
|
|
MODE_NEVER = "never";
|
|
|
|
const mode = context.options[0] || MODE_ALWAYS;
|
|
|
|
const options = {
|
|
};
|
|
|
|
if (typeof mode === "string") { // simple options configuration with just a string
|
|
options.var = { uninitialized: mode, initialized: mode };
|
|
options.let = { uninitialized: mode, initialized: mode };
|
|
options.const = { uninitialized: mode, initialized: mode };
|
|
} else if (typeof mode === "object") { // options configuration is an object
|
|
if (mode.hasOwnProperty("var") && typeof mode.var === "string") {
|
|
options.var = { uninitialized: mode.var, initialized: mode.var };
|
|
}
|
|
if (mode.hasOwnProperty("let") && typeof mode.let === "string") {
|
|
options.let = { uninitialized: mode.let, initialized: mode.let };
|
|
}
|
|
if (mode.hasOwnProperty("const") && typeof mode.const === "string") {
|
|
options.const = { uninitialized: mode.const, initialized: mode.const };
|
|
}
|
|
if (mode.hasOwnProperty("uninitialized")) {
|
|
if (!options.var) {
|
|
options.var = {};
|
|
}
|
|
if (!options.let) {
|
|
options.let = {};
|
|
}
|
|
if (!options.const) {
|
|
options.const = {};
|
|
}
|
|
options.var.uninitialized = mode.uninitialized;
|
|
options.let.uninitialized = mode.uninitialized;
|
|
options.const.uninitialized = mode.uninitialized;
|
|
}
|
|
if (mode.hasOwnProperty("initialized")) {
|
|
if (!options.var) {
|
|
options.var = {};
|
|
}
|
|
if (!options.let) {
|
|
options.let = {};
|
|
}
|
|
if (!options.const) {
|
|
options.const = {};
|
|
}
|
|
options.var.initialized = mode.initialized;
|
|
options.let.initialized = mode.initialized;
|
|
options.const.initialized = mode.initialized;
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Helpers
|
|
//--------------------------------------------------------------------------
|
|
|
|
const functionStack = [];
|
|
const blockStack = [];
|
|
|
|
/**
|
|
* Increments the blockStack counter.
|
|
* @returns {void}
|
|
* @private
|
|
*/
|
|
function startBlock() {
|
|
blockStack.push({
|
|
let: { initialized: false, uninitialized: false },
|
|
const: { initialized: false, uninitialized: false }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Increments the functionStack counter.
|
|
* @returns {void}
|
|
* @private
|
|
*/
|
|
function startFunction() {
|
|
functionStack.push({ initialized: false, uninitialized: false });
|
|
startBlock();
|
|
}
|
|
|
|
/**
|
|
* Decrements the blockStack counter.
|
|
* @returns {void}
|
|
* @private
|
|
*/
|
|
function endBlock() {
|
|
blockStack.pop();
|
|
}
|
|
|
|
/**
|
|
* Decrements the functionStack counter.
|
|
* @returns {void}
|
|
* @private
|
|
*/
|
|
function endFunction() {
|
|
functionStack.pop();
|
|
endBlock();
|
|
}
|
|
|
|
/**
|
|
* Records whether initialized or uninitialized variables are defined in current scope.
|
|
* @param {string} statementType node.kind, one of: "var", "let", or "const"
|
|
* @param {ASTNode[]} declarations List of declarations
|
|
* @param {Object} currentScope The scope being investigated
|
|
* @returns {void}
|
|
* @private
|
|
*/
|
|
function recordTypes(statementType, declarations, currentScope) {
|
|
for (let i = 0; i < declarations.length; i++) {
|
|
if (declarations[i].init === null) {
|
|
if (options[statementType] && options[statementType].uninitialized === MODE_ALWAYS) {
|
|
currentScope.uninitialized = true;
|
|
}
|
|
} else {
|
|
if (options[statementType] && options[statementType].initialized === MODE_ALWAYS) {
|
|
currentScope.initialized = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines the current scope (function or block)
|
|
* @param {string} statementType node.kind, one of: "var", "let", or "const"
|
|
* @returns {Object} The scope associated with statementType
|
|
*/
|
|
function getCurrentScope(statementType) {
|
|
let currentScope;
|
|
|
|
if (statementType === "var") {
|
|
currentScope = functionStack[functionStack.length - 1];
|
|
} else if (statementType === "let") {
|
|
currentScope = blockStack[blockStack.length - 1].let;
|
|
} else if (statementType === "const") {
|
|
currentScope = blockStack[blockStack.length - 1].const;
|
|
}
|
|
return currentScope;
|
|
}
|
|
|
|
/**
|
|
* Counts the number of initialized and uninitialized declarations in a list of declarations
|
|
* @param {ASTNode[]} declarations List of declarations
|
|
* @returns {Object} Counts of 'uninitialized' and 'initialized' declarations
|
|
* @private
|
|
*/
|
|
function countDeclarations(declarations) {
|
|
const counts = { uninitialized: 0, initialized: 0 };
|
|
|
|
for (let i = 0; i < declarations.length; i++) {
|
|
if (declarations[i].init === null) {
|
|
counts.uninitialized++;
|
|
} else {
|
|
counts.initialized++;
|
|
}
|
|
}
|
|
return counts;
|
|
}
|
|
|
|
/**
|
|
* Determines if there is more than one var statement in the current scope.
|
|
* @param {string} statementType node.kind, one of: "var", "let", or "const"
|
|
* @param {ASTNode[]} declarations List of declarations
|
|
* @returns {boolean} Returns true if it is the first var declaration, false if not.
|
|
* @private
|
|
*/
|
|
function hasOnlyOneStatement(statementType, declarations) {
|
|
|
|
const declarationCounts = countDeclarations(declarations);
|
|
const currentOptions = options[statementType] || {};
|
|
const currentScope = getCurrentScope(statementType);
|
|
|
|
if (currentOptions.uninitialized === MODE_ALWAYS && currentOptions.initialized === MODE_ALWAYS) {
|
|
if (currentScope.uninitialized || currentScope.initialized) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (declarationCounts.uninitialized > 0) {
|
|
if (currentOptions.uninitialized === MODE_ALWAYS && currentScope.uninitialized) {
|
|
return false;
|
|
}
|
|
}
|
|
if (declarationCounts.initialized > 0) {
|
|
if (currentOptions.initialized === MODE_ALWAYS && currentScope.initialized) {
|
|
return false;
|
|
}
|
|
}
|
|
recordTypes(statementType, declarations, currentScope);
|
|
return true;
|
|
}
|
|
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Public API
|
|
//--------------------------------------------------------------------------
|
|
|
|
return {
|
|
Program: startFunction,
|
|
FunctionDeclaration: startFunction,
|
|
FunctionExpression: startFunction,
|
|
ArrowFunctionExpression: startFunction,
|
|
BlockStatement: startBlock,
|
|
ForStatement: startBlock,
|
|
ForInStatement: startBlock,
|
|
ForOfStatement: startBlock,
|
|
SwitchStatement: startBlock,
|
|
|
|
VariableDeclaration(node) {
|
|
const parent = node.parent;
|
|
const type = node.kind;
|
|
|
|
if (!options[type]) {
|
|
return;
|
|
}
|
|
|
|
const declarations = node.declarations;
|
|
const declarationCounts = countDeclarations(declarations);
|
|
|
|
// always
|
|
if (!hasOnlyOneStatement(type, declarations)) {
|
|
if (options[type].initialized === MODE_ALWAYS && options[type].uninitialized === MODE_ALWAYS) {
|
|
context.report({
|
|
node,
|
|
message: "Combine this with the previous '{{type}}' statement.",
|
|
data: {
|
|
type
|
|
}
|
|
});
|
|
} else {
|
|
if (options[type].initialized === MODE_ALWAYS) {
|
|
context.report({
|
|
node,
|
|
message: "Combine this with the previous '{{type}}' statement with initialized variables.",
|
|
data: {
|
|
type
|
|
}
|
|
});
|
|
}
|
|
if (options[type].uninitialized === MODE_ALWAYS) {
|
|
if (node.parent.left === node && (node.parent.type === "ForInStatement" || node.parent.type === "ForOfStatement")) {
|
|
return;
|
|
}
|
|
context.report({
|
|
node,
|
|
message: "Combine this with the previous '{{type}}' statement with uninitialized variables.",
|
|
data: {
|
|
type
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// never
|
|
if (parent.type !== "ForStatement" || parent.init !== node) {
|
|
const totalDeclarations = declarationCounts.uninitialized + declarationCounts.initialized;
|
|
|
|
if (totalDeclarations > 1) {
|
|
|
|
if (options[type].initialized === MODE_NEVER && options[type].uninitialized === MODE_NEVER) {
|
|
|
|
// both initialized and uninitialized
|
|
context.report({
|
|
node,
|
|
message: "Split '{{type}}' declarations into multiple statements.",
|
|
data: {
|
|
type
|
|
}
|
|
});
|
|
} else if (options[type].initialized === MODE_NEVER && declarationCounts.initialized > 0) {
|
|
|
|
// initialized
|
|
context.report({
|
|
node,
|
|
message: "Split initialized '{{type}}' declarations into multiple statements.",
|
|
data: {
|
|
type
|
|
}
|
|
});
|
|
} else if (options[type].uninitialized === MODE_NEVER && declarationCounts.uninitialized > 0) {
|
|
|
|
// uninitialized
|
|
context.report({
|
|
node,
|
|
message: "Split uninitialized '{{type}}' declarations into multiple statements.",
|
|
data: {
|
|
type
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
"ForStatement:exit": endBlock,
|
|
"ForOfStatement:exit": endBlock,
|
|
"ForInStatement:exit": endBlock,
|
|
"SwitchStatement:exit": endBlock,
|
|
"BlockStatement:exit": endBlock,
|
|
"Program:exit": endFunction,
|
|
"FunctionDeclaration:exit": endFunction,
|
|
"FunctionExpression:exit": endFunction,
|
|
"ArrowFunctionExpression:exit": endFunction
|
|
};
|
|
|
|
}
|
|
};
|