Current Dev State

This commit is contained in:
Tim Lorsbach
2025-06-23 20:13:54 +02:00
parent b4f9bb277d
commit ded50edaa2
22617 changed files with 4345095 additions and 174 deletions

View File

@ -0,0 +1,156 @@
/**
* @fileoverview Rule to flag wrapping non-iife in parens
* @author Gyandeep Singh
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether or not a given node is an `Identifier` node which was named a given name.
* @param {ASTNode} node - A node to check.
* @param {string} name - An expected name of the node.
* @returns {boolean} `true` if the node is an `Identifier` node which was named as expected.
*/
function isIdentifier(node, name) {
return node.type === "Identifier" && node.name === name;
}
/**
* Checks whether or not a given node is an argument of a specified method call.
* @param {ASTNode} node - A node to check.
* @param {number} index - An expected index of the node in arguments.
* @param {string} object - An expected name of the object of the method.
* @param {string} property - An expected name of the method.
* @returns {boolean} `true` if the node is an argument of the specified method call.
*/
function isArgumentOfMethodCall(node, index, object, property) {
const parent = node.parent;
return (
parent.type === "CallExpression" &&
parent.callee.type === "MemberExpression" &&
parent.callee.computed === false &&
isIdentifier(parent.callee.object, object) &&
isIdentifier(parent.callee.property, property) &&
parent.arguments[index] === node
);
}
/**
* Checks whether or not a given node is a property descriptor.
* @param {ASTNode} node - A node to check.
* @returns {boolean} `true` if the node is a property descriptor.
*/
function isPropertyDescriptor(node) {
// Object.defineProperty(obj, "foo", {set: ...})
if (isArgumentOfMethodCall(node, 2, "Object", "defineProperty") ||
isArgumentOfMethodCall(node, 2, "Reflect", "defineProperty")
) {
return true;
}
/*
* Object.defineProperties(obj, {foo: {set: ...}})
* Object.create(proto, {foo: {set: ...}})
*/
node = node.parent.parent;
return node.type === "ObjectExpression" && (
isArgumentOfMethodCall(node, 1, "Object", "create") ||
isArgumentOfMethodCall(node, 1, "Object", "defineProperties")
);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce getter and setter pairs in objects",
category: "Best Practices",
recommended: false
},
schema: [{
type: "object",
properties: {
getWithoutSet: {
type: "boolean"
},
setWithoutGet: {
type: "boolean"
}
},
additionalProperties: false
}]
},
create(context) {
const config = context.options[0] || {};
const checkGetWithoutSet = config.getWithoutSet === true;
const checkSetWithoutGet = config.setWithoutGet !== false;
/**
* Checks a object expression to see if it has setter and getter both present or none.
* @param {ASTNode} node The node to check.
* @returns {void}
* @private
*/
function checkLonelySetGet(node) {
let isSetPresent = false;
let isGetPresent = false;
const isDescriptor = isPropertyDescriptor(node);
for (let i = 0, end = node.properties.length; i < end; i++) {
const property = node.properties[i];
let propToCheck = "";
if (property.kind === "init") {
if (isDescriptor && !property.computed) {
propToCheck = property.key.name;
}
} else {
propToCheck = property.kind;
}
switch (propToCheck) {
case "set":
isSetPresent = true;
break;
case "get":
isGetPresent = true;
break;
default:
// Do nothing
}
if (isSetPresent && isGetPresent) {
break;
}
}
if (checkSetWithoutGet && isSetPresent && !isGetPresent) {
context.report({ node, message: "Getter is not present." });
} else if (checkGetWithoutSet && isGetPresent && !isSetPresent) {
context.report({ node, message: "Setter is not present." });
}
}
return {
ObjectExpression(node) {
if (checkSetWithoutGet || checkGetWithoutSet) {
checkLonelySetGet(node);
}
}
};
}
};

View File

@ -0,0 +1,229 @@
/**
* @fileoverview Disallows or enforces spaces inside of array brackets.
* @author Jamund Ferguson
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent spacing inside array brackets",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
enum: ["always", "never"]
},
{
type: "object",
properties: {
singleValue: {
type: "boolean"
},
objectsInArrays: {
type: "boolean"
},
arraysInArrays: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
const spaced = context.options[0] === "always",
sourceCode = context.getSourceCode();
/**
* Determines whether an option is set, relative to the spacing option.
* If spaced is "always", then check whether option is set to false.
* If spaced is "never", then check whether option is set to true.
* @param {Object} option - The option to exclude.
* @returns {boolean} Whether or not the property is excluded.
*/
function isOptionSet(option) {
return context.options[1] ? context.options[1][option] === !spaced : false;
}
const options = {
spaced,
singleElementException: isOptionSet("singleValue"),
objectsInArraysException: isOptionSet("objectsInArrays"),
arraysInArraysException: isOptionSet("arraysInArrays")
};
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Reports that there shouldn't be a space after the first token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @returns {void}
*/
function reportNoBeginningSpace(node, token) {
context.report({
node,
loc: token.loc.start,
message: "There should be no space after '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
const nextToken = sourceCode.getTokenAfter(token);
return fixer.removeRange([token.range[1], nextToken.range[0]]);
}
});
}
/**
* Reports that there shouldn't be a space before the last token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @returns {void}
*/
function reportNoEndingSpace(node, token) {
context.report({
node,
loc: token.loc.start,
message: "There should be no space before '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
const previousToken = sourceCode.getTokenBefore(token);
return fixer.removeRange([previousToken.range[1], token.range[0]]);
}
});
}
/**
* Reports that there should be a space after the first token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @returns {void}
*/
function reportRequiredBeginningSpace(node, token) {
context.report({
node,
loc: token.loc.start,
message: "A space is required after '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.insertTextAfter(token, " ");
}
});
}
/**
* Reports that there should be a space before the last token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @returns {void}
*/
function reportRequiredEndingSpace(node, token) {
context.report({
node,
loc: token.loc.start,
message: "A space is required before '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.insertTextBefore(token, " ");
}
});
}
/**
* Determines if a node is an object type
* @param {ASTNode} node - The node to check.
* @returns {boolean} Whether or not the node is an object type.
*/
function isObjectType(node) {
return node && (node.type === "ObjectExpression" || node.type === "ObjectPattern");
}
/**
* Determines if a node is an array type
* @param {ASTNode} node - The node to check.
* @returns {boolean} Whether or not the node is an array type.
*/
function isArrayType(node) {
return node && (node.type === "ArrayExpression" || node.type === "ArrayPattern");
}
/**
* Validates the spacing around array brackets
* @param {ASTNode} node - The node we're checking for spacing
* @returns {void}
*/
function validateArraySpacing(node) {
if (options.spaced && node.elements.length === 0) {
return;
}
const first = sourceCode.getFirstToken(node),
second = sourceCode.getFirstToken(node, 1),
last = node.typeAnnotation
? sourceCode.getTokenBefore(node.typeAnnotation)
: sourceCode.getLastToken(node),
penultimate = sourceCode.getTokenBefore(last),
firstElement = node.elements[0],
lastElement = node.elements[node.elements.length - 1];
const openingBracketMustBeSpaced =
options.objectsInArraysException && isObjectType(firstElement) ||
options.arraysInArraysException && isArrayType(firstElement) ||
options.singleElementException && node.elements.length === 1
? !options.spaced : options.spaced;
const closingBracketMustBeSpaced =
options.objectsInArraysException && isObjectType(lastElement) ||
options.arraysInArraysException && isArrayType(lastElement) ||
options.singleElementException && node.elements.length === 1
? !options.spaced : options.spaced;
if (astUtils.isTokenOnSameLine(first, second)) {
if (openingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(first, second)) {
reportRequiredBeginningSpace(node, first);
}
if (!openingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(first, second)) {
reportNoBeginningSpace(node, first);
}
}
if (first !== penultimate && astUtils.isTokenOnSameLine(penultimate, last)) {
if (closingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(penultimate, last)) {
reportRequiredEndingSpace(node, last);
}
if (!closingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(penultimate, last)) {
reportNoEndingSpace(node, last);
}
}
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
ArrayPattern: validateArraySpacing,
ArrayExpression: validateArraySpacing
};
}
};

View File

@ -0,0 +1,228 @@
/**
* @fileoverview Rule to enforce return statements in callbacks of array's methods
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/;
const TARGET_METHODS = /^(?:every|filter|find(?:Index)?|map|reduce(?:Right)?|some|sort)$/;
/**
* Checks a given code path segment is reachable.
*
* @param {CodePathSegment} segment - A segment to check.
* @returns {boolean} `true` if the segment is reachable.
*/
function isReachable(segment) {
return segment.reachable;
}
/**
* Gets a readable location.
*
* - FunctionExpression -> the function name or `function` keyword.
* - ArrowFunctionExpression -> `=>` token.
*
* @param {ASTNode} node - A function node to get.
* @param {SourceCode} sourceCode - A source code to get tokens.
* @returns {ASTNode|Token} The node or the token of a location.
*/
function getLocation(node, sourceCode) {
if (node.type === "ArrowFunctionExpression") {
return sourceCode.getTokenBefore(node.body);
}
return node.id || node;
}
/**
* Checks a given node is a MemberExpression node which has the specified name's
* property.
*
* @param {ASTNode} node - A node to check.
* @returns {boolean} `true` if the node is a MemberExpression node which has
* the specified name's property
*/
function isTargetMethod(node) {
return (
node.type === "MemberExpression" &&
TARGET_METHODS.test(astUtils.getStaticPropertyName(node) || "")
);
}
/**
* Checks whether or not a given node is a function expression which is the
* callback of an array method.
*
* @param {ASTNode} node - A node to check. This is one of
* FunctionExpression or ArrowFunctionExpression.
* @returns {boolean} `true` if the node is the callback of an array method.
*/
function isCallbackOfArrayMethod(node) {
while (node) {
const parent = node.parent;
switch (parent.type) {
/*
* Looks up the destination. e.g.,
* foo.every(nativeFoo || function foo() { ... });
*/
case "LogicalExpression":
case "ConditionalExpression":
node = parent;
break;
// If the upper function is IIFE, checks the destination of the return value.
// e.g.
// foo.every((function() {
// // setup...
// return function callback() { ... };
// })());
case "ReturnStatement": {
const func = astUtils.getUpperFunction(parent);
if (func === null || !astUtils.isCallee(func)) {
return false;
}
node = func.parent;
break;
}
// e.g.
// Array.from([], function() {});
// list.every(function() {});
case "CallExpression":
if (astUtils.isArrayFromMethod(parent.callee)) {
return (
parent.arguments.length >= 2 &&
parent.arguments[1] === node
);
}
if (isTargetMethod(parent.callee)) {
return (
parent.arguments.length >= 1 &&
parent.arguments[0] === node
);
}
return false;
// Otherwise this node is not target.
default:
return false;
}
}
/* istanbul ignore next: unreachable */
return false;
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce `return` statements in callbacks of array methods",
category: "Best Practices",
recommended: false
},
schema: []
},
create(context) {
let funcInfo = {
upper: null,
codePath: null,
hasReturn: false,
shouldCheck: false,
node: null
};
/**
* Checks whether or not the last code path segment is reachable.
* Then reports this function if the segment is reachable.
*
* If the last code path segment is reachable, there are paths which are not
* returned or thrown.
*
* @param {ASTNode} node - A node to check.
* @returns {void}
*/
function checkLastSegment(node) {
if (funcInfo.shouldCheck &&
funcInfo.codePath.currentSegments.some(isReachable)
) {
context.report({
node,
loc: getLocation(node, context.getSourceCode()).loc.start,
message: funcInfo.hasReturn
? "Expected to return a value at the end of {{name}}."
: "Expected to return a value in {{name}}.",
data: {
name: astUtils.getFunctionNameWithKind(funcInfo.node)
}
});
}
}
return {
// Stacks this function's information.
onCodePathStart(codePath, node) {
funcInfo = {
upper: funcInfo,
codePath,
hasReturn: false,
shouldCheck:
TARGET_NODE_TYPE.test(node.type) &&
node.body.type === "BlockStatement" &&
isCallbackOfArrayMethod(node) &&
!node.async &&
!node.generator,
node
};
},
// Pops this function's information.
onCodePathEnd() {
funcInfo = funcInfo.upper;
},
// Checks the return statement is valid.
ReturnStatement(node) {
if (funcInfo.shouldCheck) {
funcInfo.hasReturn = true;
if (!node.argument) {
context.report({
node,
message: "{{name}} expected a return value.",
data: {
name: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
}
});
}
}
},
// Reports a given function if the last path is reachable.
"FunctionExpression:exit": checkLastSegment,
"ArrowFunctionExpression:exit": checkLastSegment
};
}
};

View File

@ -0,0 +1,162 @@
/**
* @fileoverview Rule to require braces in arrow function body.
* @author Alberto Rodríguez
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require braces around arrow function bodies",
category: "ECMAScript 6",
recommended: false
},
schema: {
anyOf: [
{
type: "array",
items: [
{
enum: ["always", "never"]
}
],
minItems: 0,
maxItems: 1
},
{
type: "array",
items: [
{
enum: ["as-needed"]
},
{
type: "object",
properties: {
requireReturnForObjectLiteral: { type: "boolean" }
},
additionalProperties: false
}
],
minItems: 0,
maxItems: 2
}
]
},
fixable: "code"
},
create(context) {
const options = context.options;
const always = options[0] === "always";
const asNeeded = !options[0] || options[0] === "as-needed";
const never = options[0] === "never";
const requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral;
const sourceCode = context.getSourceCode();
/**
* Determines whether a arrow function body needs braces
* @param {ASTNode} node The arrow function node.
* @returns {void}
*/
function validate(node) {
const arrowBody = node.body;
if (arrowBody.type === "BlockStatement") {
const blockBody = arrowBody.body;
if (blockBody.length !== 1 && !never) {
return;
}
if (asNeeded && requireReturnForObjectLiteral && blockBody[0].type === "ReturnStatement" &&
blockBody[0].argument && blockBody[0].argument.type === "ObjectExpression") {
return;
}
if (never || asNeeded && blockBody[0].type === "ReturnStatement") {
context.report({
node,
loc: arrowBody.loc.start,
message: "Unexpected block statement surrounding arrow body.",
fix(fixer) {
if (blockBody.length !== 1 || blockBody[0].type !== "ReturnStatement" || !blockBody[0].argument) {
return null;
}
const sourceText = sourceCode.getText();
const returnKeyword = sourceCode.getFirstToken(blockBody[0]);
const firstValueToken = sourceCode.getTokenAfter(returnKeyword);
let lastValueToken = sourceCode.getLastToken(blockBody[0]);
if (astUtils.isSemicolonToken(lastValueToken)) {
/* The last token of the returned value is the last token of the ReturnExpression (if
* the ReturnExpression has no semicolon), or the second-to-last token (if the ReturnExpression
* has a semicolon).
*/
lastValueToken = sourceCode.getTokenBefore(lastValueToken);
}
const tokenAfterArrowBody = sourceCode.getTokenAfter(arrowBody);
if (tokenAfterArrowBody && tokenAfterArrowBody.type === "Punctuator" && /^[([/`+-]/.test(tokenAfterArrowBody.value)) {
// Don't do a fix if the next token would cause ASI issues when preceded by the returned value.
return null;
}
const textBeforeReturn = sourceText.slice(arrowBody.range[0] + 1, returnKeyword.range[0]);
const textBetweenReturnAndValue = sourceText.slice(returnKeyword.range[1], firstValueToken.range[0]);
const rawReturnValueText = sourceText.slice(firstValueToken.range[0], lastValueToken.range[1]);
const returnValueText = astUtils.isOpeningBraceToken(firstValueToken) ? `(${rawReturnValueText})` : rawReturnValueText;
const textAfterValue = sourceText.slice(lastValueToken.range[1], blockBody[0].range[1] - 1);
const textAfterReturnStatement = sourceText.slice(blockBody[0].range[1], arrowBody.range[1] - 1);
/*
* For fixes that only contain spaces around the return value, remove the extra spaces.
* This avoids ugly fixes that end up with extra spaces after the arrow, e.g. `() => 0 ;`
*/
return fixer.replaceText(
arrowBody,
(textBeforeReturn + textBetweenReturnAndValue).replace(/^\s*$/, "") + returnValueText + (textAfterValue + textAfterReturnStatement).replace(/^\s*$/, "")
);
}
});
}
} else {
if (always || (asNeeded && requireReturnForObjectLiteral && arrowBody.type === "ObjectExpression")) {
context.report({
node,
loc: arrowBody.loc.start,
message: "Expected block statement surrounding arrow body.",
fix(fixer) {
const lastTokenBeforeBody = sourceCode.getLastTokenBetween(sourceCode.getFirstToken(node), arrowBody, astUtils.isNotOpeningParenToken);
const firstBodyToken = sourceCode.getTokenAfter(lastTokenBeforeBody);
return fixer.replaceTextRange(
[firstBodyToken.range[0], node.range[1]],
`{return ${sourceCode.getText().slice(firstBodyToken.range[0], node.range[1])}}`
);
}
});
}
}
}
return {
ArrowFunctionExpression: validate
};
}
};

View File

@ -0,0 +1,150 @@
/**
* @fileoverview Rule to require parens in arrow function arguments.
* @author Jxck
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require parentheses around arrow function arguments",
category: "ECMAScript 6",
recommended: false
},
fixable: "code",
schema: [
{
enum: ["always", "as-needed"]
},
{
type: "object",
properties: {
requireForBlockBody: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
const message = "Expected parentheses around arrow function argument.";
const asNeededMessage = "Unexpected parentheses around single function argument.";
const asNeeded = context.options[0] === "as-needed";
const requireForBlockBodyMessage = "Unexpected parentheses around single function argument having a body with no curly braces";
const requireForBlockBodyNoParensMessage = "Expected parentheses around arrow function argument having a body with curly braces.";
const requireForBlockBody = asNeeded && context.options[1] && context.options[1].requireForBlockBody === true;
const sourceCode = context.getSourceCode();
/**
* Determines whether a arrow function argument end with `)`
* @param {ASTNode} node The arrow function node.
* @returns {void}
*/
function parens(node) {
const token = sourceCode.getFirstToken(node, node.async ? 1 : 0);
// "as-needed", { "requireForBlockBody": true }: x => x
if (
requireForBlockBody &&
node.params.length === 1 &&
node.params[0].type === "Identifier" &&
!node.params[0].typeAnnotation &&
node.body.type !== "BlockStatement" &&
!node.returnType
) {
if (astUtils.isOpeningParenToken(token)) {
context.report({
node,
message: requireForBlockBodyMessage,
fix(fixer) {
const paramToken = context.getTokenAfter(token);
const closingParenToken = context.getTokenAfter(paramToken);
return fixer.replaceTextRange([
token.range[0],
closingParenToken.range[1]
], paramToken.value);
}
});
}
return;
}
if (
requireForBlockBody &&
node.body.type === "BlockStatement"
) {
if (!astUtils.isOpeningParenToken(token)) {
context.report({
node,
message: requireForBlockBodyNoParensMessage,
fix(fixer) {
return fixer.replaceText(token, `(${token.value})`);
}
});
}
return;
}
// "as-needed": x => x
if (asNeeded &&
node.params.length === 1 &&
node.params[0].type === "Identifier" &&
!node.params[0].typeAnnotation &&
!node.returnType
) {
if (astUtils.isOpeningParenToken(token)) {
context.report({
node,
message: asNeededMessage,
fix(fixer) {
const paramToken = context.getTokenAfter(token);
const closingParenToken = context.getTokenAfter(paramToken);
return fixer.replaceTextRange([
token.range[0],
closingParenToken.range[1]
], paramToken.value);
}
});
}
return;
}
if (token.type === "Identifier") {
const after = sourceCode.getTokenAfter(token);
// (x) => x
if (after.value !== ")") {
context.report({
node,
message,
fix(fixer) {
return fixer.replaceText(token, `(${token.value})`);
}
});
}
}
}
return {
ArrowFunctionExpression: parens
};
}
};

View File

@ -0,0 +1,149 @@
/**
* @fileoverview Rule to define spacing before/after arrow function's arrow.
* @author Jxck
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent spacing before and after the arrow in arrow functions",
category: "ECMAScript 6",
recommended: false
},
fixable: "whitespace",
schema: [
{
type: "object",
properties: {
before: {
type: "boolean"
},
after: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
// merge rules with default
const rule = { before: true, after: true },
option = context.options[0] || {};
rule.before = option.before !== false;
rule.after = option.after !== false;
const sourceCode = context.getSourceCode();
/**
* Get tokens of arrow(`=>`) and before/after arrow.
* @param {ASTNode} node The arrow function node.
* @returns {Object} Tokens of arrow and before/after arrow.
*/
function getTokens(node) {
const arrow = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken);
return {
before: sourceCode.getTokenBefore(arrow),
arrow,
after: sourceCode.getTokenAfter(arrow)
};
}
/**
* Count spaces before/after arrow(`=>`) token.
* @param {Object} tokens Tokens before/after arrow.
* @returns {Object} count of space before/after arrow.
*/
function countSpaces(tokens) {
const before = tokens.arrow.range[0] - tokens.before.range[1];
const after = tokens.after.range[0] - tokens.arrow.range[1];
return { before, after };
}
/**
* Determines whether space(s) before after arrow(`=>`) is satisfy rule.
* if before/after value is `true`, there should be space(s).
* if before/after value is `false`, there should be no space.
* @param {ASTNode} node The arrow function node.
* @returns {void}
*/
function spaces(node) {
const tokens = getTokens(node);
const countSpace = countSpaces(tokens);
if (rule.before) {
// should be space(s) before arrow
if (countSpace.before === 0) {
context.report({
node: tokens.before,
message: "Missing space before =>.",
fix(fixer) {
return fixer.insertTextBefore(tokens.arrow, " ");
}
});
}
} else {
// should be no space before arrow
if (countSpace.before > 0) {
context.report({
node: tokens.before,
message: "Unexpected space before =>.",
fix(fixer) {
return fixer.removeRange([tokens.before.range[1], tokens.arrow.range[0]]);
}
});
}
}
if (rule.after) {
// should be space(s) after arrow
if (countSpace.after === 0) {
context.report({
node: tokens.after,
message: "Missing space after =>.",
fix(fixer) {
return fixer.insertTextAfter(tokens.arrow, " ");
}
});
}
} else {
// should be no space after arrow
if (countSpace.after > 0) {
context.report({
node: tokens.after,
message: "Unexpected space after =>.",
fix(fixer) {
return fixer.removeRange([tokens.arrow.range[1], tokens.after.range[0]]);
}
});
}
}
}
return {
ArrowFunctionExpression: spaces
};
}
};

View File

@ -0,0 +1,115 @@
/**
* @fileoverview Rule to check for "block scoped" variables by binding context
* @author Matt DuVall <http://www.mattduvall.com>
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce the use of variables within the scope they are defined",
category: "Best Practices",
recommended: false
},
schema: []
},
create(context) {
let stack = [];
/**
* Makes a block scope.
* @param {ASTNode} node - A node of a scope.
* @returns {void}
*/
function enterScope(node) {
stack.push(node.range);
}
/**
* Pops the last block scope.
* @returns {void}
*/
function exitScope() {
stack.pop();
}
/**
* Reports a given reference.
* @param {escope.Reference} reference - A reference to report.
* @returns {void}
*/
function report(reference) {
const identifier = reference.identifier;
context.report({ node: identifier, message: "'{{name}}' used outside of binding context.", data: { name: identifier.name } });
}
/**
* Finds and reports references which are outside of valid scopes.
* @param {ASTNode} node - A node to get variables.
* @returns {void}
*/
function checkForVariables(node) {
if (node.kind !== "var") {
return;
}
// Defines a predicate to check whether or not a given reference is outside of valid scope.
const scopeRange = stack[stack.length - 1];
/**
* Check if a reference is out of scope
* @param {ASTNode} reference node to examine
* @returns {boolean} True is its outside the scope
* @private
*/
function isOutsideOfScope(reference) {
const idRange = reference.identifier.range;
return idRange[0] < scopeRange[0] || idRange[1] > scopeRange[1];
}
// Gets declared variables, and checks its references.
const variables = context.getDeclaredVariables(node);
for (let i = 0; i < variables.length; ++i) {
// Reports.
variables[i]
.references
.filter(isOutsideOfScope)
.forEach(report);
}
}
return {
Program(node) {
stack = [node.range];
},
// Manages scopes.
BlockStatement: enterScope,
"BlockStatement:exit": exitScope,
ForStatement: enterScope,
"ForStatement:exit": exitScope,
ForInStatement: enterScope,
"ForInStatement:exit": exitScope,
ForOfStatement: enterScope,
"ForOfStatement:exit": exitScope,
SwitchStatement: enterScope,
"SwitchStatement:exit": exitScope,
CatchClause: enterScope,
"CatchClause:exit": exitScope,
// Finds and reports references which are outside of valid scope.
VariableDeclaration: checkForVariables
};
}
};

View File

@ -0,0 +1,137 @@
/**
* @fileoverview A rule to disallow or enforce spaces inside of single line blocks.
* @author Toru Nagashima
*/
"use strict";
const util = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent spacing inside single-line blocks",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{ enum: ["always", "never"] }
]
},
create(context) {
const always = (context.options[0] !== "never"),
message = always ? "Requires a space" : "Unexpected space(s)",
sourceCode = context.getSourceCode();
/**
* Gets the open brace token from a given node.
* @param {ASTNode} node - A BlockStatement/SwitchStatement node to get.
* @returns {Token} The token of the open brace.
*/
function getOpenBrace(node) {
if (node.type === "SwitchStatement") {
if (node.cases.length > 0) {
return sourceCode.getTokenBefore(node.cases[0]);
}
return sourceCode.getLastToken(node, 1);
}
return sourceCode.getFirstToken(node);
}
/**
* Checks whether or not:
* - given tokens are on same line.
* - there is/isn't a space between given tokens.
* @param {Token} left - A token to check.
* @param {Token} right - The token which is next to `left`.
* @returns {boolean}
* When the option is `"always"`, `true` if there are one or more spaces between given tokens.
* When the option is `"never"`, `true` if there are not any spaces between given tokens.
* If given tokens are not on same line, it's always `true`.
*/
function isValid(left, right) {
return (
!util.isTokenOnSameLine(left, right) ||
sourceCode.isSpaceBetweenTokens(left, right) === always
);
}
/**
* Reports invalid spacing style inside braces.
* @param {ASTNode} node - A BlockStatement/SwitchStatement node to get.
* @returns {void}
*/
function checkSpacingInsideBraces(node) {
// Gets braces and the first/last token of content.
const openBrace = getOpenBrace(node);
const closeBrace = sourceCode.getLastToken(node);
const firstToken = sourceCode.getTokenAfter(openBrace, { includeComments: true });
const lastToken = sourceCode.getTokenBefore(closeBrace, { includeComments: true });
// Skip if the node is invalid or empty.
if (openBrace.type !== "Punctuator" ||
openBrace.value !== "{" ||
closeBrace.type !== "Punctuator" ||
closeBrace.value !== "}" ||
firstToken === closeBrace
) {
return;
}
// Skip line comments for option never
if (!always && firstToken.type === "Line") {
return;
}
// Check.
if (!isValid(openBrace, firstToken)) {
context.report({
node,
loc: openBrace.loc.start,
message: "{{message}} after '{'.",
data: {
message
},
fix(fixer) {
if (always) {
return fixer.insertTextBefore(firstToken, " ");
}
return fixer.removeRange([openBrace.range[1], firstToken.range[0]]);
}
});
}
if (!isValid(lastToken, closeBrace)) {
context.report({
node,
loc: closeBrace.loc.start,
message: "{{message}} before '}'.",
data: {
message
},
fix(fixer) {
if (always) {
return fixer.insertTextAfter(lastToken, " ");
}
return fixer.removeRange([lastToken.range[1], closeBrace.range[0]]);
}
});
}
}
return {
BlockStatement: checkSpacingInsideBraces,
SwitchStatement: checkSpacingInsideBraces
};
}
};

View File

@ -0,0 +1,180 @@
/**
* @fileoverview Rule to flag block statements that do not use the one true brace style
* @author Ian Christian Myers
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent brace style for blocks",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
enum: ["1tbs", "stroustrup", "allman"]
},
{
type: "object",
properties: {
allowSingleLine: {
type: "boolean"
}
},
additionalProperties: false
}
],
fixable: "whitespace"
},
create(context) {
const style = context.options[0] || "1tbs",
params = context.options[1] || {},
sourceCode = context.getSourceCode();
const OPEN_MESSAGE = "Opening curly brace does not appear on the same line as controlling statement.",
OPEN_MESSAGE_ALLMAN = "Opening curly brace appears on the same line as controlling statement.",
BODY_MESSAGE = "Statement inside of curly braces should be on next line.",
CLOSE_MESSAGE = "Closing curly brace does not appear on the same line as the subsequent block.",
CLOSE_MESSAGE_SINGLE = "Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.",
CLOSE_MESSAGE_STROUSTRUP_ALLMAN = "Closing curly brace appears on the same line as the subsequent block.";
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Fixes a place where a newline unexpectedly appears
* @param {Token} firstToken The token before the unexpected newline
* @param {Token} secondToken The token after the unexpected newline
* @returns {Function} A fixer function to remove the newlines between the tokens
*/
function removeNewlineBetween(firstToken, secondToken) {
const textRange = [firstToken.range[1], secondToken.range[0]];
const textBetween = sourceCode.text.slice(textRange[0], textRange[1]);
const NEWLINE_REGEX = astUtils.createGlobalLinebreakMatcher();
// Don't do a fix if there is a comment between the tokens
return fixer => fixer.replaceTextRange(textRange, textBetween.trim() ? null : textBetween.replace(NEWLINE_REGEX, ""));
}
/**
* Validates a pair of curly brackets based on the user's config
* @param {Token} openingCurly The opening curly bracket
* @param {Token} closingCurly The closing curly bracket
* @returns {void}
*/
function validateCurlyPair(openingCurly, closingCurly) {
const tokenBeforeOpeningCurly = sourceCode.getTokenBefore(openingCurly);
const tokenAfterOpeningCurly = sourceCode.getTokenAfter(openingCurly);
const tokenBeforeClosingCurly = sourceCode.getTokenBefore(closingCurly);
const singleLineException = params.allowSingleLine && astUtils.isTokenOnSameLine(openingCurly, closingCurly);
if (style !== "allman" && !astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly)) {
context.report({
node: openingCurly,
message: OPEN_MESSAGE,
fix: removeNewlineBetween(tokenBeforeOpeningCurly, openingCurly)
});
}
if (style === "allman" && astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly) && !singleLineException) {
context.report({
node: openingCurly,
message: OPEN_MESSAGE_ALLMAN,
fix: fixer => fixer.insertTextBefore(openingCurly, "\n")
});
}
if (astUtils.isTokenOnSameLine(openingCurly, tokenAfterOpeningCurly) && tokenAfterOpeningCurly !== closingCurly && !singleLineException) {
context.report({
node: openingCurly,
message: BODY_MESSAGE,
fix: fixer => fixer.insertTextAfter(openingCurly, "\n")
});
}
if (tokenBeforeClosingCurly !== openingCurly && !singleLineException && astUtils.isTokenOnSameLine(tokenBeforeClosingCurly, closingCurly)) {
context.report({
node: closingCurly,
message: CLOSE_MESSAGE_SINGLE,
fix: fixer => fixer.insertTextBefore(closingCurly, "\n")
});
}
}
/**
* Validates the location of a token that appears before a keyword (e.g. a newline before `else`)
* @param {Token} curlyToken The closing curly token. This is assumed to precede a keyword token (such as `else` or `finally`).
* @returns {void}
*/
function validateCurlyBeforeKeyword(curlyToken) {
const keywordToken = sourceCode.getTokenAfter(curlyToken);
if (style === "1tbs" && !astUtils.isTokenOnSameLine(curlyToken, keywordToken)) {
context.report({
node: curlyToken,
message: CLOSE_MESSAGE,
fix: removeNewlineBetween(curlyToken, keywordToken)
});
}
if (style !== "1tbs" && astUtils.isTokenOnSameLine(curlyToken, keywordToken)) {
context.report({
node: curlyToken,
message: CLOSE_MESSAGE_STROUSTRUP_ALLMAN,
fix: fixer => fixer.insertTextAfter(curlyToken, "\n")
});
}
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
BlockStatement(node) {
if (!astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) {
validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node));
}
},
ClassBody(node) {
validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node));
},
SwitchStatement(node) {
const closingCurly = sourceCode.getLastToken(node);
const openingCurly = sourceCode.getTokenBefore(node.cases.length ? node.cases[0] : closingCurly);
validateCurlyPair(openingCurly, closingCurly);
},
IfStatement(node) {
if (node.consequent.type === "BlockStatement" && node.alternate) {
// Handle the keyword after the `if` block (before `else`)
validateCurlyBeforeKeyword(sourceCode.getLastToken(node.consequent));
}
},
TryStatement(node) {
// Handle the keyword after the `try` block (before `catch` or `finally`)
validateCurlyBeforeKeyword(sourceCode.getLastToken(node.block));
if (node.handler && node.finalizer) {
// Handle the keyword after the `catch` block (before `finally`)
validateCurlyBeforeKeyword(sourceCode.getLastToken(node.handler.body));
}
}
};
}
};

View File

@ -0,0 +1,174 @@
/**
* @fileoverview Enforce return after a callback.
* @author Jamund Ferguson
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require `return` statements after callbacks",
category: "Node.js and CommonJS",
recommended: false
},
schema: [{
type: "array",
items: { type: "string" }
}]
},
create(context) {
const callbacks = context.options[0] || ["callback", "cb", "next"],
sourceCode = context.getSourceCode();
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Find the closest parent matching a list of types.
* @param {ASTNode} node The node whose parents we are searching
* @param {Array} types The node types to match
* @returns {ASTNode} The matched node or undefined.
*/
function findClosestParentOfType(node, types) {
if (!node.parent) {
return null;
}
if (types.indexOf(node.parent.type) === -1) {
return findClosestParentOfType(node.parent, types);
}
return node.parent;
}
/**
* Check to see if a node contains only identifers
* @param {ASTNode} node The node to check
* @returns {boolean} Whether or not the node contains only identifers
*/
function containsOnlyIdentifiers(node) {
if (node.type === "Identifier") {
return true;
}
if (node.type === "MemberExpression") {
if (node.object.type === "Identifier") {
return true;
} else if (node.object.type === "MemberExpression") {
return containsOnlyIdentifiers(node.object);
}
}
return false;
}
/**
* Check to see if a CallExpression is in our callback list.
* @param {ASTNode} node The node to check against our callback names list.
* @returns {boolean} Whether or not this function matches our callback name.
*/
function isCallback(node) {
return containsOnlyIdentifiers(node.callee) && callbacks.indexOf(sourceCode.getText(node.callee)) > -1;
}
/**
* Determines whether or not the callback is part of a callback expression.
* @param {ASTNode} node The callback node
* @param {ASTNode} parentNode The expression node
* @returns {boolean} Whether or not this is part of a callback expression
*/
function isCallbackExpression(node, parentNode) {
// ensure the parent node exists and is an expression
if (!parentNode || parentNode.type !== "ExpressionStatement") {
return false;
}
// cb()
if (parentNode.expression === node) {
return true;
}
// special case for cb && cb() and similar
if (parentNode.expression.type === "BinaryExpression" || parentNode.expression.type === "LogicalExpression") {
if (parentNode.expression.right === node) {
return true;
}
}
return false;
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
CallExpression(node) {
// if we're not a callback we can return
if (!isCallback(node)) {
return;
}
// find the closest block, return or loop
const closestBlock = findClosestParentOfType(node, ["BlockStatement", "ReturnStatement", "ArrowFunctionExpression"]) || {};
// if our parent is a return we know we're ok
if (closestBlock.type === "ReturnStatement") {
return;
}
// arrow functions don't always have blocks and implicitly return
if (closestBlock.type === "ArrowFunctionExpression") {
return;
}
// block statements are part of functions and most if statements
if (closestBlock.type === "BlockStatement") {
// find the last item in the block
const lastItem = closestBlock.body[closestBlock.body.length - 1];
// if the callback is the last thing in a block that might be ok
if (isCallbackExpression(node, lastItem)) {
const parentType = closestBlock.parent.type;
// but only if the block is part of a function
if (parentType === "FunctionExpression" ||
parentType === "FunctionDeclaration" ||
parentType === "ArrowFunctionExpression"
) {
return;
}
}
// ending a block with a return is also ok
if (lastItem.type === "ReturnStatement") {
// but only if the callback is immediately before
if (isCallbackExpression(node, closestBlock.body[closestBlock.body.length - 2])) {
return;
}
}
}
// as long as you're the child of a function at this point you should be asked to return
if (findClosestParentOfType(node, ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"])) {
context.report({ node, message: "Expected return with your callback function." });
}
}
};
}
};

View File

@ -0,0 +1,143 @@
/**
* @fileoverview Rule to flag non-camelcased identifiers
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce camelcase naming convention",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
type: "object",
properties: {
properties: {
enum: ["always", "never"]
}
},
additionalProperties: false
}
]
},
create(context) {
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
// contains reported nodes to avoid reporting twice on destructuring with shorthand notation
const reported = [];
const ALLOWED_PARENT_TYPES = new Set(["CallExpression", "NewExpression"]);
/**
* Checks if a string contains an underscore and isn't all upper-case
* @param {string} name The string to check.
* @returns {boolean} if the string is underscored
* @private
*/
function isUnderscored(name) {
// if there's an underscore, it might be A_CONSTANT, which is okay
return name.indexOf("_") > -1 && name !== name.toUpperCase();
}
/**
* Reports an AST node as a rule violation.
* @param {ASTNode} node The node to report.
* @returns {void}
* @private
*/
function report(node) {
if (reported.indexOf(node) < 0) {
reported.push(node);
context.report({ node, message: "Identifier '{{name}}' is not in camel case.", data: { name: node.name } });
}
}
const options = context.options[0] || {};
let properties = options.properties || "";
if (properties !== "always" && properties !== "never") {
properties = "always";
}
return {
Identifier(node) {
/*
* Leading and trailing underscores are commonly used to flag
* private/protected identifiers, strip them
*/
const name = node.name.replace(/^_+|_+$/g, ""),
effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent;
// MemberExpressions get special rules
if (node.parent.type === "MemberExpression") {
// "never" check properties
if (properties === "never") {
return;
}
// Always report underscored object names
if (node.parent.object.type === "Identifier" &&
node.parent.object.name === node.name &&
isUnderscored(name)) {
report(node);
// Report AssignmentExpressions only if they are the left side of the assignment
} else if (effectiveParent.type === "AssignmentExpression" &&
isUnderscored(name) &&
(effectiveParent.right.type !== "MemberExpression" ||
effectiveParent.left.type === "MemberExpression" &&
effectiveParent.left.property.name === node.name)) {
report(node);
}
// Properties have their own rules
} else if (node.parent.type === "Property") {
// "never" check properties
if (properties === "never") {
return;
}
if (node.parent.parent && node.parent.parent.type === "ObjectPattern" &&
node.parent.key === node && node.parent.value !== node) {
return;
}
if (isUnderscored(name) && !ALLOWED_PARENT_TYPES.has(effectiveParent.type)) {
report(node);
}
// Check if it's an import specifier
} else if (["ImportSpecifier", "ImportNamespaceSpecifier", "ImportDefaultSpecifier"].indexOf(node.parent.type) >= 0) {
// Report only if the local imported identifier is underscored
if (node.parent.local && node.parent.local.name === node.name && isUnderscored(name)) {
report(node);
}
// Report anything that is underscored that isn't a CallExpression
} else if (isUnderscored(name) && !ALLOWED_PARENT_TYPES.has(effectiveParent.type)) {
report(node);
}
}
};
}
};

View File

@ -0,0 +1,302 @@
/**
* @fileoverview enforce or disallow capitalization of the first letter of a comment
* @author Kevin Partington
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const LETTER_PATTERN = require("../util/patterns/letters");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const ALWAYS_MESSAGE = "Comments should not begin with a lowercase character",
NEVER_MESSAGE = "Comments should not begin with an uppercase character",
DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
WHITESPACE = /\s/g,
MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/, // TODO: Combine w/ max-len pattern?
DEFAULTS = {
ignorePattern: null,
ignoreInlineComments: false,
ignoreConsecutiveComments: false
};
/*
* Base schema body for defining the basic capitalization rule, ignorePattern,
* and ignoreInlineComments values.
* This can be used in a few different ways in the actual schema.
*/
const SCHEMA_BODY = {
type: "object",
properties: {
ignorePattern: {
type: "string"
},
ignoreInlineComments: {
type: "boolean"
},
ignoreConsecutiveComments: {
type: "boolean"
}
},
additionalProperties: false
};
/**
* Get normalized options for either block or line comments from the given
* user-provided options.
* - If the user-provided options is just a string, returns a normalized
* set of options using default values for all other options.
* - If the user-provided options is an object, then a normalized option
* set is returned. Options specified in overrides will take priority
* over options specified in the main options object, which will in
* turn take priority over the rule's defaults.
*
* @param {Object|string} rawOptions The user-provided options.
* @param {string} which Either "line" or "block".
* @returns {Object} The normalized options.
*/
function getNormalizedOptions(rawOptions, which) {
if (!rawOptions) {
return Object.assign({}, DEFAULTS);
}
return Object.assign({}, DEFAULTS, rawOptions[which] || rawOptions);
}
/**
* Get normalized options for block and line comments.
*
* @param {Object|string} rawOptions The user-provided options.
* @returns {Object} An object with "Line" and "Block" keys and corresponding
* normalized options objects.
*/
function getAllNormalizedOptions(rawOptions) {
return {
Line: getNormalizedOptions(rawOptions, "line"),
Block: getNormalizedOptions(rawOptions, "block")
};
}
/**
* Creates a regular expression for each ignorePattern defined in the rule
* options.
*
* This is done in order to avoid invoking the RegExp constructor repeatedly.
*
* @param {Object} normalizedOptions The normalized rule options.
* @returns {void}
*/
function createRegExpForIgnorePatterns(normalizedOptions) {
Object.keys(normalizedOptions).forEach(key => {
const ignorePatternStr = normalizedOptions[key].ignorePattern;
if (ignorePatternStr) {
const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`);
normalizedOptions[key].ignorePatternRegExp = regExp;
}
});
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce or disallow capitalization of the first letter of a comment",
category: "Stylistic Issues",
recommended: false
},
fixable: "code",
schema: [
{ enum: ["always", "never"] },
{
oneOf: [
SCHEMA_BODY,
{
type: "object",
properties: {
line: SCHEMA_BODY,
block: SCHEMA_BODY
},
additionalProperties: false
}
]
}
]
},
create(context) {
const capitalize = context.options[0] || "always",
normalizedOptions = getAllNormalizedOptions(context.options[1]),
sourceCode = context.getSourceCode();
createRegExpForIgnorePatterns(normalizedOptions);
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
/**
* Checks whether a comment is an inline comment.
*
* For the purpose of this rule, a comment is inline if:
* 1. The comment is preceded by a token on the same line; and
* 2. The command is followed by a token on the same line.
*
* Note that the comment itself need not be single-line!
*
* Also, it follows from this definition that only block comments can
* be considered as possibly inline. This is because line comments
* would consume any following tokens on the same line as the comment.
*
* @param {ASTNode} comment The comment node to check.
* @returns {boolean} True if the comment is an inline comment, false
* otherwise.
*/
function isInlineComment(comment) {
const previousToken = sourceCode.getTokenBefore(comment, { includeComments: true }),
nextToken = sourceCode.getTokenAfter(comment, { includeComments: true });
return Boolean(
previousToken &&
nextToken &&
comment.loc.start.line === previousToken.loc.end.line &&
comment.loc.end.line === nextToken.loc.start.line
);
}
/**
* Determine if a comment follows another comment.
*
* @param {ASTNode} comment The comment to check.
* @returns {boolean} True if the comment follows a valid comment.
*/
function isConsecutiveComment(comment) {
const previousTokenOrComment = sourceCode.getTokenBefore(comment, { includeComments: true });
return Boolean(
previousTokenOrComment &&
["Block", "Line"].indexOf(previousTokenOrComment.type) !== -1
);
}
/**
* Check a comment to determine if it is valid for this rule.
*
* @param {ASTNode} comment The comment node to process.
* @param {Object} options The options for checking this comment.
* @returns {boolean} True if the comment is valid, false otherwise.
*/
function isCommentValid(comment, options) {
// 1. Check for default ignore pattern.
if (DEFAULT_IGNORE_PATTERN.test(comment.value)) {
return true;
}
// 2. Check for custom ignore pattern.
const commentWithoutAsterisks = comment.value
.replace(/\*/g, "");
if (options.ignorePatternRegExp && options.ignorePatternRegExp.test(commentWithoutAsterisks)) {
return true;
}
// 3. Check for inline comments.
if (options.ignoreInlineComments && isInlineComment(comment)) {
return true;
}
// 4. Is this a consecutive comment (and are we tolerating those)?
if (options.ignoreConsecutiveComments && isConsecutiveComment(comment)) {
return true;
}
// 5. Does the comment start with a possible URL?
if (MAYBE_URL.test(commentWithoutAsterisks)) {
return true;
}
// 6. Is the initial word character a letter?
const commentWordCharsOnly = commentWithoutAsterisks
.replace(WHITESPACE, "");
if (commentWordCharsOnly.length === 0) {
return true;
}
const firstWordChar = commentWordCharsOnly[0];
if (!LETTER_PATTERN.test(firstWordChar)) {
return true;
}
// 7. Check the case of the initial word character.
const isUppercase = firstWordChar !== firstWordChar.toLocaleLowerCase(),
isLowercase = firstWordChar !== firstWordChar.toLocaleUpperCase();
if (capitalize === "always" && isLowercase) {
return false;
} else if (capitalize === "never" && isUppercase) {
return false;
}
return true;
}
/**
* Process a comment to determine if it needs to be reported.
*
* @param {ASTNode} comment The comment node to process.
* @returns {void}
*/
function processComment(comment) {
const options = normalizedOptions[comment.type],
commentValid = isCommentValid(comment, options);
if (!commentValid) {
const message = capitalize === "always"
? ALWAYS_MESSAGE
: NEVER_MESSAGE;
context.report({
node: null, // Intentionally using loc instead
loc: comment.loc,
message,
fix(fixer) {
const match = comment.value.match(LETTER_PATTERN);
return fixer.replaceTextRange(
// Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*)
[comment.range[0] + match.index + 2, comment.range[0] + match.index + 3],
capitalize === "always" ? match[0].toLocaleUpperCase() : match[0].toLocaleLowerCase()
);
}
});
}
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
Program() {
const comments = sourceCode.getAllComments();
comments.forEach(processComment);
}
};
}
};

View File

@ -0,0 +1,110 @@
/**
* @fileoverview Rule to enforce that all class methods use 'this'.
* @author Patrick Williams
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce that class methods utilize `this`",
category: "Best Practices",
recommended: false
},
schema: [{
type: "object",
properties: {
exceptMethods: {
type: "array",
items: {
type: "string"
}
}
},
additionalProperties: false
}]
},
create(context) {
const config = context.options[0] ? Object.assign({}, context.options[0]) : {};
const exceptMethods = new Set(config.exceptMethods || []);
const stack = [];
/**
* Initializes the current context to false and pushes it onto the stack.
* These booleans represent whether 'this' has been used in the context.
* @returns {void}
* @private
*/
function enterFunction() {
stack.push(false);
}
/**
* Check if the node is an instance method
* @param {ASTNode} node - node to check
* @returns {boolean} True if its an instance method
* @private
*/
function isInstanceMethod(node) {
return !node.static && node.kind !== "constructor" && node.type === "MethodDefinition";
}
/**
* Check if the node is an instance method not excluded by config
* @param {ASTNode} node - node to check
* @returns {boolean} True if it is an instance method, and not excluded by config
* @private
*/
function isIncludedInstanceMethod(node) {
return isInstanceMethod(node) && !exceptMethods.has(node.key.name);
}
/**
* Checks if we are leaving a function that is a method, and reports if 'this' has not been used.
* Static methods and the constructor are exempt.
* Then pops the context off the stack.
* @param {ASTNode} node - A function node that was entered.
* @returns {void}
* @private
*/
function exitFunction(node) {
const methodUsesThis = stack.pop();
if (isIncludedInstanceMethod(node.parent) && !methodUsesThis) {
context.report({
node,
message: "Expected 'this' to be used by class method '{{classMethod}}'.",
data: {
classMethod: node.parent.key.name
}
});
}
}
/**
* Mark the current context as having used 'this'.
* @returns {void}
* @private
*/
function markThisUsed() {
if (stack.length) {
stack[stack.length - 1] = true;
}
}
return {
FunctionDeclaration: enterFunction,
"FunctionDeclaration:exit": exitFunction,
FunctionExpression: enterFunction,
"FunctionExpression:exit": exitFunction,
ThisExpression: markThisUsed,
Super: markThisUsed
};
}
};

View File

@ -0,0 +1,337 @@
/**
* @fileoverview Rule to forbid or enforce dangling commas.
* @author Ian Christian Myers
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const DEFAULT_OPTIONS = Object.freeze({
arrays: "never",
objects: "never",
imports: "never",
exports: "never",
functions: "ignore"
});
/**
* Checks whether or not a trailing comma is allowed in a given node.
* If the `lastItem` is `RestElement` or `RestProperty`, it disallows trailing commas.
*
* @param {ASTNode} lastItem - The node of the last element in the given node.
* @returns {boolean} `true` if a trailing comma is allowed.
*/
function isTrailingCommaAllowed(lastItem) {
return !(
lastItem.type === "RestElement" ||
lastItem.type === "RestProperty" ||
lastItem.type === "ExperimentalRestProperty"
);
}
/**
* Normalize option value.
*
* @param {string|Object|undefined} optionValue - The 1st option value to normalize.
* @returns {Object} The normalized option value.
*/
function normalizeOptions(optionValue) {
if (typeof optionValue === "string") {
return {
arrays: optionValue,
objects: optionValue,
imports: optionValue,
exports: optionValue,
// For backward compatibility, always ignore functions.
functions: "ignore"
};
}
if (typeof optionValue === "object" && optionValue !== null) {
return {
arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays,
objects: optionValue.objects || DEFAULT_OPTIONS.objects,
imports: optionValue.imports || DEFAULT_OPTIONS.imports,
exports: optionValue.exports || DEFAULT_OPTIONS.exports,
functions: optionValue.functions || DEFAULT_OPTIONS.functions
};
}
return DEFAULT_OPTIONS;
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require or disallow trailing commas",
category: "Stylistic Issues",
recommended: false
},
fixable: "code",
schema: [
{
defs: {
value: {
enum: [
"always",
"always-multiline",
"only-multiline",
"never"
]
},
valueWithIgnore: {
anyOf: [
{
$ref: "#/defs/value"
},
{
enum: ["ignore"]
}
]
}
},
anyOf: [
{
$ref: "#/defs/value"
},
{
type: "object",
properties: {
arrays: { $refs: "#/defs/valueWithIgnore" },
objects: { $refs: "#/defs/valueWithIgnore" },
imports: { $refs: "#/defs/valueWithIgnore" },
exports: { $refs: "#/defs/valueWithIgnore" },
functions: { $refs: "#/defs/valueWithIgnore" }
},
additionalProperties: false
}
]
}
]
},
create(context) {
const options = normalizeOptions(context.options[0]);
const sourceCode = context.getSourceCode();
const UNEXPECTED_MESSAGE = "Unexpected trailing comma.";
const MISSING_MESSAGE = "Missing trailing comma.";
/**
* Gets the last item of the given node.
* @param {ASTNode} node - The node to get.
* @returns {ASTNode|null} The last node or null.
*/
function getLastItem(node) {
switch (node.type) {
case "ObjectExpression":
case "ObjectPattern":
return lodash.last(node.properties);
case "ArrayExpression":
case "ArrayPattern":
return lodash.last(node.elements);
case "ImportDeclaration":
case "ExportNamedDeclaration":
return lodash.last(node.specifiers);
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
return lodash.last(node.params);
case "CallExpression":
case "NewExpression":
return lodash.last(node.arguments);
default:
return null;
}
}
/**
* Gets the trailing comma token of the given node.
* If the trailing comma does not exist, this returns the token which is
* the insertion point of the trailing comma token.
*
* @param {ASTNode} node - The node to get.
* @param {ASTNode} lastItem - The last item of the node.
* @returns {Token} The trailing comma token or the insertion point.
*/
function getTrailingToken(node, lastItem) {
switch (node.type) {
case "ObjectExpression":
case "ArrayExpression":
case "CallExpression":
case "NewExpression":
return sourceCode.getLastToken(node, 1);
default: {
const nextToken = sourceCode.getTokenAfter(lastItem);
if (astUtils.isCommaToken(nextToken)) {
return nextToken;
}
return sourceCode.getLastToken(lastItem);
}
}
}
/**
* Checks whether or not a given node is multiline.
* This rule handles a given node as multiline when the closing parenthesis
* and the last element are not on the same line.
*
* @param {ASTNode} node - A node to check.
* @returns {boolean} `true` if the node is multiline.
*/
function isMultiline(node) {
const lastItem = getLastItem(node);
if (!lastItem) {
return false;
}
const penultimateToken = getTrailingToken(node, lastItem);
const lastToken = sourceCode.getTokenAfter(penultimateToken);
return lastToken.loc.end.line !== penultimateToken.loc.end.line;
}
/**
* Reports a trailing comma if it exists.
*
* @param {ASTNode} node - A node to check. Its type is one of
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
* ImportDeclaration, and ExportNamedDeclaration.
* @returns {void}
*/
function forbidTrailingComma(node) {
const lastItem = getLastItem(node);
if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
return;
}
const trailingToken = getTrailingToken(node, lastItem);
if (astUtils.isCommaToken(trailingToken)) {
context.report({
node: lastItem,
loc: trailingToken.loc.start,
message: UNEXPECTED_MESSAGE,
fix(fixer) {
return fixer.remove(trailingToken);
}
});
}
}
/**
* Reports the last element of a given node if it does not have a trailing
* comma.
*
* If a given node is `ArrayPattern` which has `RestElement`, the trailing
* comma is disallowed, so report if it exists.
*
* @param {ASTNode} node - A node to check. Its type is one of
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
* ImportDeclaration, and ExportNamedDeclaration.
* @returns {void}
*/
function forceTrailingComma(node) {
const lastItem = getLastItem(node);
if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
return;
}
if (!isTrailingCommaAllowed(lastItem)) {
forbidTrailingComma(node);
return;
}
const trailingToken = getTrailingToken(node, lastItem);
if (trailingToken.value !== ",") {
context.report({
node: lastItem,
loc: trailingToken.loc.end,
message: MISSING_MESSAGE,
fix(fixer) {
return fixer.insertTextAfter(trailingToken, ",");
}
});
}
}
/**
* If a given node is multiline, reports the last element of a given node
* when it does not have a trailing comma.
* Otherwise, reports a trailing comma if it exists.
*
* @param {ASTNode} node - A node to check. Its type is one of
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
* ImportDeclaration, and ExportNamedDeclaration.
* @returns {void}
*/
function forceTrailingCommaIfMultiline(node) {
if (isMultiline(node)) {
forceTrailingComma(node);
} else {
forbidTrailingComma(node);
}
}
/**
* Only if a given node is not multiline, reports the last element of a given node
* when it does not have a trailing comma.
* Otherwise, reports a trailing comma if it exists.
*
* @param {ASTNode} node - A node to check. Its type is one of
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
* ImportDeclaration, and ExportNamedDeclaration.
* @returns {void}
*/
function allowTrailingCommaIfMultiline(node) {
if (!isMultiline(node)) {
forbidTrailingComma(node);
}
}
const predicate = {
always: forceTrailingComma,
"always-multiline": forceTrailingCommaIfMultiline,
"only-multiline": allowTrailingCommaIfMultiline,
never: forbidTrailingComma,
ignore: lodash.noop
};
return {
ObjectExpression: predicate[options.objects],
ObjectPattern: predicate[options.objects],
ArrayExpression: predicate[options.arrays],
ArrayPattern: predicate[options.arrays],
ImportDeclaration: predicate[options.imports],
ExportNamedDeclaration: predicate[options.exports],
FunctionDeclaration: predicate[options.functions],
FunctionExpression: predicate[options.functions],
ArrowFunctionExpression: predicate[options.functions],
CallExpression: predicate[options.functions],
NewExpression: predicate[options.functions]
};
}
};

View File

@ -0,0 +1,183 @@
/**
* @fileoverview Comma spacing - validates spacing before and after comma
* @author Vignesh Anand aka vegetableman.
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent spacing before and after commas",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
type: "object",
properties: {
before: {
type: "boolean"
},
after: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
const sourceCode = context.getSourceCode();
const tokensAndComments = sourceCode.tokensAndComments;
const options = {
before: context.options[0] ? !!context.options[0].before : false,
after: context.options[0] ? !!context.options[0].after : true
};
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
// list of comma tokens to ignore for the check of leading whitespace
const commaTokensToIgnore = [];
/**
* Reports a spacing error with an appropriate message.
* @param {ASTNode} node The binary expression node to report.
* @param {string} dir Is the error "before" or "after" the comma?
* @param {ASTNode} otherNode The node at the left or right of `node`
* @returns {void}
* @private
*/
function report(node, dir, otherNode) {
context.report({
node,
fix(fixer) {
if (options[dir]) {
if (dir === "before") {
return fixer.insertTextBefore(node, " ");
}
return fixer.insertTextAfter(node, " ");
}
let start, end;
const newText = "";
if (dir === "before") {
start = otherNode.range[1];
end = node.range[0];
} else {
start = node.range[1];
end = otherNode.range[0];
}
return fixer.replaceTextRange([start, end], newText);
},
message: options[dir]
? "A space is required {{dir}} ','."
: "There should be no space {{dir}} ','.",
data: {
dir
}
});
}
/**
* Validates the spacing around a comma token.
* @param {Object} tokens - The tokens to be validated.
* @param {Token} tokens.comma The token representing the comma.
* @param {Token} [tokens.left] The last token before the comma.
* @param {Token} [tokens.right] The first token after the comma.
* @param {Token|ASTNode} reportItem The item to use when reporting an error.
* @returns {void}
* @private
*/
function validateCommaItemSpacing(tokens, reportItem) {
if (tokens.left && astUtils.isTokenOnSameLine(tokens.left, tokens.comma) &&
(options.before !== sourceCode.isSpaceBetweenTokens(tokens.left, tokens.comma))
) {
report(reportItem, "before", tokens.left);
}
if (tokens.right && !options.after && tokens.right.type === "Line") {
return;
}
if (tokens.right && astUtils.isTokenOnSameLine(tokens.comma, tokens.right) &&
(options.after !== sourceCode.isSpaceBetweenTokens(tokens.comma, tokens.right))
) {
report(reportItem, "after", tokens.right);
}
}
/**
* Adds null elements of the given ArrayExpression or ArrayPattern node to the ignore list.
* @param {ASTNode} node An ArrayExpression or ArrayPattern node.
* @returns {void}
*/
function addNullElementsToIgnoreList(node) {
let previousToken = sourceCode.getFirstToken(node);
node.elements.forEach(element => {
let token;
if (element === null) {
token = sourceCode.getTokenAfter(previousToken);
if (astUtils.isCommaToken(token)) {
commaTokensToIgnore.push(token);
}
} else {
token = sourceCode.getTokenAfter(element);
}
previousToken = token;
});
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
"Program:exit"() {
tokensAndComments.forEach((token, i) => {
if (!astUtils.isCommaToken(token)) {
return;
}
if (token && token.type === "JSXText") {
return;
}
const previousToken = tokensAndComments[i - 1];
const nextToken = tokensAndComments[i + 1];
validateCommaItemSpacing({
comma: token,
left: astUtils.isCommaToken(previousToken) || commaTokensToIgnore.indexOf(token) > -1 ? null : previousToken,
right: astUtils.isCommaToken(nextToken) ? null : nextToken
}, token);
});
},
ArrayExpression: addNullElementsToIgnoreList,
ArrayPattern: addNullElementsToIgnoreList
};
}
};

View File

@ -0,0 +1,297 @@
/**
* @fileoverview Comma style - enforces comma styles of two types: last and first
* @author Vignesh Anand aka vegetableman
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent comma style",
category: "Stylistic Issues",
recommended: false
},
fixable: "code",
schema: [
{
enum: ["first", "last"]
},
{
type: "object",
properties: {
exceptions: {
type: "object",
additionalProperties: {
type: "boolean"
}
}
},
additionalProperties: false
}
]
},
create(context) {
const style = context.options[0] || "last",
sourceCode = context.getSourceCode();
const exceptions = {
ArrayPattern: true,
ArrowFunctionExpression: true,
CallExpression: true,
FunctionDeclaration: true,
FunctionExpression: true,
ImportDeclaration: true,
ObjectPattern: true
};
if (context.options.length === 2 && context.options[1].hasOwnProperty("exceptions")) {
const keys = Object.keys(context.options[1].exceptions);
for (let i = 0; i < keys.length; i++) {
exceptions[keys[i]] = context.options[1].exceptions[keys[i]];
}
}
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Modified text based on the style
* @param {string} styleType Style type
* @param {string} text Source code text
* @returns {string} modified text
* @private
*/
function getReplacedText(styleType, text) {
switch (styleType) {
case "between":
return `,${text.replace("\n", "")}`;
case "first":
return `${text},`;
case "last":
return `,${text}`;
default:
return "";
}
}
/**
* Determines the fixer function for a given style.
* @param {string} styleType comma style
* @param {ASTNode} previousItemToken The token to check.
* @param {ASTNode} commaToken The token to check.
* @param {ASTNode} currentItemToken The token to check.
* @returns {Function} Fixer function
* @private
*/
function getFixerFunction(styleType, previousItemToken, commaToken, currentItemToken) {
const text =
sourceCode.text.slice(previousItemToken.range[1], commaToken.range[0]) +
sourceCode.text.slice(commaToken.range[1], currentItemToken.range[0]);
const range = [previousItemToken.range[1], currentItemToken.range[0]];
return function(fixer) {
return fixer.replaceTextRange(range, getReplacedText(styleType, text));
};
}
/**
* Validates the spacing around single items in lists.
* @param {Token} previousItemToken The last token from the previous item.
* @param {Token} commaToken The token representing the comma.
* @param {Token} currentItemToken The first token of the current item.
* @param {Token} reportItem The item to use when reporting an error.
* @returns {void}
* @private
*/
function validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem) {
// if single line
if (astUtils.isTokenOnSameLine(commaToken, currentItemToken) &&
astUtils.isTokenOnSameLine(previousItemToken, commaToken)) {
// do nothing.
} else if (!astUtils.isTokenOnSameLine(commaToken, currentItemToken) &&
!astUtils.isTokenOnSameLine(previousItemToken, commaToken)) {
// lone comma
context.report({
node: reportItem,
loc: {
line: commaToken.loc.end.line,
column: commaToken.loc.start.column
},
message: "Bad line breaking before and after ','.",
fix: getFixerFunction("between", previousItemToken, commaToken, currentItemToken)
});
} else if (style === "first" && !astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
context.report({
node: reportItem,
message: "',' should be placed first.",
fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken)
});
} else if (style === "last" && astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
context.report({
node: reportItem,
loc: {
line: commaToken.loc.end.line,
column: commaToken.loc.end.column
},
message: "',' should be placed last.",
fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken)
});
}
}
/**
* Checks the comma placement with regards to a declaration/property/element
* @param {ASTNode} node The binary expression node to check
* @param {string} property The property of the node containing child nodes.
* @private
* @returns {void}
*/
function validateComma(node, property) {
const items = node[property],
arrayLiteral = (node.type === "ArrayExpression" || node.type === "ArrayPattern");
if (items.length > 1 || arrayLiteral) {
// seed as opening [
let previousItemToken = sourceCode.getFirstToken(node);
items.forEach(item => {
const commaToken = item ? sourceCode.getTokenBefore(item) : previousItemToken,
currentItemToken = item ? sourceCode.getFirstToken(item) : sourceCode.getTokenAfter(commaToken),
reportItem = item || currentItemToken,
tokenBeforeComma = sourceCode.getTokenBefore(commaToken);
// Check if previous token is wrapped in parentheses
if (tokenBeforeComma && astUtils.isClosingParenToken(tokenBeforeComma)) {
previousItemToken = tokenBeforeComma;
}
/*
* This works by comparing three token locations:
* - previousItemToken is the last token of the previous item
* - commaToken is the location of the comma before the current item
* - currentItemToken is the first token of the current item
*
* These values get switched around if item is undefined.
* previousItemToken will refer to the last token not belonging
* to the current item, which could be a comma or an opening
* square bracket. currentItemToken could be a comma.
*
* All comparisons are done based on these tokens directly, so
* they are always valid regardless of an undefined item.
*/
if (astUtils.isCommaToken(commaToken)) {
validateCommaItemSpacing(previousItemToken, commaToken,
currentItemToken, reportItem);
}
if (item) {
const tokenAfterItem = sourceCode.getTokenAfter(item, astUtils.isNotClosingParenToken);
previousItemToken = tokenAfterItem ? sourceCode.getTokenBefore(tokenAfterItem) : sourceCode.ast.tokens[sourceCode.ast.tokens.length - 1];
}
});
/*
* Special case for array literals that have empty last items, such
* as [ 1, 2, ]. These arrays only have two items show up in the
* AST, so we need to look at the token to verify that there's no
* dangling comma.
*/
if (arrayLiteral) {
const lastToken = sourceCode.getLastToken(node),
nextToLastToken = sourceCode.getTokenBefore(lastToken);
if (astUtils.isCommaToken(nextToLastToken)) {
validateCommaItemSpacing(
sourceCode.getTokenBefore(nextToLastToken),
nextToLastToken,
lastToken,
lastToken
);
}
}
}
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
const nodes = {};
if (!exceptions.VariableDeclaration) {
nodes.VariableDeclaration = function(node) {
validateComma(node, "declarations");
};
}
if (!exceptions.ObjectExpression) {
nodes.ObjectExpression = function(node) {
validateComma(node, "properties");
};
}
if (!exceptions.ObjectPattern) {
nodes.ObjectPattern = function(node) {
validateComma(node, "properties");
};
}
if (!exceptions.ArrayExpression) {
nodes.ArrayExpression = function(node) {
validateComma(node, "elements");
};
}
if (!exceptions.ArrayPattern) {
nodes.ArrayPattern = function(node) {
validateComma(node, "elements");
};
}
if (!exceptions.FunctionDeclaration) {
nodes.FunctionDeclaration = function(node) {
validateComma(node, "params");
};
}
if (!exceptions.FunctionExpression) {
nodes.FunctionExpression = function(node) {
validateComma(node, "params");
};
}
if (!exceptions.ArrowFunctionExpression) {
nodes.ArrowFunctionExpression = function(node) {
validateComma(node, "params");
};
}
if (!exceptions.CallExpression) {
nodes.CallExpression = function(node) {
validateComma(node, "arguments");
};
}
if (!exceptions.ImportDeclaration) {
nodes.ImportDeclaration = function(node) {
validateComma(node, "specifiers");
};
}
return nodes;
}
};

View File

@ -0,0 +1,168 @@
/**
* @fileoverview Counts the cyclomatic complexity of each function of the script. See http://en.wikipedia.org/wiki/Cyclomatic_complexity.
* Counts the number of if, conditional, for, whilte, try, switch/case,
* @author Patrick Brosset
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce a maximum cyclomatic complexity allowed in a program",
category: "Best Practices",
recommended: false
},
schema: [
{
oneOf: [
{
type: "integer",
minimum: 0
},
{
type: "object",
properties: {
maximum: {
type: "integer",
minimum: 0
},
max: {
type: "integer",
minimum: 0
}
},
additionalProperties: false
}
]
}
]
},
create(context) {
const option = context.options[0];
let THRESHOLD = 20;
if (typeof option === "object" && option.hasOwnProperty("maximum") && typeof option.maximum === "number") {
THRESHOLD = option.maximum;
}
if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") {
THRESHOLD = option.max;
}
if (typeof option === "number") {
THRESHOLD = option;
}
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
// Using a stack to store complexity (handling nested functions)
const fns = [];
/**
* When parsing a new function, store it in our function stack
* @returns {void}
* @private
*/
function startFunction() {
fns.push(1);
}
/**
* Evaluate the node at the end of function
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function endFunction(node) {
const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(node));
const complexity = fns.pop();
if (complexity > THRESHOLD) {
context.report({
node,
message: "{{name}} has a complexity of {{complexity}}.",
data: { name, complexity }
});
}
}
/**
* Increase the complexity of the function in context
* @returns {void}
* @private
*/
function increaseComplexity() {
if (fns.length) {
fns[fns.length - 1]++;
}
}
/**
* Increase the switch complexity in context
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function increaseSwitchComplexity(node) {
// Avoiding `default`
if (node.test) {
increaseComplexity(node);
}
}
/**
* Increase the logical path complexity in context
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function increaseLogicalComplexity(node) {
// Avoiding &&
if (node.operator === "||") {
increaseComplexity(node);
}
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
FunctionDeclaration: startFunction,
FunctionExpression: startFunction,
ArrowFunctionExpression: startFunction,
"FunctionDeclaration:exit": endFunction,
"FunctionExpression:exit": endFunction,
"ArrowFunctionExpression:exit": endFunction,
CatchClause: increaseComplexity,
ConditionalExpression: increaseComplexity,
LogicalExpression: increaseLogicalComplexity,
ForStatement: increaseComplexity,
ForInStatement: increaseComplexity,
ForOfStatement: increaseComplexity,
IfStatement: increaseComplexity,
SwitchCase: increaseSwitchComplexity,
WhileStatement: increaseComplexity,
DoWhileStatement: increaseComplexity
};
}
};

View File

@ -0,0 +1,176 @@
/**
* @fileoverview Disallows or enforces spaces inside computed properties.
* @author Jamund Ferguson
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent spacing inside computed property brackets",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
enum: ["always", "never"]
}
]
},
create(context) {
const sourceCode = context.getSourceCode();
const propertyNameMustBeSpaced = context.options[0] === "always"; // default is "never"
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Reports that there shouldn't be a space after the first token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @param {Token} tokenAfter - The token after `token`.
* @returns {void}
*/
function reportNoBeginningSpace(node, token, tokenAfter) {
context.report({
node,
loc: token.loc.start,
message: "There should be no space after '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.removeRange([token.range[1], tokenAfter.range[0]]);
}
});
}
/**
* Reports that there shouldn't be a space before the last token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @param {Token} tokenBefore - The token before `token`.
* @returns {void}
*/
function reportNoEndingSpace(node, token, tokenBefore) {
context.report({
node,
loc: token.loc.start,
message: "There should be no space before '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.removeRange([tokenBefore.range[1], token.range[0]]);
}
});
}
/**
* Reports that there should be a space after the first token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @returns {void}
*/
function reportRequiredBeginningSpace(node, token) {
context.report({
node,
loc: token.loc.start,
message: "A space is required after '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.insertTextAfter(token, " ");
}
});
}
/**
* Reports that there should be a space before the last token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @returns {void}
*/
function reportRequiredEndingSpace(node, token) {
context.report({
node,
loc: token.loc.start,
message: "A space is required before '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.insertTextBefore(token, " ");
}
});
}
/**
* Returns a function that checks the spacing of a node on the property name
* that was passed in.
* @param {string} propertyName The property on the node to check for spacing
* @returns {Function} A function that will check spacing on a node
*/
function checkSpacing(propertyName) {
return function(node) {
if (!node.computed) {
return;
}
const property = node[propertyName];
const before = sourceCode.getTokenBefore(property),
first = sourceCode.getFirstToken(property),
last = sourceCode.getLastToken(property),
after = sourceCode.getTokenAfter(property);
if (astUtils.isTokenOnSameLine(before, first)) {
if (propertyNameMustBeSpaced) {
if (!sourceCode.isSpaceBetweenTokens(before, first) && astUtils.isTokenOnSameLine(before, first)) {
reportRequiredBeginningSpace(node, before);
}
} else {
if (sourceCode.isSpaceBetweenTokens(before, first)) {
reportNoBeginningSpace(node, before, first);
}
}
}
if (astUtils.isTokenOnSameLine(last, after)) {
if (propertyNameMustBeSpaced) {
if (!sourceCode.isSpaceBetweenTokens(last, after) && astUtils.isTokenOnSameLine(last, after)) {
reportRequiredEndingSpace(node, after);
}
} else {
if (sourceCode.isSpaceBetweenTokens(last, after)) {
reportNoEndingSpace(node, after, last);
}
}
}
};
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
Property: checkSpacing("key"),
MemberExpression: checkSpacing("property")
};
}
};

View File

@ -0,0 +1,188 @@
/**
* @fileoverview Rule to flag consistent return values
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether or not a given node is an `Identifier` node which was named a given name.
* @param {ASTNode} node - A node to check.
* @param {string} name - An expected name of the node.
* @returns {boolean} `true` if the node is an `Identifier` node which was named as expected.
*/
function isIdentifier(node, name) {
return node.type === "Identifier" && node.name === name;
}
/**
* Checks whether or not a given code path segment is unreachable.
* @param {CodePathSegment} segment - A CodePathSegment to check.
* @returns {boolean} `true` if the segment is unreachable.
*/
function isUnreachable(segment) {
return !segment.reachable;
}
/**
* Checks whether a given node is a `constructor` method in an ES6 class
* @param {ASTNode} node A node to check
* @returns {boolean} `true` if the node is a `constructor` method
*/
function isClassConstructor(node) {
return node.type === "FunctionExpression" &&
node.parent &&
node.parent.type === "MethodDefinition" &&
node.parent.kind === "constructor";
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require `return` statements to either always or never specify values",
category: "Best Practices",
recommended: false
},
schema: [{
type: "object",
properties: {
treatUndefinedAsUnspecified: {
type: "boolean"
}
},
additionalProperties: false
}]
},
create(context) {
const options = context.options[0] || {};
const treatUndefinedAsUnspecified = options.treatUndefinedAsUnspecified === true;
let funcInfo = null;
/**
* Checks whether of not the implicit returning is consistent if the last
* code path segment is reachable.
*
* @param {ASTNode} node - A program/function node to check.
* @returns {void}
*/
function checkLastSegment(node) {
let loc, name;
/*
* Skip if it expected no return value or unreachable.
* When unreachable, all paths are returned or thrown.
*/
if (!funcInfo.hasReturnValue ||
funcInfo.codePath.currentSegments.every(isUnreachable) ||
astUtils.isES5Constructor(node) ||
isClassConstructor(node)
) {
return;
}
// Adjust a location and a message.
if (node.type === "Program") {
// The head of program.
loc = { line: 1, column: 0 };
name = "program";
} else if (node.type === "ArrowFunctionExpression") {
// `=>` token
loc = context.getSourceCode().getTokenBefore(node.body, astUtils.isArrowToken).loc.start;
} else if (
node.parent.type === "MethodDefinition" ||
(node.parent.type === "Property" && node.parent.method)
) {
// Method name.
loc = node.parent.key.loc.start;
} else {
// Function name or `function` keyword.
loc = (node.id || node).loc.start;
}
if (!name) {
name = astUtils.getFunctionNameWithKind(node);
}
// Reports.
context.report({
node,
loc,
message: "Expected to return a value at the end of {{name}}.",
data: { name }
});
}
return {
// Initializes/Disposes state of each code path.
onCodePathStart(codePath, node) {
funcInfo = {
upper: funcInfo,
codePath,
hasReturn: false,
hasReturnValue: false,
message: "",
node
};
},
onCodePathEnd() {
funcInfo = funcInfo.upper;
},
// Reports a given return statement if it's inconsistent.
ReturnStatement(node) {
const argument = node.argument;
let hasReturnValue = Boolean(argument);
if (treatUndefinedAsUnspecified && hasReturnValue) {
hasReturnValue = !isIdentifier(argument, "undefined") && argument.operator !== "void";
}
if (!funcInfo.hasReturn) {
funcInfo.hasReturn = true;
funcInfo.hasReturnValue = hasReturnValue;
funcInfo.message = "{{name}} expected {{which}} return value.";
funcInfo.data = {
name: funcInfo.node.type === "Program"
? "Program"
: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node)),
which: hasReturnValue ? "a" : "no"
};
} else if (funcInfo.hasReturnValue !== hasReturnValue) {
context.report({
node,
message: funcInfo.message,
data: funcInfo.data
});
}
},
// Reports a given program/function if the implicit returning is not consistent.
"Program:exit": checkLastSegment,
"FunctionDeclaration:exit": checkLastSegment,
"FunctionExpression:exit": checkLastSegment,
"ArrowFunctionExpression:exit": checkLastSegment
};
}
};

View File

@ -0,0 +1,141 @@
/**
* @fileoverview Rule to enforce consistent naming of "this" context variables
* @author Raphael Pigulla
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent naming when capturing the current execution context",
category: "Stylistic Issues",
recommended: false
},
schema: {
type: "array",
items: {
type: "string",
minLength: 1
},
uniqueItems: true
}
},
create(context) {
let aliases = [];
if (context.options.length === 0) {
aliases.push("that");
} else {
aliases = context.options;
}
/**
* Reports that a variable declarator or assignment expression is assigning
* a non-'this' value to the specified alias.
* @param {ASTNode} node - The assigning node.
* @param {string} alias - the name of the alias that was incorrectly used.
* @returns {void}
*/
function reportBadAssignment(node, alias) {
context.report({ node, message: "Designated alias '{{alias}}' is not assigned to 'this'.", data: { alias } });
}
/**
* Checks that an assignment to an identifier only assigns 'this' to the
* appropriate alias, and the alias is only assigned to 'this'.
* @param {ASTNode} node - The assigning node.
* @param {Identifier} name - The name of the variable assigned to.
* @param {Expression} value - The value of the assignment.
* @returns {void}
*/
function checkAssignment(node, name, value) {
const isThis = value.type === "ThisExpression";
if (aliases.indexOf(name) !== -1) {
if (!isThis || node.operator && node.operator !== "=") {
reportBadAssignment(node, name);
}
} else if (isThis) {
context.report({ node, message: "Unexpected alias '{{name}}' for 'this'.", data: { name } });
}
}
/**
* Ensures that a variable declaration of the alias in a program or function
* is assigned to the correct value.
* @param {string} alias alias the check the assignment of.
* @param {Object} scope scope of the current code we are checking.
* @private
* @returns {void}
*/
function checkWasAssigned(alias, scope) {
const variable = scope.set.get(alias);
if (!variable) {
return;
}
if (variable.defs.some(def => def.node.type === "VariableDeclarator" &&
def.node.init !== null)) {
return;
}
// The alias has been declared and not assigned: check it was
// assigned later in the same scope.
if (!variable.references.some(reference => {
const write = reference.writeExpr;
return (
reference.from === scope &&
write && write.type === "ThisExpression" &&
write.parent.operator === "="
);
})) {
variable.defs.map(def => def.node).forEach(node => {
reportBadAssignment(node, alias);
});
}
}
/**
* Check each alias to ensure that is was assinged to the correct value.
* @returns {void}
*/
function ensureWasAssigned() {
const scope = context.getScope();
aliases.forEach(alias => {
checkWasAssigned(alias, scope);
});
}
return {
"Program:exit": ensureWasAssigned,
"FunctionExpression:exit": ensureWasAssigned,
"FunctionDeclaration:exit": ensureWasAssigned,
VariableDeclarator(node) {
const id = node.id;
const isDestructuring =
id.type === "ArrayPattern" || id.type === "ObjectPattern";
if (node.init !== null && !isDestructuring) {
checkAssignment(node, id.name, node.init);
}
},
AssignmentExpression(node) {
if (node.left.type === "Identifier") {
checkAssignment(node, node.left.name, node.right);
}
}
};
}
};

View File

@ -0,0 +1,385 @@
/**
* @fileoverview A rule to verify `super()` callings in constructor.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether a given code path segment is reachable or not.
*
* @param {CodePathSegment} segment - A code path segment to check.
* @returns {boolean} `true` if the segment is reachable.
*/
function isReachable(segment) {
return segment.reachable;
}
/**
* Checks whether or not a given node is a constructor.
* @param {ASTNode} node - A node to check. This node type is one of
* `Program`, `FunctionDeclaration`, `FunctionExpression`, and
* `ArrowFunctionExpression`.
* @returns {boolean} `true` if the node is a constructor.
*/
function isConstructorFunction(node) {
return (
node.type === "FunctionExpression" &&
node.parent.type === "MethodDefinition" &&
node.parent.kind === "constructor"
);
}
/**
* Checks whether a given node can be a constructor or not.
*
* @param {ASTNode} node - A node to check.
* @returns {boolean} `true` if the node can be a constructor.
*/
function isPossibleConstructor(node) {
if (!node) {
return false;
}
switch (node.type) {
case "ClassExpression":
case "FunctionExpression":
case "ThisExpression":
case "MemberExpression":
case "CallExpression":
case "NewExpression":
case "YieldExpression":
case "TaggedTemplateExpression":
case "MetaProperty":
return true;
case "Identifier":
return node.name !== "undefined";
case "AssignmentExpression":
return isPossibleConstructor(node.right);
case "LogicalExpression":
return (
isPossibleConstructor(node.left) ||
isPossibleConstructor(node.right)
);
case "ConditionalExpression":
return (
isPossibleConstructor(node.alternate) ||
isPossibleConstructor(node.consequent)
);
case "SequenceExpression": {
const lastExpression = node.expressions[node.expressions.length - 1];
return isPossibleConstructor(lastExpression);
}
default:
return false;
}
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require `super()` calls in constructors",
category: "ECMAScript 6",
recommended: true
},
schema: []
},
create(context) {
/*
* {{hasExtends: boolean, scope: Scope, codePath: CodePath}[]}
* Information for each constructor.
* - upper: Information of the upper constructor.
* - hasExtends: A flag which shows whether own class has a valid `extends`
* part.
* - scope: The scope of own class.
* - codePath: The code path object of the constructor.
*/
let funcInfo = null;
/*
* {Map<string, {calledInSomePaths: boolean, calledInEveryPaths: boolean}>}
* Information for each code path segment.
* - calledInSomePaths: A flag of be called `super()` in some code paths.
* - calledInEveryPaths: A flag of be called `super()` in all code paths.
* - validNodes:
*/
let segInfoMap = Object.create(null);
/**
* Gets the flag which shows `super()` is called in some paths.
* @param {CodePathSegment} segment - A code path segment to get.
* @returns {boolean} The flag which shows `super()` is called in some paths
*/
function isCalledInSomePath(segment) {
return segment.reachable && segInfoMap[segment.id].calledInSomePaths;
}
/**
* Gets the flag which shows `super()` is called in all paths.
* @param {CodePathSegment} segment - A code path segment to get.
* @returns {boolean} The flag which shows `super()` is called in all paths.
*/
function isCalledInEveryPath(segment) {
/*
* If specific segment is the looped segment of the current segment,
* skip the segment.
* If not skipped, this never becomes true after a loop.
*/
if (segment.nextSegments.length === 1 &&
segment.nextSegments[0].isLoopedPrevSegment(segment)
) {
return true;
}
return segment.reachable && segInfoMap[segment.id].calledInEveryPaths;
}
return {
/**
* Stacks a constructor information.
* @param {CodePath} codePath - A code path which was started.
* @param {ASTNode} node - The current node.
* @returns {void}
*/
onCodePathStart(codePath, node) {
if (isConstructorFunction(node)) {
// Class > ClassBody > MethodDefinition > FunctionExpression
const classNode = node.parent.parent.parent;
const superClass = classNode.superClass;
funcInfo = {
upper: funcInfo,
isConstructor: true,
hasExtends: Boolean(superClass),
superIsConstructor: isPossibleConstructor(superClass),
codePath
};
} else {
funcInfo = {
upper: funcInfo,
isConstructor: false,
hasExtends: false,
superIsConstructor: false,
codePath
};
}
},
/**
* Pops a constructor information.
* And reports if `super()` lacked.
* @param {CodePath} codePath - A code path which was ended.
* @param {ASTNode} node - The current node.
* @returns {void}
*/
onCodePathEnd(codePath, node) {
const hasExtends = funcInfo.hasExtends;
// Pop.
funcInfo = funcInfo.upper;
if (!hasExtends) {
return;
}
// Reports if `super()` lacked.
const segments = codePath.returnedSegments;
const calledInEveryPaths = segments.every(isCalledInEveryPath);
const calledInSomePaths = segments.some(isCalledInSomePath);
if (!calledInEveryPaths) {
context.report({
message: calledInSomePaths
? "Lacked a call of 'super()' in some code paths."
: "Expected to call 'super()'.",
node: node.parent
});
}
},
/**
* Initialize information of a given code path segment.
* @param {CodePathSegment} segment - A code path segment to initialize.
* @returns {void}
*/
onCodePathSegmentStart(segment) {
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
return;
}
// Initialize info.
const info = segInfoMap[segment.id] = {
calledInSomePaths: false,
calledInEveryPaths: false,
validNodes: []
};
// When there are previous segments, aggregates these.
const prevSegments = segment.prevSegments;
if (prevSegments.length > 0) {
info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
}
},
/**
* Update information of the code path segment when a code path was
* looped.
* @param {CodePathSegment} fromSegment - The code path segment of the
* end of a loop.
* @param {CodePathSegment} toSegment - A code path segment of the head
* of a loop.
* @returns {void}
*/
onCodePathSegmentLoop(fromSegment, toSegment) {
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
return;
}
// Update information inside of the loop.
const isRealLoop = toSegment.prevSegments.length >= 2;
funcInfo.codePath.traverseSegments(
{ first: toSegment, last: fromSegment },
segment => {
const info = segInfoMap[segment.id];
const prevSegments = segment.prevSegments;
// Updates flags.
info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
// If flags become true anew, reports the valid nodes.
if (info.calledInSomePaths || isRealLoop) {
const nodes = info.validNodes;
info.validNodes = [];
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i];
context.report({
message: "Unexpected duplicate 'super()'.",
node
});
}
}
}
);
},
/**
* Checks for a call of `super()`.
* @param {ASTNode} node - A CallExpression node to check.
* @returns {void}
*/
"CallExpression:exit"(node) {
if (!(funcInfo && funcInfo.isConstructor)) {
return;
}
// Skips except `super()`.
if (node.callee.type !== "Super") {
return;
}
// Reports if needed.
if (funcInfo.hasExtends) {
const segments = funcInfo.codePath.currentSegments;
let duplicate = false;
let info = null;
for (let i = 0; i < segments.length; ++i) {
const segment = segments[i];
if (segment.reachable) {
info = segInfoMap[segment.id];
duplicate = duplicate || info.calledInSomePaths;
info.calledInSomePaths = info.calledInEveryPaths = true;
}
}
if (info) {
if (duplicate) {
context.report({
message: "Unexpected duplicate 'super()'.",
node
});
} else if (!funcInfo.superIsConstructor) {
context.report({
message: "Unexpected 'super()' because 'super' is not a constructor.",
node
});
} else {
info.validNodes.push(node);
}
}
} else if (funcInfo.codePath.currentSegments.some(isReachable)) {
context.report({
message: "Unexpected 'super()'.",
node
});
}
},
/**
* Set the mark to the returned path as `super()` was called.
* @param {ASTNode} node - A ReturnStatement node to check.
* @returns {void}
*/
ReturnStatement(node) {
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
return;
}
// Skips if no argument.
if (!node.argument) {
return;
}
// Returning argument is a substitute of 'super()'.
const segments = funcInfo.codePath.currentSegments;
for (let i = 0; i < segments.length; ++i) {
const segment = segments[i];
if (segment.reachable) {
const info = segInfoMap[segment.id];
info.calledInSomePaths = info.calledInEveryPaths = true;
}
}
},
/**
* Resets state.
* @returns {void}
*/
"Program:exit"() {
segInfoMap = Object.create(null);
}
};
}
};

View File

@ -0,0 +1,396 @@
/**
* @fileoverview Rule to flag statements without curly braces
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
const esUtils = require("esutils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent brace style for all control statements",
category: "Best Practices",
recommended: false
},
schema: {
anyOf: [
{
type: "array",
items: [
{
enum: ["all"]
}
],
minItems: 0,
maxItems: 1
},
{
type: "array",
items: [
{
enum: ["multi", "multi-line", "multi-or-nest"]
},
{
enum: ["consistent"]
}
],
minItems: 0,
maxItems: 2
}
]
},
fixable: "code"
},
create(context) {
const multiOnly = (context.options[0] === "multi");
const multiLine = (context.options[0] === "multi-line");
const multiOrNest = (context.options[0] === "multi-or-nest");
const consistent = (context.options[1] === "consistent");
const sourceCode = context.getSourceCode();
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Determines if a given node is a one-liner that's on the same line as it's preceding code.
* @param {ASTNode} node The node to check.
* @returns {boolean} True if the node is a one-liner that's on the same line as it's preceding code.
* @private
*/
function isCollapsedOneLiner(node) {
const before = sourceCode.getTokenBefore(node);
const last = sourceCode.getLastToken(node);
const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last;
return before.loc.start.line === lastExcludingSemicolon.loc.end.line;
}
/**
* Determines if a given node is a one-liner.
* @param {ASTNode} node The node to check.
* @returns {boolean} True if the node is a one-liner.
* @private
*/
function isOneLiner(node) {
const first = sourceCode.getFirstToken(node),
last = sourceCode.getLastToken(node);
return first.loc.start.line === last.loc.end.line;
}
/**
* Checks if the given token is an `else` token or not.
*
* @param {Token} token - The token to check.
* @returns {boolean} `true` if the token is an `else` token.
*/
function isElseKeywordToken(token) {
return token.value === "else" && token.type === "Keyword";
}
/**
* Gets the `else` keyword token of a given `IfStatement` node.
* @param {ASTNode} node - A `IfStatement` node to get.
* @returns {Token} The `else` keyword token.
*/
function getElseKeyword(node) {
return node.alternate && sourceCode.getFirstTokenBetween(node.consequent, node.alternate, isElseKeywordToken);
}
/**
* Checks a given IfStatement node requires braces of the consequent chunk.
* This returns `true` when below:
*
* 1. The given node has the `alternate` node.
* 2. There is a `IfStatement` which doesn't have `alternate` node in the
* trailing statement chain of the `consequent` node.
*
* @param {ASTNode} node - A IfStatement node to check.
* @returns {boolean} `true` if the node requires braces of the consequent chunk.
*/
function requiresBraceOfConsequent(node) {
if (node.alternate && node.consequent.type === "BlockStatement") {
if (node.consequent.body.length >= 2) {
return true;
}
node = node.consequent.body[0];
while (node) {
if (node.type === "IfStatement" && !node.alternate) {
return true;
}
node = astUtils.getTrailingStatement(node);
}
}
return false;
}
/**
* Reports "Expected { after ..." error
* @param {ASTNode} node The node to report.
* @param {ASTNode} bodyNode The body node that is incorrectly missing curly brackets
* @param {string} name The name to report.
* @param {string} suffix Additional string to add to the end of a report.
* @returns {void}
* @private
*/
function reportExpectedBraceError(node, bodyNode, name, suffix) {
context.report({
node,
loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
message: "Expected { after '{{name}}'{{suffix}}.",
data: {
name,
suffix: (suffix ? ` ${suffix}` : "")
},
fix: fixer => fixer.replaceText(bodyNode, `{${sourceCode.getText(bodyNode)}}`)
});
}
/**
* Determines if a semicolon needs to be inserted after removing a set of curly brackets, in order to avoid a SyntaxError.
* @param {Token} closingBracket The } token
* @returns {boolean} `true` if a semicolon needs to be inserted after the last statement in the block.
*/
function needsSemicolon(closingBracket) {
const tokenBefore = sourceCode.getTokenBefore(closingBracket);
const tokenAfter = sourceCode.getTokenAfter(closingBracket);
const lastBlockNode = sourceCode.getNodeByRangeIndex(tokenBefore.range[0]);
if (astUtils.isSemicolonToken(tokenBefore)) {
// If the last statement already has a semicolon, don't add another one.
return false;
}
if (!tokenAfter) {
// If there are no statements after this block, there is no need to add a semicolon.
return false;
}
if (lastBlockNode.type === "BlockStatement" && lastBlockNode.parent.type !== "FunctionExpression" && lastBlockNode.parent.type !== "ArrowFunctionExpression") {
// If the last node surrounded by curly brackets is a BlockStatement (other than a FunctionExpression or an ArrowFunctionExpression),
// don't insert a semicolon. Otherwise, the semicolon would be parsed as a separate statement, which would cause
// a SyntaxError if it was followed by `else`.
return false;
}
if (tokenBefore.loc.end.line === tokenAfter.loc.start.line) {
// If the next token is on the same line, insert a semicolon.
return true;
}
if (/^[([/`+-]/.test(tokenAfter.value)) {
// If the next token starts with a character that would disrupt ASI, insert a semicolon.
return true;
}
if (tokenBefore.type === "Punctuator" && (tokenBefore.value === "++" || tokenBefore.value === "--")) {
// If the last token is ++ or --, insert a semicolon to avoid disrupting ASI.
return true;
}
// Otherwise, do not insert a semicolon.
return false;
}
/**
* Reports "Unnecessary { after ..." error
* @param {ASTNode} node The node to report.
* @param {ASTNode} bodyNode The block statement that is incorrectly surrounded by parens
* @param {string} name The name to report.
* @param {string} suffix Additional string to add to the end of a report.
* @returns {void}
* @private
*/
function reportUnnecessaryBraceError(node, bodyNode, name, suffix) {
context.report({
node,
loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
message: "Unnecessary { after '{{name}}'{{suffix}}.",
data: {
name,
suffix: (suffix ? ` ${suffix}` : "")
},
fix(fixer) {
// `do while` expressions sometimes need a space to be inserted after `do`.
// e.g. `do{foo()} while (bar)` should be corrected to `do foo() while (bar)`
const needsPrecedingSpace = node.type === "DoWhileStatement" &&
sourceCode.getTokenBefore(bodyNode).end === bodyNode.start &&
esUtils.code.isIdentifierPartES6(sourceCode.getText(bodyNode).charCodeAt(1));
const openingBracket = sourceCode.getFirstToken(bodyNode);
const closingBracket = sourceCode.getLastToken(bodyNode);
const lastTokenInBlock = sourceCode.getTokenBefore(closingBracket);
if (needsSemicolon(closingBracket)) {
/*
* If removing braces would cause a SyntaxError due to multiple statements on the same line (or
* change the semantics of the code due to ASI), don't perform a fix.
*/
return null;
}
const resultingBodyText = sourceCode.getText().slice(openingBracket.range[1], lastTokenInBlock.range[0]) +
sourceCode.getText(lastTokenInBlock) +
sourceCode.getText().slice(lastTokenInBlock.range[1], closingBracket.range[0]);
return fixer.replaceText(bodyNode, (needsPrecedingSpace ? " " : "") + resultingBodyText);
}
});
}
/**
* Prepares to check the body of a node to see if it's a block statement.
* @param {ASTNode} node The node to report if there's a problem.
* @param {ASTNode} body The body node to check for blocks.
* @param {string} name The name to report if there's a problem.
* @param {string} suffix Additional string to add to the end of a report.
* @returns {Object} a prepared check object, with "actual", "expected", "check" properties.
* "actual" will be `true` or `false` whether the body is already a block statement.
* "expected" will be `true` or `false` if the body should be a block statement or not, or
* `null` if it doesn't matter, depending on the rule options. It can be modified to change
* the final behavior of "check".
* "check" will be a function reporting appropriate problems depending on the other
* properties.
*/
function prepareCheck(node, body, name, suffix) {
const hasBlock = (body.type === "BlockStatement");
let expected = null;
if (node.type === "IfStatement" && node.consequent === body && requiresBraceOfConsequent(node)) {
expected = true;
} else if (multiOnly) {
if (hasBlock && body.body.length === 1) {
expected = false;
}
} else if (multiLine) {
if (!isCollapsedOneLiner(body)) {
expected = true;
}
} else if (multiOrNest) {
if (hasBlock && body.body.length === 1 && isOneLiner(body.body[0])) {
const leadingComments = sourceCode.getComments(body.body[0]).leading;
expected = leadingComments.length > 0;
} else if (!isOneLiner(body)) {
expected = true;
}
} else {
expected = true;
}
return {
actual: hasBlock,
expected,
check() {
if (this.expected !== null && this.expected !== this.actual) {
if (this.expected) {
reportExpectedBraceError(node, body, name, suffix);
} else {
reportUnnecessaryBraceError(node, body, name, suffix);
}
}
}
};
}
/**
* Prepares to check the bodies of a "if", "else if" and "else" chain.
* @param {ASTNode} node The first IfStatement node of the chain.
* @returns {Object[]} prepared checks for each body of the chain. See `prepareCheck` for more
* information.
*/
function prepareIfChecks(node) {
const preparedChecks = [];
do {
preparedChecks.push(prepareCheck(node, node.consequent, "if", "condition"));
if (node.alternate && node.alternate.type !== "IfStatement") {
preparedChecks.push(prepareCheck(node, node.alternate, "else"));
break;
}
node = node.alternate;
} while (node);
if (consistent) {
/*
* If any node should have or already have braces, make sure they
* all have braces.
* If all nodes shouldn't have braces, make sure they don't.
*/
const expected = preparedChecks.some(preparedCheck => {
if (preparedCheck.expected !== null) {
return preparedCheck.expected;
}
return preparedCheck.actual;
});
preparedChecks.forEach(preparedCheck => {
preparedCheck.expected = expected;
});
}
return preparedChecks;
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
IfStatement(node) {
if (node.parent.type !== "IfStatement") {
prepareIfChecks(node).forEach(preparedCheck => {
preparedCheck.check();
});
}
},
WhileStatement(node) {
prepareCheck(node, node.body, "while", "condition").check();
},
DoWhileStatement(node) {
prepareCheck(node, node.body, "do").check();
},
ForStatement(node) {
prepareCheck(node, node.body, "for", "condition").check();
},
ForInStatement(node) {
prepareCheck(node, node.body, "for-in").check();
},
ForOfStatement(node) {
prepareCheck(node, node.body, "for-of").check();
}
};
}
};

View File

@ -0,0 +1,90 @@
/**
* @fileoverview require default case in switch statements
* @author Aliaksei Shytkin
*/
"use strict";
const DEFAULT_COMMENT_PATTERN = /^no default$/i;
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require `default` cases in `switch` statements",
category: "Best Practices",
recommended: false
},
schema: [{
type: "object",
properties: {
commentPattern: {
type: "string"
}
},
additionalProperties: false
}]
},
create(context) {
const options = context.options[0] || {};
const commentPattern = options.commentPattern
? new RegExp(options.commentPattern)
: DEFAULT_COMMENT_PATTERN;
const sourceCode = context.getSourceCode();
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Shortcut to get last element of array
* @param {*[]} collection Array
* @returns {*} Last element
*/
function last(collection) {
return collection[collection.length - 1];
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
SwitchStatement(node) {
if (!node.cases.length) {
/*
* skip check of empty switch because there is no easy way
* to extract comments inside it now
*/
return;
}
const hasDefault = node.cases.some(v => v.test === null);
if (!hasDefault) {
let comment;
const lastCase = last(node.cases);
const comments = sourceCode.getComments(lastCase).trailing;
if (comments.length) {
comment = last(comments);
}
if (!comment || !commentPattern.test(comment.value.trim())) {
context.report({ node, message: "Expected a default case." });
}
}
}
};
}
};

View File

@ -0,0 +1,88 @@
/**
* @fileoverview Validates newlines before and after dots
* @author Greg Cochard
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent newlines before and after dots",
category: "Best Practices",
recommended: false
},
schema: [
{
enum: ["object", "property"]
}
],
fixable: "code"
},
create(context) {
const config = context.options[0];
// default to onObject if no preference is passed
const onObject = config === "object" || !config;
const sourceCode = context.getSourceCode();
/**
* Reports if the dot between object and property is on the correct loccation.
* @param {ASTNode} obj The object owning the property.
* @param {ASTNode} prop The property of the object.
* @param {ASTNode} node The corresponding node of the token.
* @returns {void}
*/
function checkDotLocation(obj, prop, node) {
const dot = sourceCode.getTokenBefore(prop);
const textBeforeDot = sourceCode.getText().slice(obj.range[1], dot.range[0]);
const textAfterDot = sourceCode.getText().slice(dot.range[1], prop.range[0]);
if (dot.type === "Punctuator" && dot.value === ".") {
if (onObject) {
if (!astUtils.isTokenOnSameLine(obj, dot)) {
const neededTextAfterObj = astUtils.isDecimalInteger(obj) ? " " : "";
context.report({
node,
loc: dot.loc.start,
message: "Expected dot to be on same line as object.",
fix: fixer => fixer.replaceTextRange([obj.range[1], prop.range[0]], `${neededTextAfterObj}.${textBeforeDot}${textAfterDot}`)
});
}
} else if (!astUtils.isTokenOnSameLine(dot, prop)) {
context.report({
node,
loc: dot.loc.start,
message: "Expected dot to be on same line as property.",
fix: fixer => fixer.replaceTextRange([obj.range[1], prop.range[0]], `${textBeforeDot}${textAfterDot}.`)
});
}
}
}
/**
* Checks the spacing of the dot within a member expression.
* @param {ASTNode} node The node to check.
* @returns {void}
*/
function checkNode(node) {
checkDotLocation(node.object, node.property, node);
}
return {
MemberExpression: checkNode
};
}
};

View File

@ -0,0 +1,123 @@
/**
* @fileoverview Rule to warn about using dot notation instead of square bracket notation when possible.
* @author Josh Perez
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
const keywords = require("../util/keywords");
module.exports = {
meta: {
docs: {
description: "enforce dot notation whenever possible",
category: "Best Practices",
recommended: false
},
schema: [
{
type: "object",
properties: {
allowKeywords: {
type: "boolean"
},
allowPattern: {
type: "string"
}
},
additionalProperties: false
}
],
fixable: "code"
},
create(context) {
const options = context.options[0] || {};
const allowKeywords = options.allowKeywords === void 0 || !!options.allowKeywords;
const sourceCode = context.getSourceCode();
let allowPattern;
if (options.allowPattern) {
allowPattern = new RegExp(options.allowPattern);
}
return {
MemberExpression(node) {
if (
node.computed &&
node.property.type === "Literal" &&
validIdentifier.test(node.property.value) &&
(allowKeywords || keywords.indexOf(String(node.property.value)) === -1)
) {
if (!(allowPattern && allowPattern.test(node.property.value))) {
context.report({
node: node.property,
message: "[{{propertyValue}}] is better written in dot notation.",
data: {
propertyValue: JSON.stringify(node.property.value)
},
fix(fixer) {
const leftBracket = sourceCode.getTokenAfter(node.object, astUtils.isOpeningBracketToken);
const rightBracket = sourceCode.getLastToken(node);
if (sourceCode.getFirstTokenBetween(leftBracket, rightBracket, { includeComments: true, filter: astUtils.isCommentToken })) {
// Don't perform any fixes if there are comments inside the brackets.
return null;
}
const textBeforeDot = astUtils.isDecimalInteger(node.object) ? " " : "";
return fixer.replaceTextRange(
[leftBracket.range[0], rightBracket.range[1]],
`${textBeforeDot}.${node.property.value}`
);
}
});
}
}
if (
!allowKeywords &&
!node.computed &&
keywords.indexOf(String(node.property.name)) !== -1
) {
context.report({
node: node.property,
message: ".{{propertyName}} is a syntax error.",
data: {
propertyName: node.property.name
},
fix(fixer) {
const dot = sourceCode.getTokenBefore(node.property);
const textAfterDot = sourceCode.text.slice(dot.range[1], node.property.range[0]);
if (textAfterDot.trim()) {
// Don't perform any fixes if there are comments between the dot and the property name.
return null;
}
return fixer.replaceTextRange(
[dot.range[0], node.property.range[1]],
`[${textAfterDot}"${node.property.name}"]`
);
}
});
}
}
};
}
};

View File

@ -0,0 +1,94 @@
/**
* @fileoverview Require or disallow newline at the end of files
* @author Nodeca Team <https://github.com/nodeca>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require or disallow newline at the end of files",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
enum: ["always", "never", "unix", "windows"]
}
]
},
create(context) {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
Program: function checkBadEOF(node) {
const sourceCode = context.getSourceCode(),
src = sourceCode.getText(),
location = {
column: lodash.last(sourceCode.lines).length,
line: sourceCode.lines.length
},
LF = "\n",
CRLF = `\r${LF}`,
endsWithNewline = lodash.endsWith(src, LF);
let mode = context.options[0] || "always",
appendCRLF = false;
if (mode === "unix") {
// `"unix"` should behave exactly as `"always"`
mode = "always";
}
if (mode === "windows") {
// `"windows"` should behave exactly as `"always"`, but append CRLF in the fixer for backwards compatibility
mode = "always";
appendCRLF = true;
}
if (mode === "always" && !endsWithNewline) {
// File is not newline-terminated, but should be
context.report({
node,
loc: location,
message: "Newline required at end of file but not found.",
fix(fixer) {
return fixer.insertTextAfterRange([0, src.length], appendCRLF ? CRLF : LF);
}
});
} else if (mode === "never" && endsWithNewline) {
// File is newline-terminated, but shouldn't be
context.report({
node,
loc: location,
message: "Newline not allowed at end of file.",
fix(fixer) {
const finalEOLs = /(?:\r?\n)+$/,
match = finalEOLs.exec(sourceCode.text),
start = match.index,
end = sourceCode.text.length;
return fixer.replaceTextRange([start, end], "");
}
});
}
}
};
}
};

View File

@ -0,0 +1,180 @@
/**
* @fileoverview Rule to flag statements that use != and == instead of !== and ===
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require the use of `===` and `!==`",
category: "Best Practices",
recommended: false
},
schema: {
anyOf: [
{
type: "array",
items: [
{
enum: ["always"]
},
{
type: "object",
properties: {
null: {
enum: ["always", "never", "ignore"]
}
},
additionalProperties: false
}
],
additionalItems: false
},
{
type: "array",
items: [
{
enum: ["smart", "allow-null"]
}
],
additionalItems: false
}
]
},
fixable: "code"
},
create(context) {
const config = context.options[0] || "always";
const options = context.options[1] || {};
const sourceCode = context.getSourceCode();
const nullOption = (config === "always")
? options.null || "always"
: "ignore";
const enforceRuleForNull = (nullOption === "always");
const enforceInverseRuleForNull = (nullOption === "never");
/**
* Checks if an expression is a typeof expression
* @param {ASTNode} node The node to check
* @returns {boolean} if the node is a typeof expression
*/
function isTypeOf(node) {
return node.type === "UnaryExpression" && node.operator === "typeof";
}
/**
* Checks if either operand of a binary expression is a typeof operation
* @param {ASTNode} node The node to check
* @returns {boolean} if one of the operands is typeof
* @private
*/
function isTypeOfBinary(node) {
return isTypeOf(node.left) || isTypeOf(node.right);
}
/**
* Checks if operands are literals of the same type (via typeof)
* @param {ASTNode} node The node to check
* @returns {boolean} if operands are of same type
* @private
*/
function areLiteralsAndSameType(node) {
return node.left.type === "Literal" && node.right.type === "Literal" &&
typeof node.left.value === typeof node.right.value;
}
/**
* Checks if one of the operands is a literal null
* @param {ASTNode} node The node to check
* @returns {boolean} if operands are null
* @private
*/
function isNullCheck(node) {
return astUtils.isNullLiteral(node.right) || astUtils.isNullLiteral(node.left);
}
/**
* Gets the location (line and column) of the binary expression's operator
* @param {ASTNode} node The binary expression node to check
* @param {string} operator The operator to find
* @returns {Object} { line, column } location of operator
* @private
*/
function getOperatorLocation(node) {
const opToken = sourceCode.getTokenAfter(node.left);
return { line: opToken.loc.start.line, column: opToken.loc.start.column };
}
/**
* Reports a message for this rule.
* @param {ASTNode} node The binary expression node that was checked
* @param {string} expectedOperator The operator that was expected (either '==', '!=', '===', or '!==')
* @returns {void}
* @private
*/
function report(node, expectedOperator) {
context.report({
node,
loc: getOperatorLocation(node),
message: "Expected '{{expectedOperator}}' and instead saw '{{actualOperator}}'.",
data: { expectedOperator, actualOperator: node.operator },
fix(fixer) {
// If the comparison is a `typeof` comparison or both sides are literals with the same type, then it's safe to fix.
if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) {
const operatorToken = sourceCode.getFirstTokenBetween(
node.left,
node.right,
token => token.value === node.operator
);
return fixer.replaceText(operatorToken, expectedOperator);
}
return null;
}
});
}
return {
BinaryExpression(node) {
const isNull = isNullCheck(node);
if (node.operator !== "==" && node.operator !== "!=") {
if (enforceInverseRuleForNull && isNull) {
report(node, node.operator.slice(0, -1));
}
return;
}
if (config === "smart" && (isTypeOfBinary(node) ||
areLiteralsAndSameType(node) || isNull)) {
return;
}
if (!enforceRuleForNull && isNull) {
return;
}
report(node, `${node.operator}=`);
}
};
}
};

View File

@ -0,0 +1,157 @@
/**
* @fileoverview Rule to control spacing within function calls
* @author Matt DuVall <http://www.mattduvall.com>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require or disallow spacing between function identifiers and their invocations",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: {
anyOf: [
{
type: "array",
items: [
{
enum: ["never"]
}
],
minItems: 0,
maxItems: 1
},
{
type: "array",
items: [
{
enum: ["always"]
},
{
type: "object",
properties: {
allowNewlines: {
type: "boolean"
}
},
additionalProperties: false
}
],
minItems: 0,
maxItems: 2
}
]
}
},
create(context) {
const never = context.options[0] !== "always";
const allowNewlines = !never && context.options[1] && context.options[1].allowNewlines;
const sourceCode = context.getSourceCode();
const text = sourceCode.getText();
/**
* Check if open space is present in a function name
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function checkSpacing(node) {
const lastToken = sourceCode.getLastToken(node);
const lastCalleeToken = sourceCode.getLastToken(node.callee);
const parenToken = sourceCode.getFirstTokenBetween(lastCalleeToken, lastToken, astUtils.isOpeningParenToken);
const prevToken = parenToken && sourceCode.getTokenBefore(parenToken);
// Parens in NewExpression are optional
if (!(parenToken && parenToken.range[1] < node.range[1])) {
return;
}
const textBetweenTokens = text.slice(prevToken.range[1], parenToken.range[0]).replace(/\/\*.*?\*\//g, "");
const hasWhitespace = /\s/.test(textBetweenTokens);
const hasNewline = hasWhitespace && astUtils.LINEBREAK_MATCHER.test(textBetweenTokens);
/*
* never allowNewlines hasWhitespace hasNewline message
* F F F F Missing space between function name and paren.
* F F F T (Invalid `!hasWhitespace && hasNewline`)
* F F T T Unexpected newline between function name and paren.
* F F T F (OK)
* F T T F (OK)
* F T T T (OK)
* F T F T (Invalid `!hasWhitespace && hasNewline`)
* F T F F Missing space between function name and paren.
* T T F F (Invalid `never && allowNewlines`)
* T T F T (Invalid `!hasWhitespace && hasNewline`)
* T T T T (Invalid `never && allowNewlines`)
* T T T F (Invalid `never && allowNewlines`)
* T F T F Unexpected space between function name and paren.
* T F T T Unexpected space between function name and paren.
* T F F T (Invalid `!hasWhitespace && hasNewline`)
* T F F F (OK)
*
* T T Unexpected space between function name and paren.
* F F Missing space between function name and paren.
* F F T Unexpected newline between function name and paren.
*/
if (never && hasWhitespace) {
context.report({
node,
loc: lastCalleeToken.loc.start,
message: "Unexpected space between function name and paren.",
fix(fixer) {
// Only autofix if there is no newline
// https://github.com/eslint/eslint/issues/7787
if (!hasNewline) {
return fixer.removeRange([prevToken.range[1], parenToken.range[0]]);
}
return null;
}
});
} else if (!never && !hasWhitespace) {
context.report({
node,
loc: lastCalleeToken.loc.start,
message: "Missing space between function name and paren.",
fix(fixer) {
return fixer.insertTextBefore(parenToken, " ");
}
});
} else if (!never && !allowNewlines && hasNewline) {
context.report({
node,
loc: lastCalleeToken.loc.start,
message: "Unexpected newline between function name and paren.",
fix(fixer) {
return fixer.replaceTextRange([prevToken.range[1], parenToken.range[0]], " ");
}
});
}
}
return {
CallExpression: checkSpacing,
NewExpression: checkSpacing
};
}
};

View File

@ -0,0 +1,193 @@
/**
* @fileoverview Rule to require function names to match the name of the variable or property to which they are assigned.
* @author Annie Zhang, Pavel Strashkin
*/
"use strict";
//--------------------------------------------------------------------------
// Requirements
//--------------------------------------------------------------------------
const astUtils = require("../ast-utils");
const esutils = require("esutils");
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Determines if a pattern is `module.exports` or `module["exports"]`
* @param {ASTNode} pattern The left side of the AssignmentExpression
* @returns {boolean} True if the pattern is `module.exports` or `module["exports"]`
*/
function isModuleExports(pattern) {
if (pattern.type === "MemberExpression" && pattern.object.type === "Identifier" && pattern.object.name === "module") {
// module.exports
if (pattern.property.type === "Identifier" && pattern.property.name === "exports") {
return true;
}
// module["exports"]
if (pattern.property.type === "Literal" && pattern.property.value === "exports") {
return true;
}
}
return false;
}
/**
* Determines if a string name is a valid identifier
* @param {string} name The string to be checked
* @param {int} ecmaVersion The ECMAScript version if specified in the parserOptions config
* @returns {boolean} True if the string is a valid identifier
*/
function isIdentifier(name, ecmaVersion) {
if (ecmaVersion >= 6) {
return esutils.keyword.isIdentifierES6(name);
}
return esutils.keyword.isIdentifierES5(name);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const alwaysOrNever = { enum: ["always", "never"] };
const optionsObject = {
type: "object",
properties: {
includeCommonJSModuleExports: {
type: "boolean"
}
},
additionalProperties: false
};
module.exports = {
meta: {
docs: {
description: "require function names to match the name of the variable or property to which they are assigned",
category: "Stylistic Issues",
recommended: false
},
schema: {
anyOf: [{
type: "array",
additionalItems: false,
items: [alwaysOrNever, optionsObject]
}, {
type: "array",
additionalItems: false,
items: [optionsObject]
}]
}
},
create(context) {
const options = (typeof context.options[0] === "object" ? context.options[0] : context.options[1]) || {};
const nameMatches = typeof context.options[0] === "string" ? context.options[0] : "always";
const includeModuleExports = options.includeCommonJSModuleExports;
const ecmaVersion = context.parserOptions && context.parserOptions.ecmaVersion ? context.parserOptions.ecmaVersion : 5;
/**
* Compares identifiers based on the nameMatches option
* @param {string} x the first identifier
* @param {string} y the second identifier
* @returns {boolean} whether the two identifiers should warn.
*/
function shouldWarn(x, y) {
return (nameMatches === "always" && x !== y) || (nameMatches === "never" && x === y);
}
/**
* Reports
* @param {ASTNode} node The node to report
* @param {string} name The variable or property name
* @param {string} funcName The function name
* @param {boolean} isProp True if the reported node is a property assignment
* @returns {void}
*/
function report(node, name, funcName, isProp) {
let message;
if (nameMatches === "always" && isProp) {
message = "Function name `{{funcName}}` should match property name `{{name}}`";
} else if (nameMatches === "always") {
message = "Function name `{{funcName}}` should match variable name `{{name}}`";
} else if (isProp) {
message = "Function name `{{funcName}}` should not match property name `{{name}}`";
} else {
message = "Function name `{{funcName}}` should not match variable name `{{name}}`";
}
context.report({
node,
message,
data: {
name,
funcName
}
});
}
/**
* Determines whether a given node is a string literal
* @param {ASTNode} node The node to check
* @returns {boolean} `true` if the node is a string literal
*/
function isStringLiteral(node) {
return node.type === "Literal" && typeof node.value === "string";
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
VariableDeclarator(node) {
if (!node.init || node.init.type !== "FunctionExpression" || node.id.type !== "Identifier") {
return;
}
if (node.init.id && shouldWarn(node.id.name, node.init.id.name)) {
report(node, node.id.name, node.init.id.name, false);
}
},
AssignmentExpression(node) {
if (
node.right.type !== "FunctionExpression" ||
(node.left.computed && node.left.property.type !== "Literal") ||
(!includeModuleExports && isModuleExports(node.left)) ||
(node.left.type !== "Identifier" && node.left.type !== "MemberExpression")
) {
return;
}
const isProp = node.left.type === "MemberExpression";
const name = isProp ? astUtils.getStaticPropertyName(node.left) : node.left.name;
if (node.right.id && isIdentifier(name) && shouldWarn(name, node.right.id.name)) {
report(node, name, node.right.id.name, isProp);
}
},
Property(node) {
if (node.value.type !== "FunctionExpression" || !node.value.id || node.computed && !isStringLiteral(node.key)) {
return;
}
if (node.key.type === "Identifier" && shouldWarn(node.key.name, node.value.id.name)) {
report(node, node.key.name, node.value.id.name, true);
} else if (
isStringLiteral(node.key) &&
isIdentifier(node.key.value, ecmaVersion) &&
shouldWarn(node.key.value, node.value.id.name)
) {
report(node, node.key.value, node.value.id.name, true);
}
}
};
}
};

View File

@ -0,0 +1,114 @@
/**
* @fileoverview Rule to warn when a function expression does not have a name.
* @author Kyle T. Nunery
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
/**
* Checks whether or not a given variable is a function name.
* @param {escope.Variable} variable - A variable to check.
* @returns {boolean} `true` if the variable is a function name.
*/
function isFunctionName(variable) {
return variable && variable.defs[0].type === "FunctionName";
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require or disallow named `function` expressions",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
enum: ["always", "as-needed", "never"]
}
]
},
create(context) {
const never = context.options[0] === "never";
const asNeeded = context.options[0] === "as-needed";
/**
* Determines whether the current FunctionExpression node is a get, set, or
* shorthand method in an object literal or a class.
* @param {ASTNode} node - A node to check.
* @returns {boolean} True if the node is a get, set, or shorthand method.
*/
function isObjectOrClassMethod(node) {
const parent = node.parent;
return (parent.type === "MethodDefinition" || (
parent.type === "Property" && (
parent.method ||
parent.kind === "get" ||
parent.kind === "set"
)
));
}
/**
* Determines whether the current FunctionExpression node has a name that would be
* inferred from context in a conforming ES6 environment.
* @param {ASTNode} node - A node to check.
* @returns {boolean} True if the node would have a name assigned automatically.
*/
function hasInferredName(node) {
const parent = node.parent;
return isObjectOrClassMethod(node) ||
(parent.type === "VariableDeclarator" && parent.id.type === "Identifier" && parent.init === node) ||
(parent.type === "Property" && parent.value === node) ||
(parent.type === "AssignmentExpression" && parent.left.type === "Identifier" && parent.right === node) ||
(parent.type === "ExportDefaultDeclaration" && parent.declaration === node) ||
(parent.type === "AssignmentPattern" && parent.right === node);
}
return {
"FunctionExpression:exit"(node) {
// Skip recursive functions.
const nameVar = context.getDeclaredVariables(node)[0];
if (isFunctionName(nameVar) && nameVar.references.length > 0) {
return;
}
const hasName = Boolean(node.id && node.id.name);
const name = astUtils.getFunctionNameWithKind(node);
if (never) {
if (hasName) {
context.report({
node,
message: "Unexpected named {{name}}.",
data: { name }
});
}
} else {
if (!hasName && (asNeeded ? !hasInferredName(node) : !isObjectOrClassMethod(node))) {
context.report({
node,
message: "Unexpected unnamed {{name}}.",
data: { name }
});
}
}
}
};
}
};

View File

@ -0,0 +1,89 @@
/**
* @fileoverview Rule to enforce a particular function style
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce the consistent use of either `function` declarations or expressions",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
enum: ["declaration", "expression"]
},
{
type: "object",
properties: {
allowArrowFunctions: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
const style = context.options[0],
allowArrowFunctions = context.options[1] && context.options[1].allowArrowFunctions === true,
enforceDeclarations = (style === "declaration"),
stack = [];
const nodesToCheck = {
FunctionDeclaration(node) {
stack.push(false);
if (!enforceDeclarations && node.parent.type !== "ExportDefaultDeclaration") {
context.report({ node, message: "Expected a function expression." });
}
},
"FunctionDeclaration:exit"() {
stack.pop();
},
FunctionExpression(node) {
stack.push(false);
if (enforceDeclarations && node.parent.type === "VariableDeclarator") {
context.report({ node: node.parent, message: "Expected a function declaration." });
}
},
"FunctionExpression:exit"() {
stack.pop();
},
ThisExpression() {
if (stack.length > 0) {
stack[stack.length - 1] = true;
}
}
};
if (!allowArrowFunctions) {
nodesToCheck.ArrowFunctionExpression = function() {
stack.push(false);
};
nodesToCheck["ArrowFunctionExpression:exit"] = function(node) {
const hasThisExpr = stack.pop();
if (enforceDeclarations && !hasThisExpr && node.parent.type === "VariableDeclarator") {
context.report({ node: node.parent, message: "Expected a function declaration." });
}
};
}
return nodesToCheck;
}
};

View File

@ -0,0 +1,148 @@
/**
* @fileoverview Rule to check the spacing around the * in generator functions.
* @author Jamund Ferguson
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent spacing around `*` operators in generator functions",
category: "ECMAScript 6",
recommended: false
},
fixable: "whitespace",
schema: [
{
oneOf: [
{
enum: ["before", "after", "both", "neither"]
},
{
type: "object",
properties: {
before: { type: "boolean" },
after: { type: "boolean" }
},
additionalProperties: false
}
]
}
]
},
create(context) {
const mode = (function(option) {
if (!option || typeof option === "string") {
return {
before: { before: true, after: false },
after: { before: false, after: true },
both: { before: true, after: true },
neither: { before: false, after: false }
}[option || "before"];
}
return option;
}(context.options[0]));
const sourceCode = context.getSourceCode();
/**
* Checks if the given token is a star token or not.
*
* @param {Token} token - The token to check.
* @returns {boolean} `true` if the token is a star token.
*/
function isStarToken(token) {
return token.value === "*" && token.type === "Punctuator";
}
/**
* Gets the generator star token of the given function node.
*
* @param {ASTNode} node - The function node to get.
* @returns {Token} Found star token.
*/
function getStarToken(node) {
return sourceCode.getFirstToken(
(node.parent.method || node.parent.type === "MethodDefinition") ? node.parent : node,
isStarToken
);
}
/**
* Checks the spacing between two tokens before or after the star token.
* @param {string} side Either "before" or "after".
* @param {Token} leftToken `function` keyword token if side is "before", or
* star token if side is "after".
* @param {Token} rightToken Star token if side is "before", or identifier
* token if side is "after".
* @returns {void}
*/
function checkSpacing(side, leftToken, rightToken) {
if (!!(rightToken.range[0] - leftToken.range[1]) !== mode[side]) {
const after = leftToken.value === "*";
const spaceRequired = mode[side];
const node = after ? leftToken : rightToken;
const type = spaceRequired ? "Missing" : "Unexpected";
const message = "{{type}} space {{side}} *.";
const data = {
type,
side
};
context.report({
node,
message,
data,
fix(fixer) {
if (spaceRequired) {
if (after) {
return fixer.insertTextAfter(node, " ");
}
return fixer.insertTextBefore(node, " ");
}
return fixer.removeRange([leftToken.range[1], rightToken.range[0]]);
}
});
}
}
/**
* Enforces the spacing around the star if node is a generator function.
* @param {ASTNode} node A function expression or declaration node.
* @returns {void}
*/
function checkFunction(node) {
if (!node.generator) {
return;
}
const starToken = getStarToken(node);
// Only check before when preceded by `function`|`static` keyword
const prevToken = sourceCode.getTokenBefore(starToken);
if (prevToken.value === "function" || prevToken.value === "static") {
checkSpacing("before", prevToken, starToken);
}
const nextToken = sourceCode.getTokenAfter(starToken);
checkSpacing("after", starToken, nextToken);
}
return {
FunctionDeclaration: checkFunction,
FunctionExpression: checkFunction
};
}
};

View File

@ -0,0 +1,75 @@
/**
* @fileoverview Rule for disallowing require() outside of the top-level module context
* @author Jamund Ferguson
*/
"use strict";
const ACCEPTABLE_PARENTS = [
"AssignmentExpression",
"VariableDeclarator",
"MemberExpression",
"ExpressionStatement",
"CallExpression",
"ConditionalExpression",
"Program",
"VariableDeclaration"
];
/**
* Finds the escope reference in the given scope.
* @param {Object} scope The scope to search.
* @param {ASTNode} node The identifier node.
* @returns {Reference|null} Returns the found reference or null if none were found.
*/
function findReference(scope, node) {
const references = scope.references.filter(reference => reference.identifier.range[0] === node.range[0] &&
reference.identifier.range[1] === node.range[1]);
/* istanbul ignore else: correctly returns null */
if (references.length === 1) {
return references[0];
}
return null;
}
/**
* Checks if the given identifier node is shadowed in the given scope.
* @param {Object} scope The current scope.
* @param {ASTNode} node The identifier node to check.
* @returns {boolean} Whether or not the name is shadowed.
*/
function isShadowed(scope, node) {
const reference = findReference(scope, node);
return reference && reference.resolved && reference.resolved.defs.length > 0;
}
module.exports = {
meta: {
docs: {
description: "require `require()` calls to be placed at top-level module scope",
category: "Node.js and CommonJS",
recommended: false
},
schema: []
},
create(context) {
return {
CallExpression(node) {
const currentScope = context.getScope();
if (node.callee.name === "require" && !isShadowed(currentScope, node.callee)) {
const isGoodRequire = context.getAncestors().every(parent => ACCEPTABLE_PARENTS.indexOf(parent.type) > -1);
if (!isGoodRequire) {
context.report({ node, message: "Unexpected require()." });
}
}
}
};
}
};

View File

@ -0,0 +1,42 @@
/**
* @fileoverview Rule to flag for-in loops without if statements inside
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require `for-in` loops to include an `if` statement",
category: "Best Practices",
recommended: false
},
schema: []
},
create(context) {
return {
ForInStatement(node) {
/*
* If the for-in statement has {}, then the real body is the body
* of the BlockStatement. Otherwise, just use body as provided.
*/
const body = node.body.type === "BlockStatement" ? node.body.body[0] : node.body;
if (body && body.type !== "IfStatement") {
context.report({ node, message: "The body of a for-in should be wrapped in an if statement to filter unwanted properties from the prototype." });
}
}
};
}
};

View File

@ -0,0 +1,89 @@
/**
* @fileoverview Ensure handling of errors when we know they exist.
* @author Jamund Ferguson
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require error handling in callbacks",
category: "Node.js and CommonJS",
recommended: false
},
schema: [
{
type: "string"
}
]
},
create(context) {
const errorArgument = context.options[0] || "err";
/**
* Checks if the given argument should be interpreted as a regexp pattern.
* @param {string} stringToCheck The string which should be checked.
* @returns {boolean} Whether or not the string should be interpreted as a pattern.
*/
function isPattern(stringToCheck) {
const firstChar = stringToCheck[0];
return firstChar === "^";
}
/**
* Checks if the given name matches the configured error argument.
* @param {string} name The name which should be compared.
* @returns {boolean} Whether or not the given name matches the configured error variable name.
*/
function matchesConfiguredErrorName(name) {
if (isPattern(errorArgument)) {
const regexp = new RegExp(errorArgument);
return regexp.test(name);
}
return name === errorArgument;
}
/**
* Get the parameters of a given function scope.
* @param {Object} scope The function scope.
* @returns {array} All parameters of the given scope.
*/
function getParameters(scope) {
return scope.variables.filter(variable => variable.defs[0] && variable.defs[0].type === "Parameter");
}
/**
* Check to see if we're handling the error object properly.
* @param {ASTNode} node The AST node to check.
* @returns {void}
*/
function checkForError(node) {
const scope = context.getScope(),
parameters = getParameters(scope),
firstParameter = parameters[0];
if (firstParameter && matchesConfiguredErrorName(firstParameter.name)) {
if (firstParameter.references.length === 0) {
context.report({ node, message: "Expected error to be handled." });
}
}
}
return {
FunctionDeclaration: checkForError,
FunctionExpression: checkForError,
ArrowFunctionExpression: checkForError
};
}
};

View File

@ -0,0 +1,117 @@
/**
* @fileoverview Rule that warns when identifier names that are
* blacklisted in the configuration are used.
* @author Keith Cirkel (http://keithcirkel.co.uk)
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow specified identifiers",
category: "Stylistic Issues",
recommended: false
},
schema: {
type: "array",
items: {
type: "string"
},
uniqueItems: true
}
},
create(context) {
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
const blacklist = context.options;
/**
* Checks if a string matches the provided pattern
* @param {string} name The string to check.
* @returns {boolean} if the string is a match
* @private
*/
function isInvalid(name) {
return blacklist.indexOf(name) !== -1;
}
/**
* Verifies if we should report an error or not based on the effective
* parent node and the identifier name.
* @param {ASTNode} effectiveParent The effective parent node of the node to be reported
* @param {string} name The identifier name of the identifier node
* @returns {boolean} whether an error should be reported or not
*/
function shouldReport(effectiveParent, name) {
return effectiveParent.type !== "CallExpression" &&
effectiveParent.type !== "NewExpression" &&
isInvalid(name);
}
/**
* Reports an AST node as a rule violation.
* @param {ASTNode} node The node to report.
* @returns {void}
* @private
*/
function report(node) {
context.report({ node, message: "Identifier '{{name}}' is blacklisted.", data: {
name: node.name
} });
}
return {
Identifier(node) {
const name = node.name,
effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent;
// MemberExpressions get special rules
if (node.parent.type === "MemberExpression") {
// Always check object names
if (node.parent.object.type === "Identifier" &&
node.parent.object.name === node.name) {
if (isInvalid(name)) {
report(node);
}
// Report AssignmentExpressions only if they are the left side of the assignment
} else if (effectiveParent.type === "AssignmentExpression" &&
(effectiveParent.right.type !== "MemberExpression" ||
effectiveParent.left.type === "MemberExpression" &&
effectiveParent.left.property.name === node.name)) {
if (isInvalid(name)) {
report(node);
}
}
// Properties have their own rules
} else if (node.parent.type === "Property") {
if (shouldReport(effectiveParent, name)) {
report(node);
}
// Report anything that is a match and not a CallExpression
} else if (shouldReport(effectiveParent, name)) {
report(node);
}
}
};
}
};

View File

@ -0,0 +1,116 @@
/**
* @fileoverview Rule that warns when identifier names are shorter or longer
* than the values provided in configuration.
* @author Burak Yigit Kaya aka BYK
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce minimum and maximum identifier lengths",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
type: "object",
properties: {
min: {
type: "number"
},
max: {
type: "number"
},
exceptions: {
type: "array",
uniqueItems: true,
items: {
type: "string"
}
},
properties: {
enum: ["always", "never"]
}
},
additionalProperties: false
}
]
},
create(context) {
const options = context.options[0] || {};
const minLength = typeof options.min !== "undefined" ? options.min : 2;
const maxLength = typeof options.max !== "undefined" ? options.max : Infinity;
const properties = options.properties !== "never";
const exceptions = (options.exceptions ? options.exceptions : [])
.reduce((obj, item) => {
obj[item] = true;
return obj;
}, {});
const SUPPORTED_EXPRESSIONS = {
MemberExpression: properties && function(parent) {
return !parent.computed && (
// regular property assignment
(parent.parent.left === parent && parent.parent.type === "AssignmentExpression" ||
// or the last identifier in an ObjectPattern destructuring
parent.parent.type === "Property" && parent.parent.value === parent &&
parent.parent.parent.type === "ObjectPattern" && parent.parent.parent.parent.left === parent.parent.parent)
);
},
AssignmentPattern(parent, node) {
return parent.left === node;
},
VariableDeclarator(parent, node) {
return parent.id === node;
},
Property: properties && function(parent, node) {
return parent.key === node;
},
ImportDefaultSpecifier: true,
RestElement: true,
FunctionExpression: true,
ArrowFunctionExpression: true,
ClassDeclaration: true,
FunctionDeclaration: true,
MethodDefinition: true,
CatchClause: true
};
return {
Identifier(node) {
const name = node.name;
const parent = node.parent;
const isShort = name.length < minLength;
const isLong = name.length > maxLength;
if (!(isShort || isLong) || exceptions[name]) {
return; // Nothing to report
}
const isValidExpression = SUPPORTED_EXPRESSIONS[parent.type];
if (isValidExpression && (isValidExpression === true || isValidExpression(parent, node))) {
context.report({
node,
message: isShort
? "Identifier name '{{name}}' is too short (< {{min}})."
: "Identifier name '{{name}}' is too long (> {{max}}).",
data: { name, min: minLength, max: maxLength }
});
}
}
};
}
};

View File

@ -0,0 +1,140 @@
/**
* @fileoverview Rule to flag non-matching identifiers
* @author Matthieu Larcher
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require identifiers to match a specified regular expression",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
type: "string"
},
{
type: "object",
properties: {
properties: {
type: "boolean"
}
}
}
]
},
create(context) {
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
const pattern = context.options[0] || "^.+$",
regexp = new RegExp(pattern);
const options = context.options[1] || {},
properties = !!options.properties,
onlyDeclarations = !!options.onlyDeclarations;
/**
* Checks if a string matches the provided pattern
* @param {string} name The string to check.
* @returns {boolean} if the string is a match
* @private
*/
function isInvalid(name) {
return !regexp.test(name);
}
/**
* Verifies if we should report an error or not based on the effective
* parent node and the identifier name.
* @param {ASTNode} effectiveParent The effective parent node of the node to be reported
* @param {string} name The identifier name of the identifier node
* @returns {boolean} whether an error should be reported or not
*/
function shouldReport(effectiveParent, name) {
return effectiveParent.type !== "CallExpression" &&
effectiveParent.type !== "NewExpression" &&
isInvalid(name);
}
/**
* Reports an AST node as a rule violation.
* @param {ASTNode} node The node to report.
* @returns {void}
* @private
*/
function report(node) {
context.report({ node, message: "Identifier '{{name}}' does not match the pattern '{{pattern}}'.", data: {
name: node.name,
pattern
} });
}
return {
Identifier(node) {
const name = node.name,
parent = node.parent,
effectiveParent = (parent.type === "MemberExpression") ? parent.parent : parent;
if (parent.type === "MemberExpression") {
if (!properties) {
return;
}
// Always check object names
if (parent.object.type === "Identifier" &&
parent.object.name === name) {
if (isInvalid(name)) {
report(node);
}
// Report AssignmentExpressions only if they are the left side of the assignment
} else if (effectiveParent.type === "AssignmentExpression" &&
(effectiveParent.right.type !== "MemberExpression" ||
effectiveParent.left.type === "MemberExpression" &&
effectiveParent.left.property.name === name)) {
if (isInvalid(name)) {
report(node);
}
}
} else if (parent.type === "Property") {
if (!properties || parent.key.name !== name) {
return;
}
if (shouldReport(effectiveParent, name)) {
report(node);
}
} else {
const isDeclaration = effectiveParent.type === "FunctionDeclaration" || effectiveParent.type === "VariableDeclarator";
if (onlyDeclarations && !isDeclaration) {
return;
}
if (shouldReport(effectiveParent, name)) {
report(node);
}
}
}
};
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,137 @@
/**
* @fileoverview A rule to control the style of variable initializations.
* @author Colin Ihrig
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether or not a given node is a for loop.
* @param {ASTNode} block - A node to check.
* @returns {boolean} `true` when the node is a for loop.
*/
function isForLoop(block) {
return block.type === "ForInStatement" ||
block.type === "ForOfStatement" ||
block.type === "ForStatement";
}
/**
* Checks whether or not a given declarator node has its initializer.
* @param {ASTNode} node - A declarator node to check.
* @returns {boolean} `true` when the node has its initializer.
*/
function isInitialized(node) {
const declaration = node.parent;
const block = declaration.parent;
if (isForLoop(block)) {
if (block.type === "ForStatement") {
return block.init === declaration;
}
return block.left === declaration;
}
return Boolean(node.init);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require or disallow initialization in variable declarations",
category: "Variables",
recommended: false
},
schema: {
anyOf: [
{
type: "array",
items: [
{
enum: ["always"]
}
],
minItems: 0,
maxItems: 1
},
{
type: "array",
items: [
{
enum: ["never"]
},
{
type: "object",
properties: {
ignoreForLoopInit: {
type: "boolean"
}
},
additionalProperties: false
}
],
minItems: 0,
maxItems: 2
}
]
}
},
create(context) {
const MODE_ALWAYS = "always",
MODE_NEVER = "never";
const mode = context.options[0] || MODE_ALWAYS;
const params = context.options[1] || {};
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
"VariableDeclaration:exit"(node) {
const kind = node.kind,
declarations = node.declarations;
for (let i = 0; i < declarations.length; ++i) {
const declaration = declarations[i],
id = declaration.id,
initialized = isInitialized(declaration),
isIgnoredForLoop = params.ignoreForLoopInit && isForLoop(node.parent);
if (id.type !== "Identifier") {
continue;
}
if (mode === MODE_ALWAYS && !initialized) {
context.report({
node: declaration,
message: "Variable '{{idName}}' should be initialized on declaration.",
data: {
idName: id.name
}
});
} else if (mode === MODE_NEVER && kind !== "const" && initialized && !isIgnoredForLoop) {
context.report({
node: declaration,
message: "Variable '{{idName}}' should not be initialized on declaration.",
data: {
idName: id.name
}
});
}
}
}
};
}
};

View File

@ -0,0 +1,89 @@
/**
* @fileoverview A rule to ensure consistent quotes used in jsx syntax.
* @author Mathias Schreck <https://github.com/lo1tuma>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------
const QUOTE_SETTINGS = {
"prefer-double": {
quote: "\"",
description: "singlequote",
convert(str) {
return str.replace(/'/g, "\"");
}
},
"prefer-single": {
quote: "'",
description: "doublequote",
convert(str) {
return str.replace(/"/g, "'");
}
}
};
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce the consistent use of either double or single quotes in JSX attributes",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
enum: ["prefer-single", "prefer-double"]
}
]
},
create(context) {
const quoteOption = context.options[0] || "prefer-double",
setting = QUOTE_SETTINGS[quoteOption];
/**
* Checks if the given string literal node uses the expected quotes
* @param {ASTNode} node - A string literal node.
* @returns {boolean} Whether or not the string literal used the expected quotes.
* @public
*/
function usesExpectedQuotes(node) {
return node.value.indexOf(setting.quote) !== -1 || astUtils.isSurroundedBy(node.raw, setting.quote);
}
return {
JSXAttribute(node) {
const attributeValue = node.value;
if (attributeValue && astUtils.isStringLiteral(attributeValue) && !usesExpectedQuotes(attributeValue)) {
context.report({
node: attributeValue,
message: "Unexpected usage of {{description}}.",
data: {
description: setting.description
},
fix(fixer) {
return fixer.replaceText(attributeValue, setting.convert(attributeValue.raw));
}
});
}
}
};
}
};

View File

@ -0,0 +1,639 @@
/**
* @fileoverview Rule to specify spacing of object literal keys and values
* @author Brandon Mills
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether a string contains a line terminator as defined in
* http://www.ecma-international.org/ecma-262/5.1/#sec-7.3
* @param {string} str String to test.
* @returns {boolean} True if str contains a line terminator.
*/
function containsLineTerminator(str) {
return astUtils.LINEBREAK_MATCHER.test(str);
}
/**
* Gets the last element of an array.
* @param {Array} arr An array.
* @returns {any} Last element of arr.
*/
function last(arr) {
return arr[arr.length - 1];
}
/**
* Checks whether a property is a member of the property group it follows.
* @param {ASTNode} lastMember The last Property known to be in the group.
* @param {ASTNode} candidate The next Property that might be in the group.
* @returns {boolean} True if the candidate property is part of the group.
*/
function continuesPropertyGroup(lastMember, candidate) {
const groupEndLine = lastMember.loc.start.line,
candidateStartLine = candidate.loc.start.line;
if (candidateStartLine - groupEndLine <= 1) {
return true;
}
// Check that the first comment is adjacent to the end of the group, the
// last comment is adjacent to the candidate property, and that successive
// comments are adjacent to each other.
const comments = candidate.leadingComments;
if (
comments &&
comments[0].loc.start.line - groupEndLine <= 1 &&
candidateStartLine - last(comments).loc.end.line <= 1
) {
for (let i = 1; i < comments.length; i++) {
if (comments[i].loc.start.line - comments[i - 1].loc.end.line > 1) {
return false;
}
}
return true;
}
return false;
}
/**
* Checks whether a node is contained on a single line.
* @param {ASTNode} node AST Node being evaluated.
* @returns {boolean} True if the node is a single line.
*/
function isSingleLine(node) {
return (node.loc.end.line === node.loc.start.line);
}
/**
* Initializes a single option property from the configuration with defaults for undefined values
* @param {Object} toOptions Object to be initialized
* @param {Object} fromOptions Object to be initialized from
* @returns {Object} The object with correctly initialized options and values
*/
function initOptionProperty(toOptions, fromOptions) {
toOptions.mode = fromOptions.mode || "strict";
// Set value of beforeColon
if (typeof fromOptions.beforeColon !== "undefined") {
toOptions.beforeColon = +fromOptions.beforeColon;
} else {
toOptions.beforeColon = 0;
}
// Set value of afterColon
if (typeof fromOptions.afterColon !== "undefined") {
toOptions.afterColon = +fromOptions.afterColon;
} else {
toOptions.afterColon = 1;
}
// Set align if exists
if (typeof fromOptions.align !== "undefined") {
if (typeof fromOptions.align === "object") {
toOptions.align = fromOptions.align;
} else { // "string"
toOptions.align = {
on: fromOptions.align,
mode: toOptions.mode,
beforeColon: toOptions.beforeColon,
afterColon: toOptions.afterColon
};
}
}
return toOptions;
}
/**
* Initializes all the option values (singleLine, multiLine and align) from the configuration with defaults for undefined values
* @param {Object} toOptions Object to be initialized
* @param {Object} fromOptions Object to be initialized from
* @returns {Object} The object with correctly initialized options and values
*/
function initOptions(toOptions, fromOptions) {
if (typeof fromOptions.align === "object") {
// Initialize the alignment configuration
toOptions.align = initOptionProperty({}, fromOptions.align);
toOptions.align.on = fromOptions.align.on || "colon";
toOptions.align.mode = fromOptions.align.mode || "strict";
toOptions.multiLine = initOptionProperty({}, (fromOptions.multiLine || fromOptions));
toOptions.singleLine = initOptionProperty({}, (fromOptions.singleLine || fromOptions));
} else { // string or undefined
toOptions.multiLine = initOptionProperty({}, (fromOptions.multiLine || fromOptions));
toOptions.singleLine = initOptionProperty({}, (fromOptions.singleLine || fromOptions));
// If alignment options are defined in multiLine, pull them out into the general align configuration
if (toOptions.multiLine.align) {
toOptions.align = {
on: toOptions.multiLine.align.on,
mode: toOptions.multiLine.align.mode || toOptions.multiLine.mode,
beforeColon: toOptions.multiLine.align.beforeColon,
afterColon: toOptions.multiLine.align.afterColon
};
}
}
return toOptions;
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const messages = {
key: "{{error}} space after {{computed}}key '{{key}}'.",
value: "{{error}} space before value for {{computed}}key '{{key}}'."
};
module.exports = {
meta: {
docs: {
description: "enforce consistent spacing between keys and values in object literal properties",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [{
anyOf: [
{
type: "object",
properties: {
align: {
anyOf: [
{
enum: ["colon", "value"]
},
{
type: "object",
properties: {
mode: {
enum: ["strict", "minimum"]
},
on: {
enum: ["colon", "value"]
},
beforeColon: {
type: "boolean"
},
afterColon: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
mode: {
enum: ["strict", "minimum"]
},
beforeColon: {
type: "boolean"
},
afterColon: {
type: "boolean"
}
},
additionalProperties: false
},
{
type: "object",
properties: {
singleLine: {
type: "object",
properties: {
mode: {
enum: ["strict", "minimum"]
},
beforeColon: {
type: "boolean"
},
afterColon: {
type: "boolean"
}
},
additionalProperties: false
},
multiLine: {
type: "object",
properties: {
align: {
anyOf: [
{
enum: ["colon", "value"]
},
{
type: "object",
properties: {
mode: {
enum: ["strict", "minimum"]
},
on: {
enum: ["colon", "value"]
},
beforeColon: {
type: "boolean"
},
afterColon: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
mode: {
enum: ["strict", "minimum"]
},
beforeColon: {
type: "boolean"
},
afterColon: {
type: "boolean"
}
},
additionalProperties: false
}
},
additionalProperties: false
},
{
type: "object",
properties: {
singleLine: {
type: "object",
properties: {
mode: {
enum: ["strict", "minimum"]
},
beforeColon: {
type: "boolean"
},
afterColon: {
type: "boolean"
}
},
additionalProperties: false
},
multiLine: {
type: "object",
properties: {
mode: {
enum: ["strict", "minimum"]
},
beforeColon: {
type: "boolean"
},
afterColon: {
type: "boolean"
}
},
additionalProperties: false
},
align: {
type: "object",
properties: {
mode: {
enum: ["strict", "minimum"]
},
on: {
enum: ["colon", "value"]
},
beforeColon: {
type: "boolean"
},
afterColon: {
type: "boolean"
}
},
additionalProperties: false
}
},
additionalProperties: false
}
]
}]
},
create(context) {
/**
* OPTIONS
* "key-spacing": [2, {
* beforeColon: false,
* afterColon: true,
* align: "colon" // Optional, or "value"
* }
*/
const options = context.options[0] || {},
ruleOptions = initOptions({}, options),
multiLineOptions = ruleOptions.multiLine,
singleLineOptions = ruleOptions.singleLine,
alignmentOptions = ruleOptions.align || null;
const sourceCode = context.getSourceCode();
/**
* Determines if the given property is key-value property.
* @param {ASTNode} property Property node to check.
* @returns {boolean} Whether the property is a key-value property.
*/
function isKeyValueProperty(property) {
return !(
(property.method ||
property.shorthand ||
property.kind !== "init" || property.type !== "Property") // Could be "ExperimentalSpreadProperty" or "SpreadProperty"
);
}
/**
* Starting from the given a node (a property.key node here) looks forward
* until it finds the last token before a colon punctuator and returns it.
* @param {ASTNode} node The node to start looking from.
* @returns {ASTNode} The last token before a colon punctuator.
*/
function getLastTokenBeforeColon(node) {
const colonToken = sourceCode.getTokenAfter(node, astUtils.isColonToken);
return sourceCode.getTokenBefore(colonToken);
}
/**
* Starting from the given a node (a property.key node here) looks forward
* until it finds the colon punctuator and returns it.
* @param {ASTNode} node The node to start looking from.
* @returns {ASTNode} The colon punctuator.
*/
function getNextColon(node) {
return sourceCode.getTokenAfter(node, astUtils.isColonToken);
}
/**
* Gets an object literal property's key as the identifier name or string value.
* @param {ASTNode} property Property node whose key to retrieve.
* @returns {string} The property's key.
*/
function getKey(property) {
const key = property.key;
if (property.computed) {
return sourceCode.getText().slice(key.range[0], key.range[1]);
}
return property.key.name || property.key.value;
}
/**
* Reports an appropriately-formatted error if spacing is incorrect on one
* side of the colon.
* @param {ASTNode} property Key-value pair in an object literal.
* @param {string} side Side being verified - either "key" or "value".
* @param {string} whitespace Actual whitespace string.
* @param {int} expected Expected whitespace length.
* @param {string} mode Value of the mode as "strict" or "minimum"
* @returns {void}
*/
function report(property, side, whitespace, expected, mode) {
const diff = whitespace.length - expected,
nextColon = getNextColon(property.key),
tokenBeforeColon = sourceCode.getTokenBefore(nextColon, { includeComments: true }),
tokenAfterColon = sourceCode.getTokenAfter(nextColon, { includeComments: true }),
isKeySide = side === "key",
locStart = isKeySide ? tokenBeforeColon.loc.start : tokenAfterColon.loc.start,
isExtra = diff > 0,
diffAbs = Math.abs(diff),
spaces = Array(diffAbs + 1).join(" ");
let fix;
if ((
diff && mode === "strict" ||
diff < 0 && mode === "minimum" ||
diff > 0 && !expected && mode === "minimum") &&
!(expected && containsLineTerminator(whitespace))
) {
if (isExtra) {
let range;
// Remove whitespace
if (isKeySide) {
range = [tokenBeforeColon.end, tokenBeforeColon.end + diffAbs];
} else {
range = [tokenAfterColon.start - diffAbs, tokenAfterColon.start];
}
fix = function(fixer) {
return fixer.removeRange(range);
};
} else {
// Add whitespace
if (isKeySide) {
fix = function(fixer) {
return fixer.insertTextAfter(tokenBeforeColon, spaces);
};
} else {
fix = function(fixer) {
return fixer.insertTextBefore(tokenAfterColon, spaces);
};
}
}
context.report({
node: property[side],
loc: locStart,
message: messages[side],
data: {
error: isExtra ? "Extra" : "Missing",
computed: property.computed ? "computed " : "",
key: getKey(property)
},
fix
});
}
}
/**
* Gets the number of characters in a key, including quotes around string
* keys and braces around computed property keys.
* @param {ASTNode} property Property of on object literal.
* @returns {int} Width of the key.
*/
function getKeyWidth(property) {
const startToken = sourceCode.getFirstToken(property);
const endToken = getLastTokenBeforeColon(property.key);
return endToken.range[1] - startToken.range[0];
}
/**
* Gets the whitespace around the colon in an object literal property.
* @param {ASTNode} property Property node from an object literal.
* @returns {Object} Whitespace before and after the property's colon.
*/
function getPropertyWhitespace(property) {
const whitespace = /(\s*):(\s*)/.exec(sourceCode.getText().slice(
property.key.range[1], property.value.range[0]
));
if (whitespace) {
return {
beforeColon: whitespace[1],
afterColon: whitespace[2]
};
}
return null;
}
/**
* Creates groups of properties.
* @param {ASTNode} node ObjectExpression node being evaluated.
* @returns {Array.<ASTNode[]>} Groups of property AST node lists.
*/
function createGroups(node) {
if (node.properties.length === 1) {
return [node.properties];
}
return node.properties.reduce((groups, property) => {
const currentGroup = last(groups),
prev = last(currentGroup);
if (!prev || continuesPropertyGroup(prev, property)) {
currentGroup.push(property);
} else {
groups.push([property]);
}
return groups;
}, [
[]
]);
}
/**
* Verifies correct vertical alignment of a group of properties.
* @param {ASTNode[]} properties List of Property AST nodes.
* @returns {void}
*/
function verifyGroupAlignment(properties) {
const length = properties.length,
widths = properties.map(getKeyWidth), // Width of keys, including quotes
align = alignmentOptions.on; // "value" or "colon"
let targetWidth = Math.max.apply(null, widths),
beforeColon, afterColon, mode;
if (alignmentOptions && length > 1) { // When aligning values within a group, use the alignment configuration.
beforeColon = alignmentOptions.beforeColon;
afterColon = alignmentOptions.afterColon;
mode = alignmentOptions.mode;
} else {
beforeColon = multiLineOptions.beforeColon;
afterColon = multiLineOptions.afterColon;
mode = alignmentOptions.mode;
}
// Conditionally include one space before or after colon
targetWidth += (align === "colon" ? beforeColon : afterColon);
for (let i = 0; i < length; i++) {
const property = properties[i];
const whitespace = getPropertyWhitespace(property);
if (whitespace) { // Object literal getters/setters lack a colon
const width = widths[i];
if (align === "value") {
report(property, "key", whitespace.beforeColon, beforeColon, mode);
report(property, "value", whitespace.afterColon, targetWidth - width, mode);
} else { // align = "colon"
report(property, "key", whitespace.beforeColon, targetWidth - width, mode);
report(property, "value", whitespace.afterColon, afterColon, mode);
}
}
}
}
/**
* Verifies vertical alignment, taking into account groups of properties.
* @param {ASTNode} node ObjectExpression node being evaluated.
* @returns {void}
*/
function verifyAlignment(node) {
createGroups(node).forEach(group => {
verifyGroupAlignment(group.filter(isKeyValueProperty));
});
}
/**
* Verifies spacing of property conforms to specified options.
* @param {ASTNode} node Property node being evaluated.
* @param {Object} lineOptions Configured singleLine or multiLine options
* @returns {void}
*/
function verifySpacing(node, lineOptions) {
const actual = getPropertyWhitespace(node);
if (actual) { // Object literal getters/setters lack colons
report(node, "key", actual.beforeColon, lineOptions.beforeColon, lineOptions.mode);
report(node, "value", actual.afterColon, lineOptions.afterColon, lineOptions.mode);
}
}
/**
* Verifies spacing of each property in a list.
* @param {ASTNode[]} properties List of Property AST nodes.
* @returns {void}
*/
function verifyListSpacing(properties) {
const length = properties.length;
for (let i = 0; i < length; i++) {
verifySpacing(properties[i], singleLineOptions);
}
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
if (alignmentOptions) { // Verify vertical alignment
return {
ObjectExpression(node) {
if (isSingleLine(node)) {
verifyListSpacing(node.properties.filter(isKeyValueProperty));
} else {
verifyAlignment(node);
}
}
};
}
// Obey beforeColon and afterColon in each property as configured
return {
Property(node) {
verifySpacing(node, isSingleLine(node.parent) ? singleLineOptions : multiLineOptions);
}
};
}
};

View File

@ -0,0 +1,584 @@
/**
* @fileoverview Rule to enforce spacing before and after keywords.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils"),
keywords = require("../util/keywords");
//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------
const PREV_TOKEN = /^[)\]}>]$/;
const NEXT_TOKEN = /^(?:[([{<~!]|\+\+?|--?)$/;
const PREV_TOKEN_M = /^[)\]}>*]$/;
const NEXT_TOKEN_M = /^[{*]$/;
const TEMPLATE_OPEN_PAREN = /\$\{$/;
const TEMPLATE_CLOSE_PAREN = /^\}/;
const CHECK_TYPE = /^(?:JSXElement|RegularExpression|String|Template)$/;
const KEYS = keywords.concat(["as", "async", "await", "from", "get", "let", "of", "set", "yield"]);
// check duplications.
(function() {
KEYS.sort();
for (let i = 1; i < KEYS.length; ++i) {
if (KEYS[i] === KEYS[i - 1]) {
throw new Error(`Duplication was found in the keyword list: ${KEYS[i]}`);
}
}
}());
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether or not a given token is a "Template" token ends with "${".
*
* @param {Token} token - A token to check.
* @returns {boolean} `true` if the token is a "Template" token ends with "${".
*/
function isOpenParenOfTemplate(token) {
return token.type === "Template" && TEMPLATE_OPEN_PAREN.test(token.value);
}
/**
* Checks whether or not a given token is a "Template" token starts with "}".
*
* @param {Token} token - A token to check.
* @returns {boolean} `true` if the token is a "Template" token starts with "}".
*/
function isCloseParenOfTemplate(token) {
return token.type === "Template" && TEMPLATE_CLOSE_PAREN.test(token.value);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent spacing before and after keywords",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
type: "object",
properties: {
before: { type: "boolean" },
after: { type: "boolean" },
overrides: {
type: "object",
properties: KEYS.reduce((retv, key) => {
retv[key] = {
type: "object",
properties: {
before: { type: "boolean" },
after: { type: "boolean" }
},
additionalProperties: false
};
return retv;
}, {}),
additionalProperties: false
}
},
additionalProperties: false
}
]
},
create(context) {
const sourceCode = context.getSourceCode();
/**
* Reports a given token if there are not space(s) before the token.
*
* @param {Token} token - A token to report.
* @param {RegExp|undefined} pattern - Optional. A pattern of the previous
* token to check.
* @returns {void}
*/
function expectSpaceBefore(token, pattern) {
pattern = pattern || PREV_TOKEN;
const prevToken = sourceCode.getTokenBefore(token);
if (prevToken &&
(CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
!isOpenParenOfTemplate(prevToken) &&
astUtils.isTokenOnSameLine(prevToken, token) &&
!sourceCode.isSpaceBetweenTokens(prevToken, token)
) {
context.report({
loc: token.loc.start,
message: "Expected space(s) before \"{{value}}\".",
data: token,
fix(fixer) {
return fixer.insertTextBefore(token, " ");
}
});
}
}
/**
* Reports a given token if there are space(s) before the token.
*
* @param {Token} token - A token to report.
* @param {RegExp|undefined} pattern - Optional. A pattern of the previous
* token to check.
* @returns {void}
*/
function unexpectSpaceBefore(token, pattern) {
pattern = pattern || PREV_TOKEN;
const prevToken = sourceCode.getTokenBefore(token);
if (prevToken &&
(CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
!isOpenParenOfTemplate(prevToken) &&
astUtils.isTokenOnSameLine(prevToken, token) &&
sourceCode.isSpaceBetweenTokens(prevToken, token)
) {
context.report({
loc: token.loc.start,
message: "Unexpected space(s) before \"{{value}}\".",
data: token,
fix(fixer) {
return fixer.removeRange([prevToken.range[1], token.range[0]]);
}
});
}
}
/**
* Reports a given token if there are not space(s) after the token.
*
* @param {Token} token - A token to report.
* @param {RegExp|undefined} pattern - Optional. A pattern of the next
* token to check.
* @returns {void}
*/
function expectSpaceAfter(token, pattern) {
pattern = pattern || NEXT_TOKEN;
const nextToken = sourceCode.getTokenAfter(token);
if (nextToken &&
(CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
!isCloseParenOfTemplate(nextToken) &&
astUtils.isTokenOnSameLine(token, nextToken) &&
!sourceCode.isSpaceBetweenTokens(token, nextToken)
) {
context.report({
loc: token.loc.start,
message: "Expected space(s) after \"{{value}}\".",
data: token,
fix(fixer) {
return fixer.insertTextAfter(token, " ");
}
});
}
}
/**
* Reports a given token if there are space(s) after the token.
*
* @param {Token} token - A token to report.
* @param {RegExp|undefined} pattern - Optional. A pattern of the next
* token to check.
* @returns {void}
*/
function unexpectSpaceAfter(token, pattern) {
pattern = pattern || NEXT_TOKEN;
const nextToken = sourceCode.getTokenAfter(token);
if (nextToken &&
(CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
!isCloseParenOfTemplate(nextToken) &&
astUtils.isTokenOnSameLine(token, nextToken) &&
sourceCode.isSpaceBetweenTokens(token, nextToken)
) {
context.report({
loc: token.loc.start,
message: "Unexpected space(s) after \"{{value}}\".",
data: token,
fix(fixer) {
return fixer.removeRange([token.range[1], nextToken.range[0]]);
}
});
}
}
/**
* Parses the option object and determines check methods for each keyword.
*
* @param {Object|undefined} options - The option object to parse.
* @returns {Object} - Normalized option object.
* Keys are keywords (there are for every keyword).
* Values are instances of `{"before": function, "after": function}`.
*/
function parseOptions(options) {
const before = !options || options.before !== false;
const after = !options || options.after !== false;
const defaultValue = {
before: before ? expectSpaceBefore : unexpectSpaceBefore,
after: after ? expectSpaceAfter : unexpectSpaceAfter
};
const overrides = (options && options.overrides) || {};
const retv = Object.create(null);
for (let i = 0; i < KEYS.length; ++i) {
const key = KEYS[i];
const override = overrides[key];
if (override) {
const thisBefore = ("before" in override) ? override.before : before;
const thisAfter = ("after" in override) ? override.after : after;
retv[key] = {
before: thisBefore ? expectSpaceBefore : unexpectSpaceBefore,
after: thisAfter ? expectSpaceAfter : unexpectSpaceAfter
};
} else {
retv[key] = defaultValue;
}
}
return retv;
}
const checkMethodMap = parseOptions(context.options[0]);
/**
* Reports a given token if usage of spacing followed by the token is
* invalid.
*
* @param {Token} token - A token to report.
* @param {RegExp|undefined} pattern - Optional. A pattern of the previous
* token to check.
* @returns {void}
*/
function checkSpacingBefore(token, pattern) {
checkMethodMap[token.value].before(token, pattern);
}
/**
* Reports a given token if usage of spacing preceded by the token is
* invalid.
*
* @param {Token} token - A token to report.
* @param {RegExp|undefined} pattern - Optional. A pattern of the next
* token to check.
* @returns {void}
*/
function checkSpacingAfter(token, pattern) {
checkMethodMap[token.value].after(token, pattern);
}
/**
* Reports a given token if usage of spacing around the token is invalid.
*
* @param {Token} token - A token to report.
* @returns {void}
*/
function checkSpacingAround(token) {
checkSpacingBefore(token);
checkSpacingAfter(token);
}
/**
* Reports the first token of a given node if the first token is a keyword
* and usage of spacing around the token is invalid.
*
* @param {ASTNode|null} node - A node to report.
* @returns {void}
*/
function checkSpacingAroundFirstToken(node) {
const firstToken = node && sourceCode.getFirstToken(node);
if (firstToken && firstToken.type === "Keyword") {
checkSpacingAround(firstToken);
}
}
/**
* Reports the first token of a given node if the first token is a keyword
* and usage of spacing followed by the token is invalid.
*
* This is used for unary operators (e.g. `typeof`), `function`, and `super`.
* Other rules are handling usage of spacing preceded by those keywords.
*
* @param {ASTNode|null} node - A node to report.
* @returns {void}
*/
function checkSpacingBeforeFirstToken(node) {
const firstToken = node && sourceCode.getFirstToken(node);
if (firstToken && firstToken.type === "Keyword") {
checkSpacingBefore(firstToken);
}
}
/**
* Reports the previous token of a given node if the token is a keyword and
* usage of spacing around the token is invalid.
*
* @param {ASTNode|null} node - A node to report.
* @returns {void}
*/
function checkSpacingAroundTokenBefore(node) {
if (node) {
const token = sourceCode.getTokenBefore(node, astUtils.isKeywordToken);
checkSpacingAround(token);
}
}
/**
* Reports `async` or `function` keywords of a given node if usage of
* spacing around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForFunction(node) {
const firstToken = node && sourceCode.getFirstToken(node);
if (firstToken &&
((firstToken.type === "Keyword" && firstToken.value === "function") ||
firstToken.value === "async")
) {
checkSpacingBefore(firstToken);
}
}
/**
* Reports `class` and `extends` keywords of a given node if usage of
* spacing around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForClass(node) {
checkSpacingAroundFirstToken(node);
checkSpacingAroundTokenBefore(node.superClass);
}
/**
* Reports `if` and `else` keywords of a given node if usage of spacing
* around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForIfStatement(node) {
checkSpacingAroundFirstToken(node);
checkSpacingAroundTokenBefore(node.alternate);
}
/**
* Reports `try`, `catch`, and `finally` keywords of a given node if usage
* of spacing around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForTryStatement(node) {
checkSpacingAroundFirstToken(node);
checkSpacingAroundFirstToken(node.handler);
checkSpacingAroundTokenBefore(node.finalizer);
}
/**
* Reports `do` and `while` keywords of a given node if usage of spacing
* around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForDoWhileStatement(node) {
checkSpacingAroundFirstToken(node);
checkSpacingAroundTokenBefore(node.test);
}
/**
* Reports `for` and `in` keywords of a given node if usage of spacing
* around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForForInStatement(node) {
checkSpacingAroundFirstToken(node);
checkSpacingAroundTokenBefore(node.right);
}
/**
* Reports `for` and `of` keywords of a given node if usage of spacing
* around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForForOfStatement(node) {
checkSpacingAroundFirstToken(node);
checkSpacingAround(sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken));
}
/**
* Reports `import`, `export`, `as`, and `from` keywords of a given node if
* usage of spacing around those keywords is invalid.
*
* This rule handles the `*` token in module declarations.
*
* import*as A from "./a"; /*error Expected space(s) after "import".
* error Expected space(s) before "as".
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForModuleDeclaration(node) {
const firstToken = sourceCode.getFirstToken(node);
checkSpacingBefore(firstToken, PREV_TOKEN_M);
checkSpacingAfter(firstToken, NEXT_TOKEN_M);
if (node.source) {
const fromToken = sourceCode.getTokenBefore(node.source);
checkSpacingBefore(fromToken, PREV_TOKEN_M);
checkSpacingAfter(fromToken, NEXT_TOKEN_M);
}
}
/**
* Reports `as` keyword of a given node if usage of spacing around this
* keyword is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForImportNamespaceSpecifier(node) {
const asToken = sourceCode.getFirstToken(node, 1);
checkSpacingBefore(asToken, PREV_TOKEN_M);
}
/**
* Reports `static`, `get`, and `set` keywords of a given node if usage of
* spacing around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForProperty(node) {
if (node.static) {
checkSpacingAroundFirstToken(node);
}
if (node.kind === "get" ||
node.kind === "set" ||
(
(node.method || node.type === "MethodDefinition") &&
node.value.async
)
) {
const token = sourceCode.getTokenBefore(
node.key,
tok => {
switch (tok.value) {
case "get":
case "set":
case "async":
return true;
default:
return false;
}
}
);
if (!token) {
throw new Error("Failed to find token get, set, or async beside method name");
}
checkSpacingAround(token);
}
}
/**
* Reports `await` keyword of a given node if usage of spacing before
* this keyword is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForAwaitExpression(node) {
checkSpacingBefore(sourceCode.getFirstToken(node));
}
return {
// Statements
DebuggerStatement: checkSpacingAroundFirstToken,
WithStatement: checkSpacingAroundFirstToken,
// Statements - Control flow
BreakStatement: checkSpacingAroundFirstToken,
ContinueStatement: checkSpacingAroundFirstToken,
ReturnStatement: checkSpacingAroundFirstToken,
ThrowStatement: checkSpacingAroundFirstToken,
TryStatement: checkSpacingForTryStatement,
// Statements - Choice
IfStatement: checkSpacingForIfStatement,
SwitchStatement: checkSpacingAroundFirstToken,
SwitchCase: checkSpacingAroundFirstToken,
// Statements - Loops
DoWhileStatement: checkSpacingForDoWhileStatement,
ForInStatement: checkSpacingForForInStatement,
ForOfStatement: checkSpacingForForOfStatement,
ForStatement: checkSpacingAroundFirstToken,
WhileStatement: checkSpacingAroundFirstToken,
// Statements - Declarations
ClassDeclaration: checkSpacingForClass,
ExportNamedDeclaration: checkSpacingForModuleDeclaration,
ExportDefaultDeclaration: checkSpacingAroundFirstToken,
ExportAllDeclaration: checkSpacingForModuleDeclaration,
FunctionDeclaration: checkSpacingForFunction,
ImportDeclaration: checkSpacingForModuleDeclaration,
VariableDeclaration: checkSpacingAroundFirstToken,
// Expressions
ArrowFunctionExpression: checkSpacingForFunction,
AwaitExpression: checkSpacingForAwaitExpression,
ClassExpression: checkSpacingForClass,
FunctionExpression: checkSpacingForFunction,
NewExpression: checkSpacingBeforeFirstToken,
Super: checkSpacingBeforeFirstToken,
ThisExpression: checkSpacingBeforeFirstToken,
UnaryExpression: checkSpacingBeforeFirstToken,
YieldExpression: checkSpacingBeforeFirstToken,
// Others
ImportNamespaceSpecifier: checkSpacingForImportNamespaceSpecifier,
MethodDefinition: checkSpacingForProperty,
Property: checkSpacingForProperty
};
}
};

View File

@ -0,0 +1,111 @@
/**
* @fileoverview Rule to enforce the position of line comments
* @author Alberto Rodríguez
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce position of line comments",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
oneOf: [
{
enum: ["above", "beside"]
},
{
type: "object",
properties: {
position: {
enum: ["above", "beside"]
},
ignorePattern: {
type: "string"
},
applyDefaultPatterns: {
type: "boolean"
},
applyDefaultIgnorePatterns: {
type: "boolean"
}
},
additionalProperties: false
}
]
}
]
},
create(context) {
const options = context.options[0];
let above,
ignorePattern,
applyDefaultIgnorePatterns = true;
if (!options || typeof options === "string") {
above = !options || options === "above";
} else {
above = options.position === "above";
ignorePattern = options.ignorePattern;
if (options.hasOwnProperty("applyDefaultIgnorePatterns")) {
applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns !== false;
} else {
applyDefaultIgnorePatterns = options.applyDefaultPatterns !== false;
}
}
const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN;
const fallThroughRegExp = /^\s*falls?\s?through/;
const customIgnoreRegExp = new RegExp(ignorePattern);
const sourceCode = context.getSourceCode();
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
LineComment(node) {
if (applyDefaultIgnorePatterns && (defaultIgnoreRegExp.test(node.value) || fallThroughRegExp.test(node.value))) {
return;
}
if (ignorePattern && customIgnoreRegExp.test(node.value)) {
return;
}
const previous = sourceCode.getTokenBefore(node, { includeComments: true });
const isOnSameLine = previous && previous.loc.end.line === node.loc.start.line;
if (above) {
if (isOnSameLine) {
context.report({
node,
message: "Expected comment to be above code."
});
}
} else {
if (!isOnSameLine) {
context.report({
node,
message: "Expected comment to be beside code."
});
}
}
}
};
}
};

View File

@ -0,0 +1,96 @@
/**
* @fileoverview Rule to enforce a single linebreak style.
* @author Erik Mueller
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent linebreak style",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
enum: ["unix", "windows"]
}
]
},
create(context) {
const EXPECTED_LF_MSG = "Expected linebreaks to be 'LF' but found 'CRLF'.",
EXPECTED_CRLF_MSG = "Expected linebreaks to be 'CRLF' but found 'LF'.";
const sourceCode = context.getSourceCode();
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Builds a fix function that replaces text at the specified range in the source text.
* @param {int[]} range The range to replace
* @param {string} text The text to insert.
* @returns {Function} Fixer function
* @private
*/
function createFix(range, text) {
return function(fixer) {
return fixer.replaceTextRange(range, text);
};
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
Program: function checkForlinebreakStyle(node) {
const linebreakStyle = context.options[0] || "unix",
expectedLF = linebreakStyle === "unix",
expectedLFChars = expectedLF ? "\n" : "\r\n",
source = sourceCode.getText(),
pattern = astUtils.createGlobalLinebreakMatcher();
let match;
let i = 0;
while ((match = pattern.exec(source)) !== null) {
i++;
if (match[0] === expectedLFChars) {
continue;
}
const index = match.index;
const range = [index, index + match[0].length];
context.report({
node,
loc: {
line: i,
column: sourceCode.lines[i - 1].length
},
message: expectedLF ? EXPECTED_LF_MSG : EXPECTED_CRLF_MSG,
fix: createFix(range, expectedLFChars)
});
}
}
};
}
};

View File

@ -0,0 +1,380 @@
/**
* @fileoverview Enforces empty lines around comments.
* @author Jamund Ferguson
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash"),
astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Return an array with with any line numbers that are empty.
* @param {Array} lines An array of each line of the file.
* @returns {Array} An array of line numbers.
*/
function getEmptyLineNums(lines) {
const emptyLines = lines.map((line, i) => ({
code: line.trim(),
num: i + 1
})).filter(line => !line.code).map(line => line.num);
return emptyLines;
}
/**
* Return an array with with any line numbers that contain comments.
* @param {Array} comments An array of comment nodes.
* @returns {Array} An array of line numbers.
*/
function getCommentLineNums(comments) {
const lines = [];
comments.forEach(token => {
const start = token.loc.start.line;
const end = token.loc.end.line;
lines.push(start, end);
});
return lines;
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require empty lines around comments",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
type: "object",
properties: {
beforeBlockComment: {
type: "boolean"
},
afterBlockComment: {
type: "boolean"
},
beforeLineComment: {
type: "boolean"
},
afterLineComment: {
type: "boolean"
},
allowBlockStart: {
type: "boolean"
},
allowBlockEnd: {
type: "boolean"
},
allowObjectStart: {
type: "boolean"
},
allowObjectEnd: {
type: "boolean"
},
allowArrayStart: {
type: "boolean"
},
allowArrayEnd: {
type: "boolean"
},
ignorePattern: {
type: "string"
},
applyDefaultIgnorePatterns: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
const options = context.options[0] ? Object.assign({}, context.options[0]) : {};
const ignorePattern = options.ignorePattern;
const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN;
const customIgnoreRegExp = new RegExp(ignorePattern);
const applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns !== false;
options.beforeLineComment = options.beforeLineComment || false;
options.afterLineComment = options.afterLineComment || false;
options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true;
options.afterBlockComment = options.afterBlockComment || false;
options.allowBlockStart = options.allowBlockStart || false;
options.allowBlockEnd = options.allowBlockEnd || false;
const sourceCode = context.getSourceCode();
const lines = sourceCode.lines,
numLines = lines.length + 1,
comments = sourceCode.getAllComments(),
commentLines = getCommentLineNums(comments),
emptyLines = getEmptyLineNums(lines),
commentAndEmptyLines = commentLines.concat(emptyLines);
/**
* Returns whether or not a token is a comment node type
* @param {Token} token The token to check
* @returns {boolean} True if the token is a comment node
*/
function isCommentNodeType(token) {
return token && (token.type === "Block" || token.type === "Line");
}
/**
* Returns whether or not comments are on lines starting with or ending with code
* @param {ASTNode} node The comment node to check.
* @returns {boolean} True if the comment is not alone.
*/
function codeAroundComment(node) {
let token;
token = node;
do {
token = sourceCode.getTokenBefore(token, { includeComments: true });
} while (isCommentNodeType(token));
if (token && astUtils.isTokenOnSameLine(token, node)) {
return true;
}
token = node;
do {
token = sourceCode.getTokenAfter(token, { includeComments: true });
} while (isCommentNodeType(token));
if (token && astUtils.isTokenOnSameLine(node, token)) {
return true;
}
return false;
}
/**
* Returns whether or not comments are inside a node type or not.
* @param {ASTNode} node The Comment node.
* @param {ASTNode} parent The Comment parent node.
* @param {string} nodeType The parent type to check against.
* @returns {boolean} True if the comment is inside nodeType.
*/
function isCommentInsideNodeType(node, parent, nodeType) {
return parent.type === nodeType ||
(parent.body && parent.body.type === nodeType) ||
(parent.consequent && parent.consequent.type === nodeType);
}
/**
* Returns whether or not comments are at the parent start or not.
* @param {ASTNode} node The Comment node.
* @param {string} nodeType The parent type to check against.
* @returns {boolean} True if the comment is at parent start.
*/
function isCommentAtParentStart(node, nodeType) {
const ancestors = context.getAncestors();
let parent;
if (ancestors.length) {
parent = ancestors.pop();
}
return parent && isCommentInsideNodeType(node, parent, nodeType) &&
node.loc.start.line - parent.loc.start.line === 1;
}
/**
* Returns whether or not comments are at the parent end or not.
* @param {ASTNode} node The Comment node.
* @param {string} nodeType The parent type to check against.
* @returns {boolean} True if the comment is at parent end.
*/
function isCommentAtParentEnd(node, nodeType) {
const ancestors = context.getAncestors();
let parent;
if (ancestors.length) {
parent = ancestors.pop();
}
return parent && isCommentInsideNodeType(node, parent, nodeType) &&
parent.loc.end.line - node.loc.end.line === 1;
}
/**
* Returns whether or not comments are at the block start or not.
* @param {ASTNode} node The Comment node.
* @returns {boolean} True if the comment is at block start.
*/
function isCommentAtBlockStart(node) {
return isCommentAtParentStart(node, "ClassBody") || isCommentAtParentStart(node, "BlockStatement") || isCommentAtParentStart(node, "SwitchCase");
}
/**
* Returns whether or not comments are at the block end or not.
* @param {ASTNode} node The Comment node.
* @returns {boolean} True if the comment is at block end.
*/
function isCommentAtBlockEnd(node) {
return isCommentAtParentEnd(node, "ClassBody") || isCommentAtParentEnd(node, "BlockStatement") || isCommentAtParentEnd(node, "SwitchCase") || isCommentAtParentEnd(node, "SwitchStatement");
}
/**
* Returns whether or not comments are at the object start or not.
* @param {ASTNode} node The Comment node.
* @returns {boolean} True if the comment is at object start.
*/
function isCommentAtObjectStart(node) {
return isCommentAtParentStart(node, "ObjectExpression") || isCommentAtParentStart(node, "ObjectPattern");
}
/**
* Returns whether or not comments are at the object end or not.
* @param {ASTNode} node The Comment node.
* @returns {boolean} True if the comment is at object end.
*/
function isCommentAtObjectEnd(node) {
return isCommentAtParentEnd(node, "ObjectExpression") || isCommentAtParentEnd(node, "ObjectPattern");
}
/**
* Returns whether or not comments are at the array start or not.
* @param {ASTNode} node The Comment node.
* @returns {boolean} True if the comment is at array start.
*/
function isCommentAtArrayStart(node) {
return isCommentAtParentStart(node, "ArrayExpression") || isCommentAtParentStart(node, "ArrayPattern");
}
/**
* Returns whether or not comments are at the array end or not.
* @param {ASTNode} node The Comment node.
* @returns {boolean} True if the comment is at array end.
*/
function isCommentAtArrayEnd(node) {
return isCommentAtParentEnd(node, "ArrayExpression") || isCommentAtParentEnd(node, "ArrayPattern");
}
/**
* Checks if a comment node has lines around it (ignores inline comments)
* @param {ASTNode} node The Comment node.
* @param {Object} opts Options to determine the newline.
* @param {boolean} opts.after Should have a newline after this line.
* @param {boolean} opts.before Should have a newline before this line.
* @returns {void}
*/
function checkForEmptyLine(node, opts) {
if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(node.value)) {
return;
}
if (ignorePattern && customIgnoreRegExp.test(node.value)) {
return;
}
let after = opts.after,
before = opts.before;
const prevLineNum = node.loc.start.line - 1,
nextLineNum = node.loc.end.line + 1,
commentIsNotAlone = codeAroundComment(node);
const blockStartAllowed = options.allowBlockStart && isCommentAtBlockStart(node),
blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(node),
objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(node),
objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(node),
arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(node),
arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(node);
const exceptionStartAllowed = blockStartAllowed || objectStartAllowed || arrayStartAllowed;
const exceptionEndAllowed = blockEndAllowed || objectEndAllowed || arrayEndAllowed;
// ignore top of the file and bottom of the file
if (prevLineNum < 1) {
before = false;
}
if (nextLineNum >= numLines) {
after = false;
}
// we ignore all inline comments
if (commentIsNotAlone) {
return;
}
const previousTokenOrComment = sourceCode.getTokenBefore(node, { includeComments: true });
const nextTokenOrComment = sourceCode.getTokenAfter(node, { includeComments: true });
// check for newline before
if (!exceptionStartAllowed && before && !lodash.includes(commentAndEmptyLines, prevLineNum) &&
!(isCommentNodeType(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, node))) {
const lineStart = node.range[0] - node.loc.start.column;
const range = [lineStart, lineStart];
context.report({
node,
message: "Expected line before comment.",
fix(fixer) {
return fixer.insertTextBeforeRange(range, "\n");
}
});
}
// check for newline after
if (!exceptionEndAllowed && after && !lodash.includes(commentAndEmptyLines, nextLineNum) &&
!(isCommentNodeType(nextTokenOrComment) && astUtils.isTokenOnSameLine(node, nextTokenOrComment))) {
context.report({
node,
message: "Expected line after comment.",
fix(fixer) {
return fixer.insertTextAfter(node, "\n");
}
});
}
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
LineComment(node) {
if (options.beforeLineComment || options.afterLineComment) {
checkForEmptyLine(node, {
after: options.afterLineComment,
before: options.beforeLineComment
});
}
},
BlockComment(node) {
if (options.beforeBlockComment || options.afterBlockComment) {
checkForEmptyLine(node, {
after: options.afterBlockComment,
before: options.beforeBlockComment
});
}
}
};
}
};

View File

@ -0,0 +1,191 @@
/**
* @fileoverview Require or disallow newlines around directives.
* @author Kai Cataldo
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require or disallow newlines around directives",
category: "Stylistic Issues",
recommended: false
},
schema: [{
oneOf: [
{
enum: ["always", "never"]
},
{
type: "object",
properties: {
before: {
enum: ["always", "never"]
},
after: {
enum: ["always", "never"]
}
},
additionalProperties: false,
minProperties: 2
}
]
}],
fixable: "whitespace"
},
create(context) {
const sourceCode = context.getSourceCode();
const config = context.options[0] || "always";
const expectLineBefore = typeof config === "string" ? config : config.before;
const expectLineAfter = typeof config === "string" ? config : config.after;
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Check if node is preceded by a blank newline.
* @param {ASTNode} node Node to check.
* @returns {boolean} Whether or not the passed in node is preceded by a blank newline.
*/
function hasNewlineBefore(node) {
const tokenBefore = sourceCode.getTokenBefore(node, { includeComments: true });
const tokenLineBefore = tokenBefore ? tokenBefore.loc.end.line : 0;
return node.loc.start.line - tokenLineBefore >= 2;
}
/**
* Gets the last token of a node that is on the same line as the rest of the node.
* This will usually be the last token of the node, but it will be the second-to-last token if the node has a trailing
* semicolon on a different line.
* @param {ASTNode} node A directive node
* @returns {Token} The last token of the node on the line
*/
function getLastTokenOnLine(node) {
const lastToken = sourceCode.getLastToken(node);
const secondToLastToken = sourceCode.getTokenBefore(lastToken);
return astUtils.isSemicolonToken(lastToken) && lastToken.loc.start.line > secondToLastToken.loc.end.line
? secondToLastToken
: lastToken;
}
/**
* Check if node is followed by a blank newline.
* @param {ASTNode} node Node to check.
* @returns {boolean} Whether or not the passed in node is followed by a blank newline.
*/
function hasNewlineAfter(node) {
const lastToken = getLastTokenOnLine(node);
const tokenAfter = sourceCode.getTokenAfter(lastToken, { includeComments: true });
return tokenAfter.loc.start.line - lastToken.loc.end.line >= 2;
}
/**
* Report errors for newlines around directives.
* @param {ASTNode} node Node to check.
* @param {string} location Whether the error was found before or after the directive.
* @param {boolean} expected Whether or not a newline was expected or unexpected.
* @returns {void}
*/
function reportError(node, location, expected) {
context.report({
node,
message: "{{expected}} newline {{location}} \"{{value}}\" directive.",
data: {
expected: expected ? "Expected" : "Unexpected",
value: node.expression.value,
location
},
fix(fixer) {
const lastToken = getLastTokenOnLine(node);
if (expected) {
return location === "before" ? fixer.insertTextBefore(node, "\n") : fixer.insertTextAfter(lastToken, "\n");
}
return fixer.removeRange(location === "before" ? [node.range[0] - 1, node.range[0]] : [lastToken.range[1], lastToken.range[1] + 1]);
}
});
}
/**
* Check lines around directives in node
* @param {ASTNode} node - node to check
* @returns {void}
*/
function checkDirectives(node) {
const directives = astUtils.getDirectivePrologue(node);
if (!directives.length) {
return;
}
const firstDirective = directives[0];
const hasTokenOrCommentBefore = !!sourceCode.getTokenBefore(firstDirective, { includeComments: true });
// Only check before the first directive if it is preceded by a comment or if it is at the top of
// the file and expectLineBefore is set to "never". This is to not force a newline at the top of
// the file if there are no comments as well as for compatibility with padded-blocks.
if (
firstDirective.leadingComments && firstDirective.leadingComments.length ||
// Shebangs are not added to leading comments but are accounted for by the following.
node.type === "Program" && hasTokenOrCommentBefore
) {
if (expectLineBefore === "always" && !hasNewlineBefore(firstDirective)) {
reportError(firstDirective, "before", true);
}
if (expectLineBefore === "never" && hasNewlineBefore(firstDirective)) {
reportError(firstDirective, "before", false);
}
} else if (
node.type === "Program" &&
expectLineBefore === "never" &&
!hasTokenOrCommentBefore &&
hasNewlineBefore(firstDirective)
) {
reportError(firstDirective, "before", false);
}
const lastDirective = directives[directives.length - 1];
const statements = node.type === "Program" ? node.body : node.body.body;
// Do not check after the last directive if the body only
// contains a directive prologue and isn't followed by a comment to ensure
// this rule behaves well with padded-blocks.
if (lastDirective === statements[statements.length - 1] && !lastDirective.trailingComments) {
return;
}
if (expectLineAfter === "always" && !hasNewlineAfter(lastDirective)) {
reportError(lastDirective, "after", true);
}
if (expectLineAfter === "never" && hasNewlineAfter(lastDirective)) {
reportError(lastDirective, "after", false);
}
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
Program: checkDirectives,
FunctionDeclaration: checkDirectives,
FunctionExpression: checkDirectives,
ArrowFunctionExpression: checkDirectives
};
}
};

View File

@ -0,0 +1,148 @@
/**
* @fileoverview A rule to set the maximum depth block can be nested in a function.
* @author Ian Christian Myers
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce a maximum depth that blocks can be nested",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
oneOf: [
{
type: "integer",
minimum: 0
},
{
type: "object",
properties: {
maximum: {
type: "integer",
minimum: 0
},
max: {
type: "integer",
minimum: 0
}
},
additionalProperties: false
}
]
}
]
},
create(context) {
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
const functionStack = [],
option = context.options[0];
let maxDepth = 4;
if (typeof option === "object" && option.hasOwnProperty("maximum") && typeof option.maximum === "number") {
maxDepth = option.maximum;
}
if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") {
maxDepth = option.max;
}
if (typeof option === "number") {
maxDepth = option;
}
/**
* When parsing a new function, store it in our function stack
* @returns {void}
* @private
*/
function startFunction() {
functionStack.push(0);
}
/**
* When parsing is done then pop out the reference
* @returns {void}
* @private
*/
function endFunction() {
functionStack.pop();
}
/**
* Save the block and Evaluate the node
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function pushBlock(node) {
const len = ++functionStack[functionStack.length - 1];
if (len > maxDepth) {
context.report({ node, message: "Blocks are nested too deeply ({{depth}}).", data: { depth: len } });
}
}
/**
* Pop the saved block
* @returns {void}
* @private
*/
function popBlock() {
functionStack[functionStack.length - 1]--;
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
Program: startFunction,
FunctionDeclaration: startFunction,
FunctionExpression: startFunction,
ArrowFunctionExpression: startFunction,
IfStatement(node) {
if (node.parent.type !== "IfStatement") {
pushBlock(node);
}
},
SwitchStatement: pushBlock,
TryStatement: pushBlock,
DoWhileStatement: pushBlock,
WhileStatement: pushBlock,
WithStatement: pushBlock,
ForStatement: pushBlock,
ForInStatement: pushBlock,
ForOfStatement: pushBlock,
"IfStatement:exit": popBlock,
"SwitchStatement:exit": popBlock,
"TryStatement:exit": popBlock,
"DoWhileStatement:exit": popBlock,
"WhileStatement:exit": popBlock,
"WithStatement:exit": popBlock,
"ForStatement:exit": popBlock,
"ForInStatement:exit": popBlock,
"ForOfStatement:exit": popBlock,
"FunctionDeclaration:exit": endFunction,
"FunctionExpression:exit": endFunction,
"ArrowFunctionExpression:exit": endFunction,
"Program:exit": endFunction
};
}
};

View File

@ -0,0 +1,363 @@
/**
* @fileoverview Rule to check for max length on a line.
* @author Matt DuVall <http://www.mattduvall.com>
*/
"use strict";
//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------
const OPTIONS_SCHEMA = {
type: "object",
properties: {
code: {
type: "integer",
minimum: 0
},
comments: {
type: "integer",
minimum: 0
},
tabWidth: {
type: "integer",
minimum: 0
},
ignorePattern: {
type: "string"
},
ignoreComments: {
type: "boolean"
},
ignoreStrings: {
type: "boolean"
},
ignoreUrls: {
type: "boolean"
},
ignoreTemplateLiterals: {
type: "boolean"
},
ignoreRegExpLiterals: {
type: "boolean"
},
ignoreTrailingComments: {
type: "boolean"
}
},
additionalProperties: false
};
const OPTIONS_OR_INTEGER_SCHEMA = {
anyOf: [
OPTIONS_SCHEMA,
{
type: "integer",
minimum: 0
}
]
};
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce a maximum line length",
category: "Stylistic Issues",
recommended: false
},
schema: [
OPTIONS_OR_INTEGER_SCHEMA,
OPTIONS_OR_INTEGER_SCHEMA,
OPTIONS_SCHEMA
]
},
create(context) {
/*
* Inspired by http://tools.ietf.org/html/rfc3986#appendix-B, however:
* - They're matching an entire string that we know is a URI
* - We're matching part of a string where we think there *might* be a URL
* - We're only concerned about URLs, as picking out any URI would cause
* too many false positives
* - We don't care about matching the entire URL, any small segment is fine
*/
const URL_REGEXP = /[^:/?#]:\/\/[^?#]/;
const sourceCode = context.getSourceCode();
/**
* Computes the length of a line that may contain tabs. The width of each
* tab will be the number of spaces to the next tab stop.
* @param {string} line The line.
* @param {int} tabWidth The width of each tab stop in spaces.
* @returns {int} The computed line length.
* @private
*/
function computeLineLength(line, tabWidth) {
let extraCharacterCount = 0;
line.replace(/\t/g, (match, offset) => {
const totalOffset = offset + extraCharacterCount,
previousTabStopOffset = tabWidth ? totalOffset % tabWidth : 0,
spaceCount = tabWidth - previousTabStopOffset;
extraCharacterCount += spaceCount - 1; // -1 for the replaced tab
});
return Array.from(line).length + extraCharacterCount;
}
// The options object must be the last option specified…
const lastOption = context.options[context.options.length - 1];
const options = typeof lastOption === "object" ? Object.create(lastOption) : {};
// …but max code length…
if (typeof context.options[0] === "number") {
options.code = context.options[0];
}
// …and tabWidth can be optionally specified directly as integers.
if (typeof context.options[1] === "number") {
options.tabWidth = context.options[1];
}
const maxLength = options.code || 80,
tabWidth = options.tabWidth || 4,
ignoreComments = options.ignoreComments || false,
ignoreStrings = options.ignoreStrings || false,
ignoreTemplateLiterals = options.ignoreTemplateLiterals || false,
ignoreRegExpLiterals = options.ignoreRegExpLiterals || false,
ignoreTrailingComments = options.ignoreTrailingComments || options.ignoreComments || false,
ignoreUrls = options.ignoreUrls || false,
maxCommentLength = options.comments;
let ignorePattern = options.ignorePattern || null;
if (ignorePattern) {
ignorePattern = new RegExp(ignorePattern);
}
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Tells if a given comment is trailing: it starts on the current line and
* extends to or past the end of the current line.
* @param {string} line The source line we want to check for a trailing comment on
* @param {number} lineNumber The one-indexed line number for line
* @param {ASTNode} comment The comment to inspect
* @returns {boolean} If the comment is trailing on the given line
*/
function isTrailingComment(line, lineNumber, comment) {
return comment &&
(comment.loc.start.line === lineNumber && lineNumber <= comment.loc.end.line) &&
(comment.loc.end.line > lineNumber || comment.loc.end.column === line.length);
}
/**
* Tells if a comment encompasses the entire line.
* @param {string} line The source line with a trailing comment
* @param {number} lineNumber The one-indexed line number this is on
* @param {ASTNode} comment The comment to remove
* @returns {boolean} If the comment covers the entire line
*/
function isFullLineComment(line, lineNumber, comment) {
const start = comment.loc.start,
end = comment.loc.end,
isFirstTokenOnLine = !line.slice(0, comment.loc.start.column).trim();
return comment &&
(start.line < lineNumber || (start.line === lineNumber && isFirstTokenOnLine)) &&
(end.line > lineNumber || (end.line === lineNumber && end.column === line.length));
}
/**
* Gets the line after the comment and any remaining trailing whitespace is
* stripped.
* @param {string} line The source line with a trailing comment
* @param {number} lineNumber The one-indexed line number this is on
* @param {ASTNode} comment The comment to remove
* @returns {string} Line without comment and trailing whitepace
*/
function stripTrailingComment(line, lineNumber, comment) {
// loc.column is zero-indexed
return line.slice(0, comment.loc.start.column).replace(/\s+$/, "");
}
/**
* Ensure that an array exists at [key] on `object`, and add `value` to it.
*
* @param {Object} object the object to mutate
* @param {string} key the object's key
* @param {*} value the value to add
* @returns {void}
* @private
*/
function ensureArrayAndPush(object, key, value) {
if (!Array.isArray(object[key])) {
object[key] = [];
}
object[key].push(value);
}
/**
* Retrieves an array containing all strings (" or ') in the source code.
*
* @returns {ASTNode[]} An array of string nodes.
*/
function getAllStrings() {
return sourceCode.ast.tokens.filter(token => token.type === "String");
}
/**
* Retrieves an array containing all template literals in the source code.
*
* @returns {ASTNode[]} An array of template literal nodes.
*/
function getAllTemplateLiterals() {
return sourceCode.ast.tokens.filter(token => token.type === "Template");
}
/**
* Retrieves an array containing all RegExp literals in the source code.
*
* @returns {ASTNode[]} An array of RegExp literal nodes.
*/
function getAllRegExpLiterals() {
return sourceCode.ast.tokens.filter(token => token.type === "RegularExpression");
}
/**
* A reducer to group an AST node by line number, both start and end.
*
* @param {Object} acc the accumulator
* @param {ASTNode} node the AST node in question
* @returns {Object} the modified accumulator
* @private
*/
function groupByLineNumber(acc, node) {
for (let i = node.loc.start.line; i <= node.loc.end.line; ++i) {
ensureArrayAndPush(acc, i, node);
}
return acc;
}
/**
* Check the program for max length
* @param {ASTNode} node Node to examine
* @returns {void}
* @private
*/
function checkProgramForMaxLength(node) {
// split (honors line-ending)
const lines = sourceCode.lines,
// list of comments to ignore
comments = ignoreComments || maxCommentLength || ignoreTrailingComments ? sourceCode.getAllComments() : [];
// we iterate over comments in parallel with the lines
let commentsIndex = 0;
const strings = getAllStrings(sourceCode);
const stringsByLine = strings.reduce(groupByLineNumber, {});
const templateLiterals = getAllTemplateLiterals(sourceCode);
const templateLiteralsByLine = templateLiterals.reduce(groupByLineNumber, {});
const regExpLiterals = getAllRegExpLiterals(sourceCode);
const regExpLiteralsByLine = regExpLiterals.reduce(groupByLineNumber, {});
lines.forEach((line, i) => {
// i is zero-indexed, line numbers are one-indexed
const lineNumber = i + 1;
/*
* if we're checking comment length; we need to know whether this
* line is a comment
*/
let lineIsComment = false;
/*
* We can short-circuit the comment checks if we're already out of
* comments to check.
*/
if (commentsIndex < comments.length) {
let comment = null;
// iterate over comments until we find one past the current line
do {
comment = comments[++commentsIndex];
} while (comment && comment.loc.start.line <= lineNumber);
// and step back by one
comment = comments[--commentsIndex];
if (isFullLineComment(line, lineNumber, comment)) {
lineIsComment = true;
} else if (ignoreTrailingComments && isTrailingComment(line, lineNumber, comment)) {
line = stripTrailingComment(line, lineNumber, comment);
}
}
if (ignorePattern && ignorePattern.test(line) ||
ignoreUrls && URL_REGEXP.test(line) ||
ignoreStrings && stringsByLine[lineNumber] ||
ignoreTemplateLiterals && templateLiteralsByLine[lineNumber] ||
ignoreRegExpLiterals && regExpLiteralsByLine[lineNumber]
) {
// ignore this line
return;
}
const lineLength = computeLineLength(line, tabWidth);
if (lineIsComment && ignoreComments) {
return;
}
if (lineIsComment && lineLength > maxCommentLength) {
context.report({
node,
loc: { line: lineNumber, column: 0 },
message: "Line {{lineNumber}} exceeds the maximum comment line length of {{maxCommentLength}}.",
data: {
lineNumber: i + 1,
maxCommentLength
}
});
} else if (lineLength > maxLength) {
context.report({
node,
loc: { line: lineNumber, column: 0 },
message: "Line {{lineNumber}} exceeds the maximum line length of {{maxLength}}.",
data: {
lineNumber: i + 1,
maxLength
}
});
}
});
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
Program: checkProgramForMaxLength
};
}
};

View File

@ -0,0 +1,144 @@
/**
* @fileoverview enforce a maximum file length
* @author Alberto Rodríguez
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce a maximum number of lines per file",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
oneOf: [
{
type: "integer",
minimum: 0
},
{
type: "object",
properties: {
max: {
type: "integer",
minimum: 0
},
skipComments: {
type: "boolean"
},
skipBlankLines: {
type: "boolean"
}
},
additionalProperties: false
}
]
}
]
},
create(context) {
const option = context.options[0];
let max = 300;
if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") {
max = option.max;
}
if (typeof option === "number") {
max = option;
}
const skipComments = option && option.skipComments;
const skipBlankLines = option && option.skipBlankLines;
const sourceCode = context.getSourceCode();
/**
* Returns whether or not a token is a comment node type
* @param {Token} token The token to check
* @returns {boolean} True if the token is a comment node
*/
function isCommentNodeType(token) {
return token && (token.type === "Block" || token.type === "Line");
}
/**
* Returns the line numbers of a comment that don't have any code on the same line
* @param {Node} comment The comment node to check
* @returns {int[]} The line numbers
*/
function getLinesWithoutCode(comment) {
let start = comment.loc.start.line;
let end = comment.loc.end.line;
let token;
token = comment;
do {
token = sourceCode.getTokenBefore(token, { includeComments: true });
} while (isCommentNodeType(token));
if (token && astUtils.isTokenOnSameLine(token, comment)) {
start += 1;
}
token = comment;
do {
token = sourceCode.getTokenAfter(token, { includeComments: true });
} while (isCommentNodeType(token));
if (token && astUtils.isTokenOnSameLine(comment, token)) {
end -= 1;
}
if (start <= end) {
return lodash.range(start, end + 1);
}
return [];
}
return {
"Program:exit"() {
let lines = sourceCode.lines.map((text, i) => ({ lineNumber: i + 1, text }));
if (skipBlankLines) {
lines = lines.filter(l => l.text.trim() !== "");
}
if (skipComments) {
const comments = sourceCode.getAllComments();
const commentLines = lodash.flatten(comments.map(comment => getLinesWithoutCode(comment)));
lines = lines.filter(l => !lodash.includes(commentLines, l.lineNumber));
}
if (lines.length > max) {
context.report({
loc: { line: 1, column: 0 },
message: "File must be at most {{max}} lines long. It's {{actual}} lines long.",
data: {
max,
actual: lines.length
}
});
}
}
};
}
};

View File

@ -0,0 +1,112 @@
/**
* @fileoverview Rule to enforce a maximum number of nested callbacks.
* @author Ian Christian Myers
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce a maximum depth that callbacks can be nested",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
oneOf: [
{
type: "integer",
minimum: 0
},
{
type: "object",
properties: {
maximum: {
type: "integer",
minimum: 0
},
max: {
type: "integer",
minimum: 0
}
},
additionalProperties: false
}
]
}
]
},
create(context) {
//--------------------------------------------------------------------------
// Constants
//--------------------------------------------------------------------------
const option = context.options[0];
let THRESHOLD = 10;
if (typeof option === "object" && option.hasOwnProperty("maximum") && typeof option.maximum === "number") {
THRESHOLD = option.maximum;
}
if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") {
THRESHOLD = option.max;
}
if (typeof option === "number") {
THRESHOLD = option;
}
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
const callbackStack = [];
/**
* Checks a given function node for too many callbacks.
* @param {ASTNode} node The node to check.
* @returns {void}
* @private
*/
function checkFunction(node) {
const parent = node.parent;
if (parent.type === "CallExpression") {
callbackStack.push(node);
}
if (callbackStack.length > THRESHOLD) {
const opts = { num: callbackStack.length, max: THRESHOLD };
context.report({ node, message: "Too many nested callbacks ({{num}}). Maximum allowed is {{max}}.", data: opts });
}
}
/**
* Pops the call stack.
* @returns {void}
* @private
*/
function popStack() {
callbackStack.pop();
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
ArrowFunctionExpression: checkFunction,
"ArrowFunctionExpression:exit": popStack,
FunctionExpression: checkFunction,
"FunctionExpression:exit": popStack
};
}
};

View File

@ -0,0 +1,96 @@
/**
* @fileoverview Rule to flag when a function has too many parameters
* @author Ilya Volodin
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce a maximum number of parameters in function definitions",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
oneOf: [
{
type: "integer",
minimum: 0
},
{
type: "object",
properties: {
maximum: {
type: "integer",
minimum: 0
},
max: {
type: "integer",
minimum: 0
}
},
additionalProperties: false
}
]
}
]
},
create(context) {
const option = context.options[0];
let numParams = 3;
if (typeof option === "object" && option.hasOwnProperty("maximum") && typeof option.maximum === "number") {
numParams = option.maximum;
}
if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") {
numParams = option.max;
}
if (typeof option === "number") {
numParams = option;
}
/**
* Checks a function to see if it has too many parameters.
* @param {ASTNode} node The node to check.
* @returns {void}
* @private
*/
function checkFunction(node) {
if (node.params.length > numParams) {
context.report({
node,
message: "{{name}} has too many parameters ({{count}}). Maximum allowed is {{max}}.",
data: {
name: lodash.upperFirst(astUtils.getFunctionNameWithKind(node)),
count: node.params.length,
max: numParams
}
});
}
}
return {
FunctionDeclaration: checkFunction,
ArrowFunctionExpression: checkFunction,
FunctionExpression: checkFunction
};
}
};

View File

@ -0,0 +1,192 @@
/**
* @fileoverview Specify the maximum number of statements allowed per line.
* @author Kenneth Williams
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce a maximum number of statements allowed per line",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
type: "object",
properties: {
max: {
type: "integer",
minimum: 1
}
},
additionalProperties: false
}
]
},
create(context) {
const sourceCode = context.getSourceCode(),
options = context.options[0] || {},
maxStatementsPerLine = typeof options.max !== "undefined" ? options.max : 1,
message = "This line has {{numberOfStatementsOnThisLine}} {{statements}}. Maximum allowed is {{maxStatementsPerLine}}.";
let lastStatementLine = 0,
numberOfStatementsOnThisLine = 0,
firstExtraStatement;
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
const SINGLE_CHILD_ALLOWED = /^(?:(?:DoWhile|For|ForIn|ForOf|If|Labeled|While)Statement|Export(?:Default|Named)Declaration)$/;
/**
* Reports with the first extra statement, and clears it.
*
* @returns {void}
*/
function reportFirstExtraStatementAndClear() {
if (firstExtraStatement) {
context.report({
node: firstExtraStatement,
message,
data: {
numberOfStatementsOnThisLine,
maxStatementsPerLine,
statements: numberOfStatementsOnThisLine === 1 ? "statement" : "statements"
}
});
}
firstExtraStatement = null;
}
/**
* Gets the actual last token of a given node.
*
* @param {ASTNode} node - A node to get. This is a node except EmptyStatement.
* @returns {Token} The actual last token.
*/
function getActualLastToken(node) {
return sourceCode.getLastToken(node, astUtils.isNotSemicolonToken);
}
/**
* Addresses a given node.
* It updates the state of this rule, then reports the node if the node violated this rule.
*
* @param {ASTNode} node - A node to check.
* @returns {void}
*/
function enterStatement(node) {
const line = node.loc.start.line;
// Skip to allow non-block statements if this is direct child of control statements.
// `if (a) foo();` is counted as 1.
// But `if (a) foo(); else foo();` should be counted as 2.
if (SINGLE_CHILD_ALLOWED.test(node.parent.type) &&
node.parent.alternate !== node
) {
return;
}
// Update state.
if (line === lastStatementLine) {
numberOfStatementsOnThisLine += 1;
} else {
reportFirstExtraStatementAndClear();
numberOfStatementsOnThisLine = 1;
lastStatementLine = line;
}
// Reports if the node violated this rule.
if (numberOfStatementsOnThisLine === maxStatementsPerLine + 1) {
firstExtraStatement = firstExtraStatement || node;
}
}
/**
* Updates the state of this rule with the end line of leaving node to check with the next statement.
*
* @param {ASTNode} node - A node to check.
* @returns {void}
*/
function leaveStatement(node) {
const line = getActualLastToken(node).loc.end.line;
// Update state.
if (line !== lastStatementLine) {
reportFirstExtraStatementAndClear();
numberOfStatementsOnThisLine = 1;
lastStatementLine = line;
}
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
BreakStatement: enterStatement,
ClassDeclaration: enterStatement,
ContinueStatement: enterStatement,
DebuggerStatement: enterStatement,
DoWhileStatement: enterStatement,
ExpressionStatement: enterStatement,
ForInStatement: enterStatement,
ForOfStatement: enterStatement,
ForStatement: enterStatement,
FunctionDeclaration: enterStatement,
IfStatement: enterStatement,
ImportDeclaration: enterStatement,
LabeledStatement: enterStatement,
ReturnStatement: enterStatement,
SwitchStatement: enterStatement,
ThrowStatement: enterStatement,
TryStatement: enterStatement,
VariableDeclaration: enterStatement,
WhileStatement: enterStatement,
WithStatement: enterStatement,
ExportNamedDeclaration: enterStatement,
ExportDefaultDeclaration: enterStatement,
ExportAllDeclaration: enterStatement,
"BreakStatement:exit": leaveStatement,
"ClassDeclaration:exit": leaveStatement,
"ContinueStatement:exit": leaveStatement,
"DebuggerStatement:exit": leaveStatement,
"DoWhileStatement:exit": leaveStatement,
"ExpressionStatement:exit": leaveStatement,
"ForInStatement:exit": leaveStatement,
"ForOfStatement:exit": leaveStatement,
"ForStatement:exit": leaveStatement,
"FunctionDeclaration:exit": leaveStatement,
"IfStatement:exit": leaveStatement,
"ImportDeclaration:exit": leaveStatement,
"LabeledStatement:exit": leaveStatement,
"ReturnStatement:exit": leaveStatement,
"SwitchStatement:exit": leaveStatement,
"ThrowStatement:exit": leaveStatement,
"TryStatement:exit": leaveStatement,
"VariableDeclaration:exit": leaveStatement,
"WhileStatement:exit": leaveStatement,
"WithStatement:exit": leaveStatement,
"ExportNamedDeclaration:exit": leaveStatement,
"ExportDefaultDeclaration:exit": leaveStatement,
"ExportAllDeclaration:exit": leaveStatement,
"Program:exit": reportFirstExtraStatementAndClear
};
}
};

View File

@ -0,0 +1,170 @@
/**
* @fileoverview A rule to set the maximum number of statements in a function.
* @author Ian Christian Myers
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce a maximum number of statements allowed in function blocks",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
oneOf: [
{
type: "integer",
minimum: 0
},
{
type: "object",
properties: {
maximum: {
type: "integer",
minimum: 0
},
max: {
type: "integer",
minimum: 0
}
},
additionalProperties: false
}
]
},
{
type: "object",
properties: {
ignoreTopLevelFunctions: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
const functionStack = [],
option = context.options[0],
ignoreTopLevelFunctions = context.options[1] && context.options[1].ignoreTopLevelFunctions || false,
topLevelFunctions = [];
let maxStatements = 10;
if (typeof option === "object" && option.hasOwnProperty("maximum") && typeof option.maximum === "number") {
maxStatements = option.maximum;
}
if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") {
maxStatements = option.max;
}
if (typeof option === "number") {
maxStatements = option;
}
/**
* Reports a node if it has too many statements
* @param {ASTNode} node node to evaluate
* @param {int} count Number of statements in node
* @param {int} max Maximum number of statements allowed
* @returns {void}
* @private
*/
function reportIfTooManyStatements(node, count, max) {
if (count > max) {
const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(node));
context.report({
node,
message: "{{name}} has too many statements ({{count}}). Maximum allowed is {{max}}.",
data: { name, count, max }
});
}
}
/**
* When parsing a new function, store it in our function stack
* @returns {void}
* @private
*/
function startFunction() {
functionStack.push(0);
}
/**
* Evaluate the node at the end of function
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function endFunction(node) {
const count = functionStack.pop();
if (ignoreTopLevelFunctions && functionStack.length === 0) {
topLevelFunctions.push({ node, count });
} else {
reportIfTooManyStatements(node, count, maxStatements);
}
}
/**
* Increment the count of the functions
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function countStatements(node) {
functionStack[functionStack.length - 1] += node.body.length;
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
FunctionDeclaration: startFunction,
FunctionExpression: startFunction,
ArrowFunctionExpression: startFunction,
BlockStatement: countStatements,
"FunctionDeclaration:exit": endFunction,
"FunctionExpression:exit": endFunction,
"ArrowFunctionExpression:exit": endFunction,
"Program:exit"() {
if (topLevelFunctions.length === 1) {
return;
}
topLevelFunctions.forEach(element => {
const count = element.count;
const node = element.node;
reportIfTooManyStatements(node, count, maxStatements);
});
}
};
}
};

View File

@ -0,0 +1,83 @@
/**
* @fileoverview Enforce newlines between operands of ternary expressions
* @author Kai Cataldo
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce newlines between operands of ternary expressions",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
enum: ["always", "never"]
}
]
},
create(context) {
const multiline = context.options[0] !== "never";
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Tests whether node is preceded by supplied tokens
* @param {ASTNode} node - node to check
* @param {ASTNode} parentNode - parent of node to report
* @param {boolean} expected - whether newline was expected or not
* @returns {void}
* @private
*/
function reportError(node, parentNode, expected) {
context.report({
node,
message: "{{expected}} newline between {{typeOfError}} of ternary expression.",
data: {
expected: expected ? "Expected" : "Unexpected",
typeOfError: node === parentNode.test ? "test and consequent" : "consequent and alternate"
}
});
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
ConditionalExpression(node) {
const areTestAndConsequentOnSameLine = astUtils.isTokenOnSameLine(node.test, node.consequent);
const areConsequentAndAlternateOnSameLine = astUtils.isTokenOnSameLine(node.consequent, node.alternate);
if (!multiline) {
if (!areTestAndConsequentOnSameLine) {
reportError(node.test, node, false);
}
if (!areConsequentAndAlternateOnSameLine) {
reportError(node.consequent, node, false);
}
} else {
if (areTestAndConsequentOnSameLine) {
reportError(node.test, node, true);
}
if (areConsequentAndAlternateOnSameLine) {
reportError(node.consequent, node, true);
}
}
}
};
}
};

View File

@ -0,0 +1,271 @@
/**
* @fileoverview Rule to flag use of constructors without capital letters
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const CAPS_ALLOWED = [
"Array",
"Boolean",
"Date",
"Error",
"Function",
"Number",
"Object",
"RegExp",
"String",
"Symbol"
];
/**
* Ensure that if the key is provided, it must be an array.
* @param {Object} obj Object to check with `key`.
* @param {string} key Object key to check on `obj`.
* @param {*} fallback If obj[key] is not present, this will be returned.
* @returns {string[]} Returns obj[key] if it's an Array, otherwise `fallback`
*/
function checkArray(obj, key, fallback) {
/* istanbul ignore if */
if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) {
throw new TypeError(`${key}, if provided, must be an Array`);
}
return obj[key] || fallback;
}
/**
* A reducer function to invert an array to an Object mapping the string form of the key, to `true`.
* @param {Object} map Accumulator object for the reduce.
* @param {string} key Object key to set to `true`.
* @returns {Object} Returns the updated Object for further reduction.
*/
function invert(map, key) {
map[key] = true;
return map;
}
/**
* Creates an object with the cap is new exceptions as its keys and true as their values.
* @param {Object} config Rule configuration
* @returns {Object} Object with cap is new exceptions.
*/
function calculateCapIsNewExceptions(config) {
let capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED);
if (capIsNewExceptions !== CAPS_ALLOWED) {
capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED);
}
return capIsNewExceptions.reduce(invert, {});
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require constructor names to begin with a capital letter",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
type: "object",
properties: {
newIsCap: {
type: "boolean"
},
capIsNew: {
type: "boolean"
},
newIsCapExceptions: {
type: "array",
items: {
type: "string"
}
},
newIsCapExceptionPattern: {
type: "string"
},
capIsNewExceptions: {
type: "array",
items: {
type: "string"
}
},
capIsNewExceptionPattern: {
type: "string"
},
properties: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
const config = context.options[0] ? Object.assign({}, context.options[0]) : {};
config.newIsCap = config.newIsCap !== false;
config.capIsNew = config.capIsNew !== false;
const skipProperties = config.properties === false;
const newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {});
const newIsCapExceptionPattern = config.newIsCapExceptionPattern ? new RegExp(config.newIsCapExceptionPattern) : null;
const capIsNewExceptions = calculateCapIsNewExceptions(config);
const capIsNewExceptionPattern = config.capIsNewExceptionPattern ? new RegExp(config.capIsNewExceptionPattern) : null;
const listeners = {};
const sourceCode = context.getSourceCode();
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Get exact callee name from expression
* @param {ASTNode} node CallExpression or NewExpression node
* @returns {string} name
*/
function extractNameFromExpression(node) {
let name = "";
if (node.callee.type === "MemberExpression") {
const property = node.callee.property;
if (property.type === "Literal" && (typeof property.value === "string")) {
name = property.value;
} else if (property.type === "Identifier" && !node.callee.computed) {
name = property.name;
}
} else {
name = node.callee.name;
}
return name;
}
/**
* Returns the capitalization state of the string -
* Whether the first character is uppercase, lowercase, or non-alphabetic
* @param {string} str String
* @returns {string} capitalization state: "non-alpha", "lower", or "upper"
*/
function getCap(str) {
const firstChar = str.charAt(0);
const firstCharLower = firstChar.toLowerCase();
const firstCharUpper = firstChar.toUpperCase();
if (firstCharLower === firstCharUpper) {
// char has no uppercase variant, so it's non-alphabetic
return "non-alpha";
} else if (firstChar === firstCharLower) {
return "lower";
}
return "upper";
}
/**
* Check if capitalization is allowed for a CallExpression
* @param {Object} allowedMap Object mapping calleeName to a Boolean
* @param {ASTNode} node CallExpression node
* @param {string} calleeName Capitalized callee name from a CallExpression
* @param {Object} pattern RegExp object from options pattern
* @returns {boolean} Returns true if the callee may be capitalized
*/
function isCapAllowed(allowedMap, node, calleeName, pattern) {
const sourceText = sourceCode.getText(node.callee);
if (allowedMap[calleeName] || allowedMap[sourceText]) {
return true;
}
if (pattern && pattern.test(sourceText)) {
return true;
}
if (calleeName === "UTC" && node.callee.type === "MemberExpression") {
// allow if callee is Date.UTC
return node.callee.object.type === "Identifier" &&
node.callee.object.name === "Date";
}
return skipProperties && node.callee.type === "MemberExpression";
}
/**
* Reports the given message for the given node. The location will be the start of the property or the callee.
* @param {ASTNode} node CallExpression or NewExpression node.
* @param {string} message The message to report.
* @returns {void}
*/
function report(node, message) {
let callee = node.callee;
if (callee.type === "MemberExpression") {
callee = callee.property;
}
context.report({ node, loc: callee.loc.start, message });
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
if (config.newIsCap) {
listeners.NewExpression = function(node) {
const constructorName = extractNameFromExpression(node);
if (constructorName) {
const capitalization = getCap(constructorName);
const isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName, newIsCapExceptionPattern);
if (!isAllowed) {
report(node, "A constructor name should not start with a lowercase letter.");
}
}
};
}
if (config.capIsNew) {
listeners.CallExpression = function(node) {
const calleeName = extractNameFromExpression(node);
if (calleeName) {
const capitalization = getCap(calleeName);
const isAllowed = capitalization !== "upper" || isCapAllowed(capIsNewExceptions, node, calleeName, capIsNewExceptionPattern);
if (!isAllowed) {
report(node, "A function with a name starting with an uppercase letter should only be used as a constructor.");
}
}
};
}
return listeners;
}
};

View File

@ -0,0 +1,58 @@
/**
* @fileoverview Rule to flag when using constructor without parentheses
* @author Ilya Volodin
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require parentheses when invoking a constructor with no arguments",
category: "Stylistic Issues",
recommended: false
},
schema: [],
fixable: "code"
},
create(context) {
const sourceCode = context.getSourceCode();
return {
NewExpression(node) {
if (node.arguments.length !== 0) {
return; // shortcut: if there are arguments, there have to be parens
}
const lastToken = sourceCode.getLastToken(node);
const hasLastParen = lastToken && astUtils.isClosingParenToken(lastToken);
const hasParens = hasLastParen && astUtils.isOpeningParenToken(sourceCode.getTokenBefore(lastToken));
if (!hasParens) {
context.report({
node,
message: "Missing '()' invoking a constructor.",
fix: fixer => fixer.insertTextAfter(node, "()")
});
}
}
};
}
};

View File

@ -0,0 +1,248 @@
/**
* @fileoverview Rule to check empty newline after "var" statement
* @author Gopal Venkatesan
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require or disallow an empty line after variable declarations",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
enum: ["never", "always"]
}
],
fixable: "whitespace"
},
create(context) {
const ALWAYS_MESSAGE = "Expected blank line after variable declarations.",
NEVER_MESSAGE = "Unexpected blank line after variable declarations.";
const sourceCode = context.getSourceCode();
// Default `mode` to "always".
const mode = context.options[0] === "never" ? "never" : "always";
// Cache starting and ending line numbers of comments for faster lookup
const commentEndLine = sourceCode.getAllComments().reduce((result, token) => {
result[token.loc.start.line] = token.loc.end.line;
return result;
}, {});
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Gets a token from the given node to compare line to the next statement.
*
* In general, the token is the last token of the node. However, the token is the second last token if the following conditions satisfy.
*
* - The last token is semicolon.
* - The semicolon is on a different line from the previous token of the semicolon.
*
* This behavior would address semicolon-less style code. e.g.:
*
* var foo = 1
*
* ;(a || b).doSomething()
*
* @param {ASTNode} node - The node to get.
* @returns {Token} The token to compare line to the next statement.
*/
function getLastToken(node) {
const lastToken = sourceCode.getLastToken(node);
if (lastToken.type === "Punctuator" && lastToken.value === ";") {
const prevToken = sourceCode.getTokenBefore(lastToken);
if (prevToken.loc.end.line !== lastToken.loc.start.line) {
return prevToken;
}
}
return lastToken;
}
/**
* Determine if provided keyword is a variable declaration
* @private
* @param {string} keyword - keyword to test
* @returns {boolean} True if `keyword` is a type of var
*/
function isVar(keyword) {
return keyword === "var" || keyword === "let" || keyword === "const";
}
/**
* Determine if provided keyword is a variant of for specifiers
* @private
* @param {string} keyword - keyword to test
* @returns {boolean} True if `keyword` is a variant of for specifier
*/
function isForTypeSpecifier(keyword) {
return keyword === "ForStatement" || keyword === "ForInStatement" || keyword === "ForOfStatement";
}
/**
* Determine if provided keyword is an export specifiers
* @private
* @param {string} nodeType - nodeType to test
* @returns {boolean} True if `nodeType` is an export specifier
*/
function isExportSpecifier(nodeType) {
return nodeType === "ExportNamedDeclaration" || nodeType === "ExportSpecifier" ||
nodeType === "ExportDefaultDeclaration" || nodeType === "ExportAllDeclaration";
}
/**
* Determine if provided node is the last of their parent block.
* @private
* @param {ASTNode} node - node to test
* @returns {boolean} True if `node` is last of their parent block.
*/
function isLastNode(node) {
const token = sourceCode.getTokenAfter(node);
return !token || (token.type === "Punctuator" && token.value === "}");
}
/**
* Gets the last line of a group of consecutive comments
* @param {number} commentStartLine The starting line of the group
* @returns {number} The number of the last comment line of the group
*/
function getLastCommentLineOfBlock(commentStartLine) {
const currentCommentEnd = commentEndLine[commentStartLine];
return commentEndLine[currentCommentEnd + 1] ? getLastCommentLineOfBlock(currentCommentEnd + 1) : currentCommentEnd;
}
/**
* Determine if a token starts more than one line after a comment ends
* @param {token} token The token being checked
* @param {integer} commentStartLine The line number on which the comment starts
* @returns {boolean} True if `token` does not start immediately after a comment
*/
function hasBlankLineAfterComment(token, commentStartLine) {
return token.loc.start.line > getLastCommentLineOfBlock(commentStartLine) + 1;
}
/**
* Checks that a blank line exists after a variable declaration when mode is
* set to "always", or checks that there is no blank line when mode is set
* to "never"
* @private
* @param {ASTNode} node - `VariableDeclaration` node to test
* @returns {void}
*/
function checkForBlankLine(node) {
/*
* lastToken is the last token on the node's line. It will usually also be the last token of the node, but it will
* sometimes be second-last if there is a semicolon on a different line.
*/
const lastToken = getLastToken(node),
/*
* If lastToken is the last token of the node, nextToken should be the token after the node. Otherwise, nextToken
* is the last token of the node.
*/
nextToken = lastToken === sourceCode.getLastToken(node) ? sourceCode.getTokenAfter(node) : sourceCode.getLastToken(node),
nextLineNum = lastToken.loc.end.line + 1;
// Ignore if there is no following statement
if (!nextToken) {
return;
}
// Ignore if parent of node is a for variant
if (isForTypeSpecifier(node.parent.type)) {
return;
}
// Ignore if parent of node is an export specifier
if (isExportSpecifier(node.parent.type)) {
return;
}
// Some coding styles use multiple `var` statements, so do nothing if
// the next token is a `var` statement.
if (nextToken.type === "Keyword" && isVar(nextToken.value)) {
return;
}
// Ignore if it is last statement in a block
if (isLastNode(node)) {
return;
}
// Next statement is not a `var`...
const noNextLineToken = nextToken.loc.start.line > nextLineNum;
const hasNextLineComment = (typeof commentEndLine[nextLineNum] !== "undefined");
if (mode === "never" && noNextLineToken && !hasNextLineComment) {
context.report({
node,
message: NEVER_MESSAGE,
data: { identifier: node.name },
fix(fixer) {
const linesBetween = sourceCode.getText().slice(lastToken.range[1], nextToken.range[0]).split(astUtils.LINEBREAK_MATCHER);
return fixer.replaceTextRange([lastToken.range[1], nextToken.range[0]], `${linesBetween.slice(0, -1).join("")}\n${linesBetween[linesBetween.length - 1]}`);
}
});
}
// Token on the next line, or comment without blank line
if (
mode === "always" && (
!noNextLineToken ||
hasNextLineComment && !hasBlankLineAfterComment(nextToken, nextLineNum)
)
) {
context.report({
node,
message: ALWAYS_MESSAGE,
data: { identifier: node.name },
fix(fixer) {
if ((noNextLineToken ? getLastCommentLineOfBlock(nextLineNum) : lastToken.loc.end.line) === nextToken.loc.start.line) {
return fixer.insertTextBefore(nextToken, "\n\n");
}
return fixer.insertTextBeforeRange([nextToken.range[0] - nextToken.loc.start.column, nextToken.range[1]], "\n");
}
});
}
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
VariableDeclaration: checkForBlankLine
};
}
};

View File

@ -0,0 +1,203 @@
/**
* @fileoverview Rule to require newlines before `return` statement
* @author Kai Cataldo
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require an empty line before `return` statements",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: []
},
create(context) {
const sourceCode = context.getSourceCode();
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Tests whether node is preceded by supplied tokens
* @param {ASTNode} node - node to check
* @param {array} testTokens - array of tokens to test against
* @returns {boolean} Whether or not the node is preceded by one of the supplied tokens
* @private
*/
function isPrecededByTokens(node, testTokens) {
const tokenBefore = sourceCode.getTokenBefore(node);
return testTokens.some(token => tokenBefore.value === token);
}
/**
* Checks whether node is the first node after statement or in block
* @param {ASTNode} node - node to check
* @returns {boolean} Whether or not the node is the first node after statement or in block
* @private
*/
function isFirstNode(node) {
const parentType = node.parent.type;
if (node.parent.body) {
return Array.isArray(node.parent.body)
? node.parent.body[0] === node
: node.parent.body === node;
}
if (parentType === "IfStatement") {
return isPrecededByTokens(node, ["else", ")"]);
} else if (parentType === "DoWhileStatement") {
return isPrecededByTokens(node, ["do"]);
} else if (parentType === "SwitchCase") {
return isPrecededByTokens(node, [":"]);
}
return isPrecededByTokens(node, [")"]);
}
/**
* Returns the number of lines of comments that precede the node
* @param {ASTNode} node - node to check for overlapping comments
* @param {number} lineNumTokenBefore - line number of previous token, to check for overlapping comments
* @returns {number} Number of lines of comments that precede the node
* @private
*/
function calcCommentLines(node, lineNumTokenBefore) {
const comments = sourceCode.getComments(node).leading;
let numLinesComments = 0;
if (!comments.length) {
return numLinesComments;
}
comments.forEach(comment => {
numLinesComments++;
if (comment.type === "Block") {
numLinesComments += comment.loc.end.line - comment.loc.start.line;
}
// avoid counting lines with inline comments twice
if (comment.loc.start.line === lineNumTokenBefore) {
numLinesComments--;
}
if (comment.loc.end.line === node.loc.start.line) {
numLinesComments--;
}
});
return numLinesComments;
}
/**
* Returns the line number of the token before the node that is passed in as an argument
* @param {ASTNode} node - The node to use as the start of the calculation
* @returns {number} Line number of the token before `node`
* @private
*/
function getLineNumberOfTokenBefore(node) {
const tokenBefore = sourceCode.getTokenBefore(node);
let lineNumTokenBefore;
/**
* Global return (at the beginning of a script) is a special case.
* If there is no token before `return`, then we expect no line
* break before the return. Comments are allowed to occupy lines
* before the global return, just no blank lines.
* Setting lineNumTokenBefore to zero in that case results in the
* desired behavior.
*/
if (tokenBefore) {
lineNumTokenBefore = tokenBefore.loc.end.line;
} else {
lineNumTokenBefore = 0; // global return at beginning of script
}
return lineNumTokenBefore;
}
/**
* Checks whether node is preceded by a newline
* @param {ASTNode} node - node to check
* @returns {boolean} Whether or not the node is preceded by a newline
* @private
*/
function hasNewlineBefore(node) {
const lineNumNode = node.loc.start.line;
const lineNumTokenBefore = getLineNumberOfTokenBefore(node);
const commentLines = calcCommentLines(node, lineNumTokenBefore);
return (lineNumNode - lineNumTokenBefore - commentLines) > 1;
}
/**
* Checks whether it is safe to apply a fix to a given return statement.
*
* The fix is not considered safe if the given return statement has leading comments,
* as we cannot safely determine if the newline should be added before or after the comments.
* For more information, see: https://github.com/eslint/eslint/issues/5958#issuecomment-222767211
*
* @param {ASTNode} node - The return statement node to check.
* @returns {boolean} `true` if it can fix the node.
* @private
*/
function canFix(node) {
const leadingComments = sourceCode.getComments(node).leading;
const lastLeadingComment = leadingComments[leadingComments.length - 1];
const tokenBefore = sourceCode.getTokenBefore(node);
if (leadingComments.length === 0) {
return true;
}
// if the last leading comment ends in the same line as the previous token and
// does not share a line with the `return` node, we can consider it safe to fix.
// Example:
// function a() {
// var b; //comment
// return;
// }
if (lastLeadingComment.loc.end.line === tokenBefore.loc.end.line &&
lastLeadingComment.loc.end.line !== node.loc.start.line) {
return true;
}
return false;
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
ReturnStatement(node) {
if (!isFirstNode(node) && !hasNewlineBefore(node)) {
context.report({
node,
message: "Expected newline before return statement.",
fix(fixer) {
if (canFix(node)) {
const tokenBefore = sourceCode.getTokenBefore(node);
const newlines = node.loc.start.line === tokenBefore.loc.end.line ? "\n\n" : "\n";
return fixer.insertTextBefore(node, newlines);
}
return null;
}
});
}
}
};
}
};

View File

@ -0,0 +1,86 @@
/**
* @fileoverview Rule to ensure newline per method call when chaining calls
* @author Rajendra Patil
* @author Burak Yigit Kaya
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require a newline after each call in a method chain",
category: "Stylistic Issues",
recommended: false
},
schema: [{
type: "object",
properties: {
ignoreChainWithDepth: {
type: "integer",
minimum: 1,
maximum: 10
}
},
additionalProperties: false
}]
},
create(context) {
const options = context.options[0] || {},
ignoreChainWithDepth = options.ignoreChainWithDepth || 2;
const sourceCode = context.getSourceCode();
/**
* Gets the property text of a given MemberExpression node.
* If the text is multiline, this returns only the first line.
*
* @param {ASTNode} node - A MemberExpression node to get.
* @returns {string} The property text of the node.
*/
function getPropertyText(node) {
const prefix = node.computed ? "[" : ".";
const lines = sourceCode.getText(node.property).split(astUtils.LINEBREAK_MATCHER);
const suffix = node.computed && lines.length === 1 ? "]" : "";
return prefix + lines[0] + suffix;
}
return {
"CallExpression:exit"(node) {
if (!node.callee || node.callee.type !== "MemberExpression") {
return;
}
const callee = node.callee;
let parent = callee.object;
let depth = 1;
while (parent && parent.callee) {
depth += 1;
parent = parent.callee.object;
}
if (depth > ignoreChainWithDepth && callee.property.loc.start.line === callee.object.loc.end.line) {
context.report({
node: callee.property,
loc: callee.property.loc.start,
message: "Expected line break before `{{callee}}`.",
data: {
callee: getPropertyText(callee)
}
});
}
}
};
}
};

View File

@ -0,0 +1,131 @@
/**
* @fileoverview Rule to flag use of alert, confirm, prompt
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const getPropertyName = require("../ast-utils").getStaticPropertyName;
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks if the given name is a prohibited identifier.
* @param {string} name The name to check
* @returns {boolean} Whether or not the name is prohibited.
*/
function isProhibitedIdentifier(name) {
return /^(alert|confirm|prompt)$/.test(name);
}
/**
* Reports the given node and identifier name.
* @param {RuleContext} context The ESLint rule context.
* @param {ASTNode} node The node to report on.
* @param {string} identifierName The name of the identifier.
* @returns {void}
*/
function report(context, node, identifierName) {
context.report(node, "Unexpected {{name}}.", { name: identifierName });
}
/**
* Finds the escope reference in the given scope.
* @param {Object} scope The scope to search.
* @param {ASTNode} node The identifier node.
* @returns {Reference|null} Returns the found reference or null if none were found.
*/
function findReference(scope, node) {
const references = scope.references.filter(reference => reference.identifier.range[0] === node.range[0] &&
reference.identifier.range[1] === node.range[1]);
if (references.length === 1) {
return references[0];
}
return null;
}
/**
* Checks if the given identifier node is shadowed in the given scope.
* @param {Object} scope The current scope.
* @param {Object} globalScope The global scope.
* @param {string} node The identifier node to check
* @returns {boolean} Whether or not the name is shadowed.
*/
function isShadowed(scope, globalScope, node) {
const reference = findReference(scope, node);
return reference && reference.resolved && reference.resolved.defs.length > 0;
}
/**
* Checks if the given identifier node is a ThisExpression in the global scope or the global window property.
* @param {Object} scope The current scope.
* @param {Object} globalScope The global scope.
* @param {string} node The identifier node to check
* @returns {boolean} Whether or not the node is a reference to the global object.
*/
function isGlobalThisReferenceOrGlobalWindow(scope, globalScope, node) {
if (scope.type === "global" && node.type === "ThisExpression") {
return true;
} else if (node.name === "window") {
return !isShadowed(scope, globalScope, node);
}
return false;
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow the use of `alert`, `confirm`, and `prompt`",
category: "Best Practices",
recommended: false
},
schema: []
},
create(context) {
let globalScope;
return {
Program() {
globalScope = context.getScope();
},
CallExpression(node) {
const callee = node.callee,
currentScope = context.getScope();
// without window.
if (callee.type === "Identifier") {
const identifierName = callee.name;
if (!isShadowed(currentScope, globalScope, callee) && isProhibitedIdentifier(callee.name)) {
report(context, node, identifierName);
}
} else if (callee.type === "MemberExpression" && isGlobalThisReferenceOrGlobalWindow(currentScope, globalScope, callee.object)) {
const identifierName = getPropertyName(callee);
if (isProhibitedIdentifier(identifierName)) {
report(context, node, identifierName);
}
}
}
};
}
};

View File

@ -0,0 +1,47 @@
/**
* @fileoverview Disallow construction of dense arrays using the Array constructor
* @author Matt DuVall <http://www.mattduvall.com/>
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow `Array` constructors",
category: "Stylistic Issues",
recommended: false
},
schema: []
},
create(context) {
/**
* Disallow construction of dense arrays using the Array constructor
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function check(node) {
if (
node.arguments.length !== 1 &&
node.callee.type === "Identifier" &&
node.callee.name === "Array"
) {
context.report({ node, message: "The array literal notation [] is preferrable." });
}
}
return {
CallExpression: check,
NewExpression: check
};
}
};

View File

@ -0,0 +1,75 @@
/**
* @fileoverview Rule to disallow uses of await inside of loops.
* @author Nat Mote (nmote)
*/
"use strict";
// Node types which are considered loops.
const loopTypes = new Set([
"ForStatement",
"ForOfStatement",
"ForInStatement",
"WhileStatement",
"DoWhileStatement"
]);
// Node types at which we should stop looking for loops. For example, it is fine to declare an async
// function within a loop, and use await inside of that.
const boundaryTypes = new Set([
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression"
]);
module.exports = {
meta: {
docs: {
description: "disallow `await` inside of loops",
category: "Possible Errors",
recommended: false
},
schema: []
},
create(context) {
return {
AwaitExpression(node) {
const ancestors = context.getAncestors();
// Reverse so that we can traverse from the deepest node upwards.
ancestors.reverse();
// Create a set of all the ancestors plus this node so that we can check
// if this use of await appears in the body of the loop as opposed to
// the right-hand side of a for...of, for example.
const ancestorSet = new Set(ancestors).add(node);
for (let i = 0; i < ancestors.length; i++) {
const ancestor = ancestors[i];
if (boundaryTypes.has(ancestor.type)) {
// Short-circuit out if we encounter a boundary type. Loops above
// this do not matter.
return;
}
if (loopTypes.has(ancestor.type)) {
// Only report if we are actually in the body or another part that gets executed on
// every iteration.
if (
ancestorSet.has(ancestor.body) ||
ancestorSet.has(ancestor.test) ||
ancestorSet.has(ancestor.update)
) {
context.report({
node,
message: "Unexpected `await` inside a loop."
});
return;
}
}
}
}
};
}
};

View File

@ -0,0 +1,109 @@
/**
* @fileoverview Rule to flag bitwise identifiers
* @author Nicholas C. Zakas
*/
"use strict";
//
// Set of bitwise operators.
//
const BITWISE_OPERATORS = [
"^", "|", "&", "<<", ">>", ">>>",
"^=", "|=", "&=", "<<=", ">>=", ">>>=",
"~"
];
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow bitwise operators",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
type: "object",
properties: {
allow: {
type: "array",
items: {
enum: BITWISE_OPERATORS
},
uniqueItems: true
},
int32Hint: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
const options = context.options[0] || {};
const allowed = options.allow || [];
const int32Hint = options.int32Hint === true;
/**
* Reports an unexpected use of a bitwise operator.
* @param {ASTNode} node Node which contains the bitwise operator.
* @returns {void}
*/
function report(node) {
context.report({ node, message: "Unexpected use of '{{operator}}'.", data: { operator: node.operator } });
}
/**
* Checks if the given node has a bitwise operator.
* @param {ASTNode} node The node to check.
* @returns {boolean} Whether or not the node has a bitwise operator.
*/
function hasBitwiseOperator(node) {
return BITWISE_OPERATORS.indexOf(node.operator) !== -1;
}
/**
* Checks if exceptions were provided, e.g. `{ allow: ['~', '|'] }`.
* @param {ASTNode} node The node to check.
* @returns {boolean} Whether or not the node has a bitwise operator.
*/
function allowedOperator(node) {
return allowed.indexOf(node.operator) !== -1;
}
/**
* Checks if the given bitwise operator is used for integer typecasting, i.e. "|0"
* @param {ASTNode} node The node to check.
* @returns {boolean} whether the node is used in integer typecasting.
*/
function isInt32Hint(node) {
return int32Hint && node.operator === "|" && node.right &&
node.right.type === "Literal" && node.right.value === 0;
}
/**
* Report if the given node contains a bitwise operator.
* @param {ASTNode} node The node to check.
* @returns {void}
*/
function checkNodeForBitwiseOperator(node) {
if (hasBitwiseOperator(node) && !allowedOperator(node) && !isInt32Hint(node)) {
report(node);
}
}
return {
AssignmentExpression: checkNodeForBitwiseOperator,
BinaryExpression: checkNodeForBitwiseOperator,
UnaryExpression: checkNodeForBitwiseOperator
};
}
};

View File

@ -0,0 +1,39 @@
/**
* @fileoverview Rule to flag use of arguments.callee and arguments.caller.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow the use of `arguments.caller` or `arguments.callee`",
category: "Best Practices",
recommended: false
},
schema: []
},
create(context) {
return {
MemberExpression(node) {
const objectName = node.object.name,
propertyName = node.property.name;
if (objectName === "arguments" && !node.computed && propertyName && propertyName.match(/^calle[er]$/)) {
context.report({ node, message: "Avoid arguments.{{property}}.", data: { property: propertyName } });
}
}
};
}
};

View File

@ -0,0 +1,57 @@
/**
* @fileoverview Rule to flag use of an lexical declarations inside a case clause
* @author Erik Arvidsson
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow lexical declarations in case clauses",
category: "Best Practices",
recommended: true
},
schema: []
},
create(context) {
/**
* Checks whether or not a node is a lexical declaration.
* @param {ASTNode} node A direct child statement of a switch case.
* @returns {boolean} Whether or not the node is a lexical declaration.
*/
function isLexicalDeclaration(node) {
switch (node.type) {
case "FunctionDeclaration":
case "ClassDeclaration":
return true;
case "VariableDeclaration":
return node.kind !== "var";
default:
return false;
}
}
return {
SwitchCase(node) {
for (let i = 0; i < node.consequent.length; i++) {
const statement = node.consequent[i];
if (isLexicalDeclaration(statement)) {
context.report({
node,
message: "Unexpected lexical declaration in case block."
});
}
}
}
};
}
};

View File

@ -0,0 +1,67 @@
/**
* @fileoverview Rule to flag variable leak in CatchClauses in IE 8 and earlier
* @author Ian Christian Myers
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow `catch` clause parameters from shadowing variables in the outer scope",
category: "Variables",
recommended: false
},
schema: []
},
create(context) {
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Check if the parameters are been shadowed
* @param {Object} scope current scope
* @param {string} name parameter name
* @returns {boolean} True is its been shadowed
*/
function paramIsShadowing(scope, name) {
return astUtils.getVariableByName(scope, name) !== null;
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
CatchClause(node) {
let scope = context.getScope();
// When blockBindings is enabled, CatchClause creates its own scope
// so start from one upper scope to exclude the current node
if (scope.block === node) {
scope = scope.upper;
}
if (paramIsShadowing(scope, node.param.name)) {
context.report({ node, message: "Value of '{{name}}' may be overwritten in IE 8 and earlier.", data: { name: node.param.name } });
}
}
};
}
};

View File

@ -0,0 +1,54 @@
/**
* @fileoverview A rule to disallow modifying variables of class declarations
* @author Toru Nagashima
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow reassigning class members",
category: "ECMAScript 6",
recommended: true
},
schema: []
},
create(context) {
/**
* Finds and reports references that are non initializer and writable.
* @param {Variable} variable - A variable to check.
* @returns {void}
*/
function checkVariable(variable) {
astUtils.getModifyingReferences(variable.references).forEach(reference => {
context.report({ node: reference.identifier, message: "'{{name}}' is a class.", data: { name: reference.identifier.name } });
});
}
/**
* Finds and reports references that are non initializer and writable.
* @param {ASTNode} node - A ClassDeclaration/ClassExpression node to check.
* @returns {void}
*/
function checkForClass(node) {
context.getDeclaredVariables(node).forEach(checkVariable);
}
return {
ClassDeclaration: checkForClass,
ClassExpression: checkForClass
};
}
};

View File

@ -0,0 +1,53 @@
/**
* @fileoverview The rule should warn against code that tries to compare against -0.
* @author Aladdin-ADD <hh_2013@foxmail.com>
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow comparing against -0",
category: "Possible Errors",
recommended: false
},
fixable: null,
schema: []
},
create(context) {
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Checks a given node is -0
*
* @param {ASTNode} node - A node to check.
* @returns {boolean} `true` if the node is -0.
*/
function isNegZero(node) {
return node.type === "UnaryExpression" && node.operator === "-" && node.argument.type === "Literal" && node.argument.value === 0;
}
const OPERATORS_TO_CHECK = new Set([">", ">=", "<", "<=", "==", "===", "!=", "!=="]);
return {
BinaryExpression(node) {
if (OPERATORS_TO_CHECK.has(node.operator)) {
if (isNegZero(node.left) || isNegZero(node.right)) {
context.report({
node,
message: "Do not use the '{{operator}}' operator to compare against -0.",
data: { operator: node.operator }
});
}
}
}
};
}
};

View File

@ -0,0 +1,135 @@
/**
* @fileoverview Rule to flag assignment in a conditional statement's test expression
* @author Stephen Murray <spmurrayzzz>
*/
"use strict";
const astUtils = require("../ast-utils");
const NODE_DESCRIPTIONS = {
DoWhileStatement: "a 'do...while' statement",
ForStatement: "a 'for' statement",
IfStatement: "an 'if' statement",
WhileStatement: "a 'while' statement"
};
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow assignment operators in conditional expressions",
category: "Possible Errors",
recommended: true
},
schema: [
{
enum: ["except-parens", "always"]
}
]
},
create(context) {
const prohibitAssign = (context.options[0] || "except-parens");
const sourceCode = context.getSourceCode();
/**
* Check whether an AST node is the test expression for a conditional statement.
* @param {!Object} node The node to test.
* @returns {boolean} `true` if the node is the text expression for a conditional statement; otherwise, `false`.
*/
function isConditionalTestExpression(node) {
return node.parent &&
node.parent.test &&
node === node.parent.test;
}
/**
* Given an AST node, perform a bottom-up search for the first ancestor that represents a conditional statement.
* @param {!Object} node The node to use at the start of the search.
* @returns {?Object} The closest ancestor node that represents a conditional statement.
*/
function findConditionalAncestor(node) {
let currentAncestor = node;
do {
if (isConditionalTestExpression(currentAncestor)) {
return currentAncestor.parent;
}
} while ((currentAncestor = currentAncestor.parent) && !astUtils.isFunction(currentAncestor));
return null;
}
/**
* Check whether the code represented by an AST node is enclosed in two sets of parentheses.
* @param {!Object} node The node to test.
* @returns {boolean} `true` if the code is enclosed in two sets of parentheses; otherwise, `false`.
*/
function isParenthesisedTwice(node) {
const previousToken = sourceCode.getTokenBefore(node, 1),
nextToken = sourceCode.getTokenAfter(node, 1);
return astUtils.isParenthesised(sourceCode, node) &&
astUtils.isOpeningParenToken(previousToken) && previousToken.range[1] <= node.range[0] &&
astUtils.isClosingParenToken(nextToken) && nextToken.range[0] >= node.range[1];
}
/**
* Check a conditional statement's test expression for top-level assignments that are not enclosed in parentheses.
* @param {!Object} node The node for the conditional statement.
* @returns {void}
*/
function testForAssign(node) {
if (node.test &&
(node.test.type === "AssignmentExpression") &&
(node.type === "ForStatement"
? !astUtils.isParenthesised(sourceCode, node.test)
: !isParenthesisedTwice(node.test)
)
) {
// must match JSHint's error message
context.report({
node,
loc: node.test.loc.start,
message: "Expected a conditional expression and instead saw an assignment."
});
}
}
/**
* Check whether an assignment expression is descended from a conditional statement's test expression.
* @param {!Object} node The node for the assignment expression.
* @returns {void}
*/
function testForConditionalAncestor(node) {
const ancestor = findConditionalAncestor(node);
if (ancestor) {
context.report({ node: ancestor, message: "Unexpected assignment within {{type}}.", data: {
type: NODE_DESCRIPTIONS[ancestor.type] || ancestor.type
} });
}
}
if (prohibitAssign === "always") {
return {
AssignmentExpression: testForConditionalAncestor
};
}
return {
DoWhileStatement: testForAssign,
ForStatement: testForAssign,
IfStatement: testForAssign,
WhileStatement: testForAssign
};
}
};

View File

@ -0,0 +1,66 @@
/**
* @fileoverview A rule to warn against using arrow functions when they could be
* confused with comparisions
* @author Jxck <https://github.com/Jxck>
*/
"use strict";
const astUtils = require("../ast-utils.js");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether or not a node is a conditional expression.
* @param {ASTNode} node - node to test
* @returns {boolean} `true` if the node is a conditional expression.
*/
function isConditional(node) {
return node && node.type === "ConditionalExpression";
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow arrow functions where they could be confused with comparisons",
category: "ECMAScript 6",
recommended: false
},
schema: [{
type: "object",
properties: {
allowParens: { type: "boolean" }
},
additionalProperties: false
}]
},
create(context) {
const config = context.options[0] || {};
const sourceCode = context.getSourceCode();
/**
* Reports if an arrow function contains an ambiguous conditional.
* @param {ASTNode} node - A node to check and report.
* @returns {void}
*/
function checkArrowFunc(node) {
const body = node.body;
if (isConditional(body) && !(config.allowParens && astUtils.isParenthesised(sourceCode, body))) {
context.report({ node, message: "Arrow function used ambiguously with a conditional expression." });
}
}
return {
ArrowFunctionExpression: checkArrowFunc
};
}
};

View File

@ -0,0 +1,130 @@
/**
* @fileoverview Rule to flag use of console object
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow the use of `console`",
category: "Possible Errors",
recommended: true
},
schema: [
{
type: "object",
properties: {
allow: {
type: "array",
items: {
type: "string"
},
minItems: 1,
uniqueItems: true
}
},
additionalProperties: false
}
]
},
create(context) {
const options = context.options[0] || {};
const allowed = options.allow || [];
/**
* Checks whether the given reference is 'console' or not.
*
* @param {escope.Reference} reference - The reference to check.
* @returns {boolean} `true` if the reference is 'console'.
*/
function isConsole(reference) {
const id = reference.identifier;
return id && id.name === "console";
}
/**
* Checks whether the property name of the given MemberExpression node
* is allowed by options or not.
*
* @param {ASTNode} node - The MemberExpression node to check.
* @returns {boolean} `true` if the property name of the node is allowed.
*/
function isAllowed(node) {
const propertyName = astUtils.getStaticPropertyName(node);
return propertyName && allowed.indexOf(propertyName) !== -1;
}
/**
* Checks whether the given reference is a member access which is not
* allowed by options or not.
*
* @param {escope.Reference} reference - The reference to check.
* @returns {boolean} `true` if the reference is a member access which
* is not allowed by options.
*/
function isMemberAccessExceptAllowed(reference) {
const node = reference.identifier;
const parent = node.parent;
return (
parent.type === "MemberExpression" &&
parent.object === node &&
!isAllowed(parent)
);
}
/**
* Reports the given reference as a violation.
*
* @param {escope.Reference} reference - The reference to report.
* @returns {void}
*/
function report(reference) {
const node = reference.identifier.parent;
context.report({
node,
loc: node.loc,
message: "Unexpected console statement."
});
}
return {
"Program:exit"() {
const scope = context.getScope();
const consoleVar = astUtils.getVariableByName(scope, "console");
const shadowed = consoleVar && consoleVar.defs.length > 0;
/* 'scope.through' includes all references to undefined
* variables. If the variable 'console' is not defined, it uses
* 'scope.through'.
*/
const references = consoleVar
? consoleVar.references
: scope.through.filter(isConsole);
if (!shadowed) {
references
.filter(isMemberAccessExceptAllowed)
.forEach(report);
}
}
};
}
};

View File

@ -0,0 +1,47 @@
/**
* @fileoverview A rule to disallow modifying variables that are declared using `const`
* @author Toru Nagashima
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow reassigning `const` variables",
category: "ECMAScript 6",
recommended: true
},
schema: []
},
create(context) {
/**
* Finds and reports references that are non initializer and writable.
* @param {Variable} variable - A variable to check.
* @returns {void}
*/
function checkVariable(variable) {
astUtils.getModifyingReferences(variable.references).forEach(reference => {
context.report({ node: reference.identifier, message: "'{{name}}' is constant.", data: { name: reference.identifier.name } });
});
}
return {
VariableDeclaration(node) {
if (node.kind === "const") {
context.getDeclaredVariables(node).forEach(checkVariable);
}
}
};
}
};

View File

@ -0,0 +1,154 @@
/**
* @fileoverview Rule to flag use constant conditions
* @author Christian Schulz <http://rndm.de>
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow constant expressions in conditions",
category: "Possible Errors",
recommended: true
},
schema: [
{
type: "object",
properties: {
checkLoops: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
const options = context.options[0] || {},
checkLoops = options.checkLoops !== false;
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Checks if a branch node of LogicalExpression short circuits the whole condition
* @param {ASTNode} node The branch of main condition which needs to be checked
* @param {string} operator The operator of the main LogicalExpression.
* @returns {boolean} true when condition short circuits whole condition
*/
function isLogicalIdentity(node, operator) {
switch (node.type) {
case "Literal":
return (operator === "||" && node.value === true) ||
(operator === "&&" && node.value === false);
case "UnaryExpression":
return (operator === "&&" && node.operator === "void");
case "LogicalExpression":
return isLogicalIdentity(node.left, node.operator) ||
isLogicalIdentity(node.right, node.operator);
// no default
}
return false;
}
/**
* Checks if a node has a constant truthiness value.
* @param {ASTNode} node The AST node to check.
* @param {boolean} inBooleanPosition `false` if checking branch of a condition.
* `true` in all other cases
* @returns {Bool} true when node's truthiness is constant
* @private
*/
function isConstant(node, inBooleanPosition) {
switch (node.type) {
case "Literal":
case "ArrowFunctionExpression":
case "FunctionExpression":
case "ObjectExpression":
case "ArrayExpression":
return true;
case "UnaryExpression":
if (node.operator === "void") {
return true;
}
return (node.operator === "typeof" && inBooleanPosition) ||
isConstant(node.argument, true);
case "BinaryExpression":
return isConstant(node.left, false) &&
isConstant(node.right, false) &&
node.operator !== "in";
case "LogicalExpression": {
const isLeftConstant = isConstant(node.left, inBooleanPosition);
const isRightConstant = isConstant(node.right, inBooleanPosition);
const isLeftShortCircuit = (isLeftConstant && isLogicalIdentity(node.left, node.operator));
const isRightShortCircuit = (isRightConstant && isLogicalIdentity(node.right, node.operator));
return (isLeftConstant && isRightConstant) || isLeftShortCircuit || isRightShortCircuit;
}
case "AssignmentExpression":
return (node.operator === "=") && isConstant(node.right, inBooleanPosition);
case "SequenceExpression":
return isConstant(node.expressions[node.expressions.length - 1], inBooleanPosition);
// no default
}
return false;
}
/**
* Reports when the given node contains a constant condition.
* @param {ASTNode} node The AST node to check.
* @returns {void}
* @private
*/
function checkConstantCondition(node) {
if (node.test && isConstant(node.test, true)) {
context.report({ node, message: "Unexpected constant condition." });
}
}
/**
* Checks node when checkLoops option is enabled
* @param {ASTNode} node The AST node to check.
* @returns {void}
* @private
*/
function checkLoop(node) {
if (checkLoops) {
checkConstantCondition(node);
}
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
ConditionalExpression: checkConstantCondition,
IfStatement: checkConstantCondition,
WhileStatement: checkLoop,
DoWhileStatement: checkLoop,
ForStatement: checkLoop
};
}
};

View File

@ -0,0 +1,32 @@
/**
* @fileoverview Rule to flag use of continue statement
* @author Borislav Zhivkov
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow `continue` statements",
category: "Stylistic Issues",
recommended: false
},
schema: []
},
create(context) {
return {
ContinueStatement(node) {
context.report({ node, message: "Unexpected use of continue statement." });
}
};
}
};

View File

@ -0,0 +1,126 @@
/**
* @fileoverview Rule to forbid control charactes from regular expressions.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow control characters in regular expressions",
category: "Possible Errors",
recommended: true
},
schema: []
},
create(context) {
/**
* Get the regex expression
* @param {ASTNode} node node to evaluate
* @returns {*} Regex if found else null
* @private
*/
function getRegExp(node) {
if (node.value instanceof RegExp) {
return node.value;
} else if (typeof node.value === "string") {
const parent = context.getAncestors().pop();
if ((parent.type === "NewExpression" || parent.type === "CallExpression") &&
parent.callee.type === "Identifier" && parent.callee.name === "RegExp"
) {
// there could be an invalid regular expression string
try {
return new RegExp(node.value);
} catch (ex) {
return null;
}
}
}
return null;
}
const controlChar = /[\x00-\x1f]/g; // eslint-disable-line no-control-regex
const consecutiveSlashes = /\\+/g;
const consecutiveSlashesAtEnd = /\\+$/g;
const stringControlChar = /\\x[01][0-9a-f]/ig;
const stringControlCharWithoutSlash = /x[01][0-9a-f]/ig;
/**
* Return a list of the control characters in the given regex string
* @param {string} regexStr regex as string to check
* @returns {array} returns a list of found control characters on given string
* @private
*/
function getControlCharacters(regexStr) {
// check control characters, if RegExp object used
const controlChars = regexStr.match(controlChar) || [];
let stringControlChars = [];
// check substr, if regex literal used
const subStrIndex = regexStr.search(stringControlChar);
if (subStrIndex > -1) {
// is it escaped, check backslash count
const possibleEscapeCharacters = regexStr.slice(0, subStrIndex).match(consecutiveSlashesAtEnd);
const hasControlChars = possibleEscapeCharacters === null || !(possibleEscapeCharacters[0].length % 2);
if (hasControlChars) {
stringControlChars = regexStr.slice(subStrIndex, -1)
.split(consecutiveSlashes)
.filter(Boolean)
.map(x => {
const match = x.match(stringControlCharWithoutSlash) || [x];
return `\\${match[0]}`;
});
}
}
return controlChars.map(x => {
const hexCode = `0${x.charCodeAt(0).toString(16)}`.slice(-2);
return `\\x${hexCode}`;
}).concat(stringControlChars);
}
return {
Literal(node) {
const regex = getRegExp(node);
if (regex) {
const computedValue = regex.toString();
const controlCharacters = getControlCharacters(computedValue);
if (controlCharacters.length > 0) {
context.report({
node,
message: "Unexpected control character(s) in regular expression: {{controlChars}}.",
data: {
controlChars: controlCharacters.join(", ")
}
});
}
}
}
};
}
};

View File

@ -0,0 +1,32 @@
/**
* @fileoverview Rule to flag use of a debugger statement
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow the use of `debugger`",
category: "Possible Errors",
recommended: true
},
schema: []
},
create(context) {
return {
DebuggerStatement(node) {
context.report({ node, message: "Unexpected 'debugger' statement." });
}
};
}
};

View File

@ -0,0 +1,35 @@
/**
* @fileoverview Rule to flag when deleting variables
* @author Ilya Volodin
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow deleting variables",
category: "Variables",
recommended: true
},
schema: []
},
create(context) {
return {
UnaryExpression(node) {
if (node.operator === "delete" && node.argument.type === "Identifier") {
context.report({ node, message: "Variables should not be deleted." });
}
}
};
}
};

View File

@ -0,0 +1,38 @@
/**
* @fileoverview Rule to check for ambiguous div operator in regexes
* @author Matt DuVall <http://www.mattduvall.com>
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow division operators explicitly at the beginning of regular expressions",
category: "Best Practices",
recommended: false
},
schema: []
},
create(context) {
const sourceCode = context.getSourceCode();
return {
Literal(node) {
const token = sourceCode.getFirstToken(node);
if (token.type === "RegularExpression" && token.value[1] === "=") {
context.report({ node, message: "A regular expression literal can be confused with '/='." });
}
}
};
}
};

View File

@ -0,0 +1,73 @@
/**
* @fileoverview Rule to flag duplicate arguments
* @author Jamund Ferguson
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow duplicate arguments in `function` definitions",
category: "Possible Errors",
recommended: true
},
schema: []
},
create(context) {
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Checks whether or not a given definition is a parameter's.
* @param {escope.DefEntry} def - A definition to check.
* @returns {boolean} `true` if the definition is a parameter's.
*/
function isParameter(def) {
return def.type === "Parameter";
}
/**
* Determines if a given node has duplicate parameters.
* @param {ASTNode} node The node to check.
* @returns {void}
* @private
*/
function checkParams(node) {
const variables = context.getDeclaredVariables(node);
for (let i = 0; i < variables.length; ++i) {
const variable = variables[i];
// Checks and reports duplications.
const defs = variable.defs.filter(isParameter);
if (defs.length >= 2) {
context.report({
node,
message: "Duplicate param '{{name}}'.",
data: { name: variable.name }
});
}
}
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
FunctionDeclaration: checkParams,
FunctionExpression: checkParams
};
}
};

View File

@ -0,0 +1,109 @@
/**
* @fileoverview A rule to disallow duplicate name in class members.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow duplicate class members",
category: "ECMAScript 6",
recommended: true
},
schema: []
},
create(context) {
let stack = [];
/**
* Gets state of a given member name.
* @param {string} name - A name of a member.
* @param {boolean} isStatic - A flag which specifies that is a static member.
* @returns {Object} A state of a given member name.
* - retv.init {boolean} A flag which shows the name is declared as normal member.
* - retv.get {boolean} A flag which shows the name is declared as getter.
* - retv.set {boolean} A flag which shows the name is declared as setter.
*/
function getState(name, isStatic) {
const stateMap = stack[stack.length - 1];
const key = `$${name}`; // to avoid "__proto__".
if (!stateMap[key]) {
stateMap[key] = {
nonStatic: { init: false, get: false, set: false },
static: { init: false, get: false, set: false }
};
}
return stateMap[key][isStatic ? "static" : "nonStatic"];
}
/**
* Gets the name text of a given node.
*
* @param {ASTNode} node - A node to get the name.
* @returns {string} The name text of the node.
*/
function getName(node) {
switch (node.type) {
case "Identifier": return node.name;
case "Literal": return String(node.value);
/* istanbul ignore next: syntax error */
default: return "";
}
}
return {
// Initializes the stack of state of member declarations.
Program() {
stack = [];
},
// Initializes state of member declarations for the class.
ClassBody() {
stack.push(Object.create(null));
},
// Disposes the state for the class.
"ClassBody:exit"() {
stack.pop();
},
// Reports the node if its name has been declared already.
MethodDefinition(node) {
if (node.computed) {
return;
}
const name = getName(node.key);
const state = getState(name, node.static);
let isDuplicate = false;
if (node.kind === "get") {
isDuplicate = (state.init || state.get);
state.get = true;
} else if (node.kind === "set") {
isDuplicate = (state.init || state.set);
state.set = true;
} else {
isDuplicate = (state.init || state.get || state.set);
state.init = true;
}
if (isDuplicate) {
context.report({ node, message: "Duplicate name '{{name}}'.", data: { name } });
}
}
};
}
};

View File

@ -0,0 +1,135 @@
/**
* @fileoverview Rule to flag use of duplicate keys in an object.
* @author Ian Christian Myers
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const GET_KIND = /^(?:init|get)$/;
const SET_KIND = /^(?:init|set)$/;
/**
* The class which stores properties' information of an object.
*/
class ObjectInfo {
/**
* @param {ObjectInfo|null} upper - The information of the outer object.
* @param {ASTNode} node - The ObjectExpression node of this information.
*/
constructor(upper, node) {
this.upper = upper;
this.node = node;
this.properties = new Map();
}
/**
* Gets the information of the given Property node.
* @param {ASTNode} node - The Property node to get.
* @returns {{get: boolean, set: boolean}} The information of the property.
*/
getPropertyInfo(node) {
const name = astUtils.getStaticPropertyName(node);
if (!this.properties.has(name)) {
this.properties.set(name, { get: false, set: false });
}
return this.properties.get(name);
}
/**
* Checks whether the given property has been defined already or not.
* @param {ASTNode} node - The Property node to check.
* @returns {boolean} `true` if the property has been defined.
*/
isPropertyDefined(node) {
const entry = this.getPropertyInfo(node);
return (
(GET_KIND.test(node.kind) && entry.get) ||
(SET_KIND.test(node.kind) && entry.set)
);
}
/**
* Defines the given property.
* @param {ASTNode} node - The Property node to define.
* @returns {void}
*/
defineProperty(node) {
const entry = this.getPropertyInfo(node);
if (GET_KIND.test(node.kind)) {
entry.get = true;
}
if (SET_KIND.test(node.kind)) {
entry.set = true;
}
}
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow duplicate keys in object literals",
category: "Possible Errors",
recommended: true
},
schema: []
},
create(context) {
let info = null;
return {
ObjectExpression(node) {
info = new ObjectInfo(info, node);
},
"ObjectExpression:exit"() {
info = info.upper;
},
Property(node) {
const name = astUtils.getStaticPropertyName(node);
// Skip destructuring.
if (node.parent.type !== "ObjectExpression") {
return;
}
// Skip if the name is not static.
if (!name) {
return;
}
// Reports if the name is defined already.
if (info.isPropertyDefined(node)) {
context.report({
node: info.node,
loc: node.key.loc,
message: "Duplicate key '{{name}}'.",
data: { name }
});
}
// Update info.
info.defineProperty(node);
}
};
}
};

View File

@ -0,0 +1,43 @@
/**
* @fileoverview Rule to disallow a duplicate case label.
* @author Dieter Oberkofler
* @author Burak Yigit Kaya
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow duplicate case labels",
category: "Possible Errors",
recommended: true
},
schema: []
},
create(context) {
const sourceCode = context.getSourceCode();
return {
SwitchStatement(node) {
const mapping = {};
node.cases.forEach(switchCase => {
const key = sourceCode.getText(switchCase.test);
if (mapping[key]) {
context.report({ node: switchCase, message: "Duplicate case label." });
} else {
mapping[key] = switchCase;
}
});
}
};
}
};

View File

@ -0,0 +1,137 @@
/**
* @fileoverview Restrict usage of duplicate imports.
* @author Simen Bekkhus
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/**
* Returns the name of the module imported or re-exported.
*
* @param {ASTNode} node - A node to get.
* @returns {string} the name of the module, or empty string if no name.
*/
function getValue(node) {
if (node && node.source && node.source.value) {
return node.source.value.trim();
}
return "";
}
/**
* Checks if the name of the import or export exists in the given array, and reports if so.
*
* @param {RuleContext} context - The ESLint rule context object.
* @param {ASTNode} node - A node to get.
* @param {string} value - The name of the imported or exported module.
* @param {string[]} array - The array containing other imports or exports in the file.
* @param {string} message - A message to be reported after the name of the module
*
* @returns {void} No return value
*/
function checkAndReport(context, node, value, array, message) {
if (array.indexOf(value) !== -1) {
context.report({
node,
message: "'{{module}}' {{message}}",
data: {
module: value,
message
}
});
}
}
/**
* @callback nodeCallback
* @param {ASTNode} node - A node to handle.
*/
/**
* Returns a function handling the imports of a given file
*
* @param {RuleContext} context - The ESLint rule context object.
* @param {boolean} includeExports - Whether or not to check for exports in addition to imports.
* @param {string[]} importsInFile - The array containing other imports in the file.
* @param {string[]} exportsInFile - The array containing other exports in the file.
*
* @returns {nodeCallback} A function passed to ESLint to handle the statement.
*/
function handleImports(context, includeExports, importsInFile, exportsInFile) {
return function(node) {
const value = getValue(node);
if (value) {
checkAndReport(context, node, value, importsInFile, "import is duplicated.");
if (includeExports) {
checkAndReport(context, node, value, exportsInFile, "import is duplicated as export.");
}
importsInFile.push(value);
}
};
}
/**
* Returns a function handling the exports of a given file
*
* @param {RuleContext} context - The ESLint rule context object.
* @param {string[]} importsInFile - The array containing other imports in the file.
* @param {string[]} exportsInFile - The array containing other exports in the file.
*
* @returns {nodeCallback} A function passed to ESLint to handle the statement.
*/
function handleExports(context, importsInFile, exportsInFile) {
return function(node) {
const value = getValue(node);
if (value) {
checkAndReport(context, node, value, exportsInFile, "export is duplicated.");
checkAndReport(context, node, value, importsInFile, "export is duplicated as import.");
exportsInFile.push(value);
}
};
}
module.exports = {
meta: {
docs: {
description: "disallow duplicate module imports",
category: "ECMAScript 6",
recommended: false
},
schema: [{
type: "object",
properties: {
includeExports: {
type: "boolean"
}
},
additionalProperties: false
}]
},
create(context) {
const includeExports = (context.options[0] || {}).includeExports,
importsInFile = [],
exportsInFile = [];
const handlers = {
ImportDeclaration: handleImports(context, includeExports, importsInFile, exportsInFile)
};
if (includeExports) {
handlers.ExportNamedDeclaration = handleExports(context, importsInFile, exportsInFile);
handlers.ExportAllDeclaration = handleExports(context, importsInFile, exportsInFile);
}
return handlers;
}
};

View File

@ -0,0 +1,235 @@
/**
* @fileoverview Rule to flag `else` after a `return` in `if`
* @author Ian Christian Myers
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
const FixTracker = require("../util/fix-tracker");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow `else` blocks after `return` statements in `if` statements",
category: "Best Practices",
recommended: false
},
schema: [],
fixable: "code"
},
create(context) {
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Display the context report if rule is violated
*
* @param {Node} node The 'else' node
* @returns {void}
*/
function displayReport(node) {
context.report({
node,
message: "Unnecessary 'else' after 'return'.",
fix: fixer => {
const sourceCode = context.getSourceCode();
const startToken = sourceCode.getFirstToken(node);
const elseToken = sourceCode.getTokenBefore(startToken);
const source = sourceCode.getText(node);
const lastIfToken = sourceCode.getTokenBefore(elseToken);
let fixedSource, firstTokenOfElseBlock;
if (startToken.type === "Punctuator" && startToken.value === "{") {
firstTokenOfElseBlock = sourceCode.getTokenAfter(startToken);
} else {
firstTokenOfElseBlock = startToken;
}
// If the if block does not have curly braces and does not end in a semicolon
// and the else block starts with (, [, /, +, ` or -, then it is not
// safe to remove the else keyword, because ASI will not add a semicolon
// after the if block
const ifBlockMaybeUnsafe = node.parent.consequent.type !== "BlockStatement" && lastIfToken.value !== ";";
const elseBlockUnsafe = /^[([/+`-]/.test(firstTokenOfElseBlock.value);
if (ifBlockMaybeUnsafe && elseBlockUnsafe) {
return null;
}
const endToken = sourceCode.getLastToken(node);
const lastTokenOfElseBlock = sourceCode.getTokenBefore(endToken);
if (lastTokenOfElseBlock.value !== ";") {
const nextToken = sourceCode.getTokenAfter(endToken);
const nextTokenUnsafe = nextToken && /^[([/+`-]/.test(nextToken.value);
const nextTokenOnSameLine = nextToken && nextToken.loc.start.line === lastTokenOfElseBlock.loc.start.line;
// If the else block contents does not end in a semicolon,
// and the else block starts with (, [, /, +, ` or -, then it is not
// safe to remove the else block, because ASI will not add a semicolon
// after the remaining else block contents
if (nextTokenUnsafe || (nextTokenOnSameLine && nextToken.value !== "}")) {
return null;
}
}
if (startToken.type === "Punctuator" && startToken.value === "{") {
fixedSource = source.slice(1, -1);
} else {
fixedSource = source;
}
// Extend the replacement range to include the entire
// function to avoid conflicting with no-useless-return.
// https://github.com/eslint/eslint/issues/8026
return new FixTracker(fixer, sourceCode)
.retainEnclosingFunction(node)
.replaceTextRange([elseToken.start, node.end], fixedSource);
}
});
}
/**
* Check to see if the node is a ReturnStatement
*
* @param {Node} node The node being evaluated
* @returns {boolean} True if node is a return
*/
function checkForReturn(node) {
return node.type === "ReturnStatement";
}
/**
* Naive return checking, does not iterate through the whole
* BlockStatement because we make the assumption that the ReturnStatement
* will be the last node in the body of the BlockStatement.
*
* @param {Node} node The consequent/alternate node
* @returns {boolean} True if it has a return
*/
function naiveHasReturn(node) {
if (node.type === "BlockStatement") {
const body = node.body,
lastChildNode = body[body.length - 1];
return lastChildNode && checkForReturn(lastChildNode);
}
return checkForReturn(node);
}
/**
* Check to see if the node is valid for evaluation,
* meaning it has an else and not an else-if
*
* @param {Node} node The node being evaluated
* @returns {boolean} True if the node is valid
*/
function hasElse(node) {
return node.alternate && node.consequent && node.alternate.type !== "IfStatement";
}
/**
* If the consequent is an IfStatement, check to see if it has an else
* and both its consequent and alternate path return, meaning this is
* a nested case of rule violation. If-Else not considered currently.
*
* @param {Node} node The consequent node
* @returns {boolean} True if this is a nested rule violation
*/
function checkForIf(node) {
return node.type === "IfStatement" && hasElse(node) &&
naiveHasReturn(node.alternate) && naiveHasReturn(node.consequent);
}
/**
* Check the consequent/body node to make sure it is not
* a ReturnStatement or an IfStatement that returns on both
* code paths.
*
* @param {Node} node The consequent or body node
* @param {Node} alternate The alternate node
* @returns {boolean} `true` if it is a Return/If node that always returns.
*/
function checkForReturnOrIf(node) {
return checkForReturn(node) || checkForIf(node);
}
/**
* Check whether a node returns in every codepath.
* @param {Node} node The node to be checked
* @returns {boolean} `true` if it returns on every codepath.
*/
function alwaysReturns(node) {
if (node.type === "BlockStatement") {
// If we have a BlockStatement, check each consequent body node.
return node.body.some(checkForReturnOrIf);
}
/*
* If not a block statement, make sure the consequent isn't a
* ReturnStatement or an IfStatement with returns on both paths.
*/
return checkForReturnOrIf(node);
}
/**
* Check the if statement
* @returns {void}
* @param {Node} node The node for the if statement to check
* @private
*/
function IfStatement(node) {
const parent = context.getAncestors().pop();
let consequents,
alternate;
/*
* Fixing this would require splitting one statement into two, so no error should
* be reported if this node is in a position where only one statement is allowed.
*/
if (!astUtils.STATEMENT_LIST_PARENTS.has(parent.type)) {
return;
}
for (consequents = []; node.type === "IfStatement"; node = node.alternate) {
if (!node.alternate) {
return;
}
consequents.push(node.consequent);
alternate = node.alternate;
}
if (consequents.every(alwaysReturns)) {
displayReport(alternate);
}
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
"IfStatement:exit": IfStatement
};
}
};

View File

@ -0,0 +1,57 @@
/**
* @fileoverview Rule to flag the use of empty character classes in regular expressions
* @author Ian Christian Myers
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/*
plain-English description of the following regexp:
0. `^` fix the match at the beginning of the string
1. `\/`: the `/` that begins the regexp
2. `([^\\[]|\\.|\[([^\\\]]|\\.)+\])*`: regexp contents; 0 or more of the following
2.0. `[^\\[]`: any character that's not a `\` or a `[` (anything but escape sequences and character classes)
2.1. `\\.`: an escape sequence
2.2. `\[([^\\\]]|\\.)+\]`: a character class that isn't empty
3. `\/` the `/` that ends the regexp
4. `[gimuy]*`: optional regexp flags
5. `$`: fix the match at the end of the string
*/
const regex = /^\/([^\\[]|\\.|\[([^\\\]]|\\.)+])*\/[gimuy]*$/;
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow empty character classes in regular expressions",
category: "Possible Errors",
recommended: true
},
schema: []
},
create(context) {
const sourceCode = context.getSourceCode();
return {
Literal(node) {
const token = sourceCode.getFirstToken(node);
if (token.type === "RegularExpression" && !regex.test(token.value)) {
context.report({ node, message: "Empty class." });
}
}
};
}
};

View File

@ -0,0 +1,156 @@
/**
* @fileoverview Rule to disallow empty functions.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const ALLOW_OPTIONS = Object.freeze([
"functions",
"arrowFunctions",
"generatorFunctions",
"methods",
"generatorMethods",
"getters",
"setters",
"constructors"
]);
/**
* Gets the kind of a given function node.
*
* @param {ASTNode} node - A function node to get. This is one of
* an ArrowFunctionExpression, a FunctionDeclaration, or a
* FunctionExpression.
* @returns {string} The kind of the function. This is one of "functions",
* "arrowFunctions", "generatorFunctions", "asyncFunctions", "methods",
* "generatorMethods", "asyncMethods", "getters", "setters", and
* "constructors".
*/
function getKind(node) {
const parent = node.parent;
let kind = "";
if (node.type === "ArrowFunctionExpression") {
return "arrowFunctions";
}
// Detects main kind.
if (parent.type === "Property") {
if (parent.kind === "get") {
return "getters";
}
if (parent.kind === "set") {
return "setters";
}
kind = parent.method ? "methods" : "functions";
} else if (parent.type === "MethodDefinition") {
if (parent.kind === "get") {
return "getters";
}
if (parent.kind === "set") {
return "setters";
}
if (parent.kind === "constructor") {
return "constructors";
}
kind = "methods";
} else {
kind = "functions";
}
// Detects prefix.
let prefix = "";
if (node.generator) {
prefix = "generator";
} else if (node.async) {
prefix = "async";
} else {
return kind;
}
return prefix + kind[0].toUpperCase() + kind.slice(1);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow empty functions",
category: "Best Practices",
recommended: false
},
schema: [
{
type: "object",
properties: {
allow: {
type: "array",
items: { enum: ALLOW_OPTIONS },
uniqueItems: true
}
},
additionalProperties: false
}
]
},
create(context) {
const options = context.options[0] || {};
const allowed = options.allow || [];
const sourceCode = context.getSourceCode();
/**
* Reports a given function node if the node matches the following patterns.
*
* - Not allowed by options.
* - The body is empty.
* - The body doesn't have any comments.
*
* @param {ASTNode} node - A function node to report. This is one of
* an ArrowFunctionExpression, a FunctionDeclaration, or a
* FunctionExpression.
* @returns {void}
*/
function reportIfEmpty(node) {
const kind = getKind(node);
const name = astUtils.getFunctionNameWithKind(node);
if (allowed.indexOf(kind) === -1 &&
node.body.type === "BlockStatement" &&
node.body.body.length === 0 &&
sourceCode.getComments(node.body).trailing.length === 0
) {
context.report({
node,
loc: node.body.loc.start,
message: "Unexpected empty {{name}}.",
data: { name }
});
}
}
return {
ArrowFunctionExpression: reportIfEmpty,
FunctionDeclaration: reportIfEmpty,
FunctionExpression: reportIfEmpty
};
}
};

View File

@ -0,0 +1,36 @@
/**
* @fileoverview Rule to disallow an empty pattern
* @author Alberto Rodríguez
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow empty destructuring patterns",
category: "Best Practices",
recommended: true
},
schema: []
},
create(context) {
return {
ObjectPattern(node) {
if (node.properties.length === 0) {
context.report({ node, message: "Unexpected empty object pattern." });
}
},
ArrayPattern(node) {
if (node.elements.length === 0) {
context.report({ node, message: "Unexpected empty array pattern." });
}
}
};
}
};

View File

@ -0,0 +1,78 @@
/**
* @fileoverview Rule to flag use of an empty block statement
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow empty block statements",
category: "Possible Errors",
recommended: true
},
schema: [
{
type: "object",
properties: {
allowEmptyCatch: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
const options = context.options[0] || {},
allowEmptyCatch = options.allowEmptyCatch || false;
const sourceCode = context.getSourceCode();
return {
BlockStatement(node) {
// if the body is not empty, we can just return immediately
if (node.body.length !== 0) {
return;
}
// a function is generally allowed to be empty
if (astUtils.isFunction(node.parent)) {
return;
}
if (allowEmptyCatch && node.parent.type === "CatchClause") {
return;
}
// any other block is only allowed to be empty, if it contains a comment
if (sourceCode.getComments(node).trailing.length > 0) {
return;
}
context.report({ node, message: "Empty block statement." });
},
SwitchStatement(node) {
if (typeof node.cases === "undefined" || node.cases.length === 0) {
context.report({ node, message: "Empty switch statement." });
}
}
};
}
};

View File

@ -0,0 +1,39 @@
/**
* @fileoverview Rule to flag comparisons to null without a type-checking
* operator.
* @author Ian Christian Myers
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow `null` comparisons without type-checking operators",
category: "Best Practices",
recommended: false
},
schema: []
},
create(context) {
return {
BinaryExpression(node) {
const badOperator = node.operator === "==" || node.operator === "!=";
if (node.right.type === "Literal" && node.right.raw === "null" && badOperator ||
node.left.type === "Literal" && node.left.raw === "null" && badOperator) {
context.report({ node, message: "Use === to compare with null." });
}
}
};
}
};

View File

@ -0,0 +1,308 @@
/**
* @fileoverview Rule to flag use of eval() statement
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const candidatesOfGlobalObject = Object.freeze([
"global",
"window"
]);
/**
* Checks a given node is a Identifier node of the specified name.
*
* @param {ASTNode} node - A node to check.
* @param {string} name - A name to check.
* @returns {boolean} `true` if the node is a Identifier node of the name.
*/
function isIdentifier(node, name) {
return node.type === "Identifier" && node.name === name;
}
/**
* Checks a given node is a Literal node of the specified string value.
*
* @param {ASTNode} node - A node to check.
* @param {string} name - A name to check.
* @returns {boolean} `true` if the node is a Literal node of the name.
*/
function isConstant(node, name) {
switch (node.type) {
case "Literal":
return node.value === name;
case "TemplateLiteral":
return (
node.expressions.length === 0 &&
node.quasis[0].value.cooked === name
);
default:
return false;
}
}
/**
* Checks a given node is a MemberExpression node which has the specified name's
* property.
*
* @param {ASTNode} node - A node to check.
* @param {string} name - A name to check.
* @returns {boolean} `true` if the node is a MemberExpression node which has
* the specified name's property
*/
function isMember(node, name) {
return (
node.type === "MemberExpression" &&
(node.computed ? isConstant : isIdentifier)(node.property, name)
);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow the use of `eval()`",
category: "Best Practices",
recommended: false
},
schema: [
{
type: "object",
properties: {
allowIndirect: { type: "boolean" }
},
additionalProperties: false
}
]
},
create(context) {
const allowIndirect = Boolean(
context.options[0] &&
context.options[0].allowIndirect
);
const sourceCode = context.getSourceCode();
let funcInfo = null;
/**
* Pushs a variable scope (Program or Function) information to the stack.
*
* This is used in order to check whether or not `this` binding is a
* reference to the global object.
*
* @param {ASTNode} node - A node of the scope. This is one of Program,
* FunctionDeclaration, FunctionExpression, and ArrowFunctionExpression.
* @returns {void}
*/
function enterVarScope(node) {
const strict = context.getScope().isStrict;
funcInfo = {
upper: funcInfo,
node,
strict,
defaultThis: false,
initialized: strict
};
}
/**
* Pops a variable scope from the stack.
*
* @returns {void}
*/
function exitVarScope() {
funcInfo = funcInfo.upper;
}
/**
* Reports a given node.
*
* `node` is `Identifier` or `MemberExpression`.
* The parent of `node` might be `CallExpression`.
*
* The location of the report is always `eval` `Identifier` (or possibly
* `Literal`). The type of the report is `CallExpression` if the parent is
* `CallExpression`. Otherwise, it's the given node type.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function report(node) {
let locationNode = node;
const parent = node.parent;
if (node.type === "MemberExpression") {
locationNode = node.property;
}
if (parent.type === "CallExpression" && parent.callee === node) {
node = parent;
}
context.report({
node,
loc: locationNode.loc.start,
message: "eval can be harmful."
});
}
/**
* Reports accesses of `eval` via the global object.
*
* @param {escope.Scope} globalScope - The global scope.
* @returns {void}
*/
function reportAccessingEvalViaGlobalObject(globalScope) {
for (let i = 0; i < candidatesOfGlobalObject.length; ++i) {
const name = candidatesOfGlobalObject[i];
const variable = astUtils.getVariableByName(globalScope, name);
if (!variable) {
continue;
}
const references = variable.references;
for (let j = 0; j < references.length; ++j) {
const identifier = references[j].identifier;
let node = identifier.parent;
// To detect code like `window.window.eval`.
while (isMember(node, name)) {
node = node.parent;
}
// Reports.
if (isMember(node, "eval")) {
report(node);
}
}
}
}
/**
* Reports all accesses of `eval` (excludes direct calls to eval).
*
* @param {escope.Scope} globalScope - The global scope.
* @returns {void}
*/
function reportAccessingEval(globalScope) {
const variable = astUtils.getVariableByName(globalScope, "eval");
if (!variable) {
return;
}
const references = variable.references;
for (let i = 0; i < references.length; ++i) {
const reference = references[i];
const id = reference.identifier;
if (id.name === "eval" && !astUtils.isCallee(id)) {
// Is accessing to eval (excludes direct calls to eval)
report(id);
}
}
}
if (allowIndirect) {
// Checks only direct calls to eval. It's simple!
return {
"CallExpression:exit"(node) {
const callee = node.callee;
if (isIdentifier(callee, "eval")) {
report(callee);
}
}
};
}
return {
"CallExpression:exit"(node) {
const callee = node.callee;
if (isIdentifier(callee, "eval")) {
report(callee);
}
},
Program(node) {
const scope = context.getScope(),
features = context.parserOptions.ecmaFeatures || {},
strict =
scope.isStrict ||
node.sourceType === "module" ||
(features.globalReturn && scope.childScopes[0].isStrict);
funcInfo = {
upper: null,
node,
strict,
defaultThis: true,
initialized: true
};
},
"Program:exit"() {
const globalScope = context.getScope();
exitVarScope();
reportAccessingEval(globalScope);
reportAccessingEvalViaGlobalObject(globalScope);
},
FunctionDeclaration: enterVarScope,
"FunctionDeclaration:exit": exitVarScope,
FunctionExpression: enterVarScope,
"FunctionExpression:exit": exitVarScope,
ArrowFunctionExpression: enterVarScope,
"ArrowFunctionExpression:exit": exitVarScope,
ThisExpression(node) {
if (!isMember(node.parent, "eval")) {
return;
}
/*
* `this.eval` is found.
* Checks whether or not the value of `this` is the global object.
*/
if (!funcInfo.initialized) {
funcInfo.initialized = true;
funcInfo.defaultThis = astUtils.isDefaultThisBinding(
funcInfo.node,
sourceCode
);
}
if (!funcInfo.strict && funcInfo.defaultThis) {
// `this.eval` is possible built-in `eval`.
report(node.parent);
}
}
};
}
};

View File

@ -0,0 +1,45 @@
/**
* @fileoverview Rule to flag assignment of the exception parameter
* @author Stephen Murray <spmurrayzzz>
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow reassigning exceptions in `catch` clauses",
category: "Possible Errors",
recommended: true
},
schema: []
},
create(context) {
/**
* Finds and reports references that are non initializer and writable.
* @param {Variable} variable - A variable to check.
* @returns {void}
*/
function checkVariable(variable) {
astUtils.getModifyingReferences(variable.references).forEach(reference => {
context.report({ node: reference.identifier, message: "Do not assign to the exception parameter." });
});
}
return {
CatchClause(node) {
context.getDeclaredVariables(node).forEach(checkVariable);
}
};
}
};

View File

@ -0,0 +1,117 @@
/**
* @fileoverview Rule to flag adding properties to native object's prototypes.
* @author David Nelson
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const globals = require("globals");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow extending native types",
category: "Best Practices",
recommended: false
},
schema: [
{
type: "object",
properties: {
exceptions: {
type: "array",
items: {
type: "string"
},
uniqueItems: true
}
},
additionalProperties: false
}
]
},
create(context) {
const config = context.options[0] || {};
const exceptions = config.exceptions || [];
let modifiedBuiltins = Object.keys(globals.builtin).filter(builtin => builtin[0].toUpperCase() === builtin[0]);
if (exceptions.length) {
modifiedBuiltins = modifiedBuiltins.filter(builtIn => exceptions.indexOf(builtIn) === -1);
}
return {
// handle the Array.prototype.extra style case
AssignmentExpression(node) {
const lhs = node.left;
if (lhs.type !== "MemberExpression" || lhs.object.type !== "MemberExpression") {
return;
}
const affectsProto = lhs.object.computed
? lhs.object.property.type === "Literal" && lhs.object.property.value === "prototype"
: lhs.object.property.name === "prototype";
if (!affectsProto) {
return;
}
modifiedBuiltins.forEach(builtin => {
if (lhs.object.object.name === builtin) {
context.report({
node,
message: "{{builtin}} prototype is read only, properties should not be added.",
data: {
builtin
}
});
}
});
},
// handle the Object.definePropert[y|ies](Array.prototype) case
CallExpression(node) {
const callee = node.callee;
// only worry about Object.definePropert[y|ies]
if (callee.type === "MemberExpression" &&
callee.object.name === "Object" &&
(callee.property.name === "defineProperty" || callee.property.name === "defineProperties")) {
// verify the object being added to is a native prototype
const subject = node.arguments[0];
const object = subject && subject.object;
if (object &&
object.type === "Identifier" &&
(modifiedBuiltins.indexOf(object.name) > -1) &&
subject.property.name === "prototype") {
context.report({
node,
message: "{{objectName}} prototype is read only, properties should not be added.",
data: {
objectName: object.name
}
});
}
}
}
};
}
};

View File

@ -0,0 +1,145 @@
/**
* @fileoverview Rule to flag unnecessary bind calls
* @author Bence Dányi <bence@danyi.me>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow unnecessary calls to `.bind()`",
category: "Best Practices",
recommended: false
},
schema: [],
fixable: "code"
},
create(context) {
let scopeInfo = null;
/**
* Reports a given function node.
*
* @param {ASTNode} node - A node to report. This is a FunctionExpression or
* an ArrowFunctionExpression.
* @returns {void}
*/
function report(node) {
context.report({
node: node.parent.parent,
message: "The function binding is unnecessary.",
loc: node.parent.property.loc.start,
fix(fixer) {
const firstTokenToRemove = context.getSourceCode()
.getFirstTokenBetween(node.parent.object, node.parent.property, astUtils.isNotClosingParenToken);
return fixer.removeRange([firstTokenToRemove.range[0], node.parent.parent.range[1]]);
}
});
}
/**
* Checks whether or not a given function node is the callee of `.bind()`
* method.
*
* e.g. `(function() {}.bind(foo))`
*
* @param {ASTNode} node - A node to report. This is a FunctionExpression or
* an ArrowFunctionExpression.
* @returns {boolean} `true` if the node is the callee of `.bind()` method.
*/
function isCalleeOfBindMethod(node) {
const parent = node.parent;
const grandparent = parent.parent;
return (
grandparent &&
grandparent.type === "CallExpression" &&
grandparent.callee === parent &&
grandparent.arguments.length === 1 &&
parent.type === "MemberExpression" &&
parent.object === node &&
astUtils.getStaticPropertyName(parent) === "bind"
);
}
/**
* Adds a scope information object to the stack.
*
* @param {ASTNode} node - A node to add. This node is a FunctionExpression
* or a FunctionDeclaration node.
* @returns {void}
*/
function enterFunction(node) {
scopeInfo = {
isBound: isCalleeOfBindMethod(node),
thisFound: false,
upper: scopeInfo
};
}
/**
* Removes the scope information object from the top of the stack.
* At the same time, this reports the function node if the function has
* `.bind()` and the `this` keywords found.
*
* @param {ASTNode} node - A node to remove. This node is a
* FunctionExpression or a FunctionDeclaration node.
* @returns {void}
*/
function exitFunction(node) {
if (scopeInfo.isBound && !scopeInfo.thisFound) {
report(node);
}
scopeInfo = scopeInfo.upper;
}
/**
* Reports a given arrow function if the function is callee of `.bind()`
* method.
*
* @param {ASTNode} node - A node to report. This node is an
* ArrowFunctionExpression.
* @returns {void}
*/
function exitArrowFunction(node) {
if (isCalleeOfBindMethod(node)) {
report(node);
}
}
/**
* Set the mark as the `this` keyword was found in this scope.
*
* @returns {void}
*/
function markAsThisFound() {
if (scopeInfo) {
scopeInfo.thisFound = true;
}
}
return {
"ArrowFunctionExpression:exit": exitArrowFunction,
FunctionDeclaration: enterFunction,
"FunctionDeclaration:exit": exitFunction,
FunctionExpression: enterFunction,
"FunctionExpression:exit": exitFunction,
ThisExpression: markAsThisFound
};
}
};

View File

@ -0,0 +1,122 @@
/**
* @fileoverview Rule to flag unnecessary double negation in Boolean contexts
* @author Brandon Mills
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow unnecessary boolean casts",
category: "Possible Errors",
recommended: true
},
schema: [],
fixable: "code"
},
create(context) {
const sourceCode = context.getSourceCode();
// Node types which have a test which will coerce values to booleans.
const BOOLEAN_NODE_TYPES = [
"IfStatement",
"DoWhileStatement",
"WhileStatement",
"ConditionalExpression",
"ForStatement"
];
/**
* Check if a node is in a context where its value would be coerced to a boolean at runtime.
*
* @param {Object} node The node
* @param {Object} parent Its parent
* @returns {boolean} If it is in a boolean context
*/
function isInBooleanContext(node, parent) {
return (
(BOOLEAN_NODE_TYPES.indexOf(parent.type) !== -1 &&
node === parent.test) ||
// !<bool>
(parent.type === "UnaryExpression" &&
parent.operator === "!")
);
}
return {
UnaryExpression(node) {
const ancestors = context.getAncestors(),
parent = ancestors.pop(),
grandparent = ancestors.pop();
// Exit early if it's guaranteed not to match
if (node.operator !== "!" ||
parent.type !== "UnaryExpression" ||
parent.operator !== "!") {
return;
}
if (isInBooleanContext(parent, grandparent) ||
// Boolean(<bool>) and new Boolean(<bool>)
((grandparent.type === "CallExpression" || grandparent.type === "NewExpression") &&
grandparent.callee.type === "Identifier" &&
grandparent.callee.name === "Boolean")
) {
context.report({
node,
message: "Redundant double negation.",
fix: fixer => fixer.replaceText(parent, sourceCode.getText(node.argument))
});
}
},
CallExpression(node) {
const parent = node.parent;
if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") {
return;
}
if (isInBooleanContext(node, parent)) {
context.report({
node,
message: "Redundant Boolean call.",
fix: fixer => {
if (!node.arguments.length) {
return fixer.replaceText(parent, "true");
}
if (node.arguments.length > 1 || node.arguments[0].type === "SpreadElement") {
return null;
}
const argument = node.arguments[0];
if (astUtils.getPrecedence(argument) < astUtils.getPrecedence(node.parent)) {
return fixer.replaceText(node, `(${sourceCode.getText(argument)})`);
}
return fixer.replaceText(node, sourceCode.getText(argument));
}
});
}
}
};
}
};

View File

@ -0,0 +1,140 @@
/**
* @fileoverview Rule to disallow unnecessary labels
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow unnecessary labels",
category: "Best Practices",
recommended: false
},
schema: [],
fixable: "code"
},
create(context) {
const sourceCode = context.getSourceCode();
let scopeInfo = null;
/**
* Creates a new scope with a breakable statement.
*
* @param {ASTNode} node - A node to create. This is a BreakableStatement.
* @returns {void}
*/
function enterBreakableStatement(node) {
scopeInfo = {
label: node.parent.type === "LabeledStatement" ? node.parent.label : null,
breakable: true,
upper: scopeInfo
};
}
/**
* Removes the top scope of the stack.
*
* @returns {void}
*/
function exitBreakableStatement() {
scopeInfo = scopeInfo.upper;
}
/**
* Creates a new scope with a labeled statement.
*
* This ignores it if the body is a breakable statement.
* In this case it's handled in the `enterBreakableStatement` function.
*
* @param {ASTNode} node - A node to create. This is a LabeledStatement.
* @returns {void}
*/
function enterLabeledStatement(node) {
if (!astUtils.isBreakableStatement(node.body)) {
scopeInfo = {
label: node.label,
breakable: false,
upper: scopeInfo
};
}
}
/**
* Removes the top scope of the stack.
*
* This ignores it if the body is a breakable statement.
* In this case it's handled in the `exitBreakableStatement` function.
*
* @param {ASTNode} node - A node. This is a LabeledStatement.
* @returns {void}
*/
function exitLabeledStatement(node) {
if (!astUtils.isBreakableStatement(node.body)) {
scopeInfo = scopeInfo.upper;
}
}
/**
* Reports a given control node if it's unnecessary.
*
* @param {ASTNode} node - A node. This is a BreakStatement or a
* ContinueStatement.
* @returns {void}
*/
function reportIfUnnecessary(node) {
if (!node.label) {
return;
}
const labelNode = node.label;
for (let info = scopeInfo; info !== null; info = info.upper) {
if (info.breakable || info.label && info.label.name === labelNode.name) {
if (info.breakable && info.label && info.label.name === labelNode.name) {
context.report({
node: labelNode,
message: "This label '{{name}}' is unnecessary.",
data: labelNode,
fix: fixer => fixer.removeRange([sourceCode.getFirstToken(node).range[1], labelNode.range[1]])
});
}
return;
}
}
}
return {
WhileStatement: enterBreakableStatement,
"WhileStatement:exit": exitBreakableStatement,
DoWhileStatement: enterBreakableStatement,
"DoWhileStatement:exit": exitBreakableStatement,
ForStatement: enterBreakableStatement,
"ForStatement:exit": exitBreakableStatement,
ForInStatement: enterBreakableStatement,
"ForInStatement:exit": exitBreakableStatement,
ForOfStatement: enterBreakableStatement,
"ForOfStatement:exit": exitBreakableStatement,
SwitchStatement: enterBreakableStatement,
"SwitchStatement:exit": exitBreakableStatement,
LabeledStatement: enterLabeledStatement,
"LabeledStatement:exit": exitLabeledStatement,
BreakStatement: reportIfUnnecessary,
ContinueStatement: reportIfUnnecessary
};
}
};

View File

@ -0,0 +1,664 @@
/**
* @fileoverview Disallow parenthesising higher precedence subexpressions.
* @author Michael Ficarra
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils.js");
const esUtils = require("esutils");
module.exports = {
meta: {
docs: {
description: "disallow unnecessary parentheses",
category: "Possible Errors",
recommended: false
},
fixable: "code",
schema: {
anyOf: [
{
type: "array",
items: [
{
enum: ["functions"]
}
],
minItems: 0,
maxItems: 1
},
{
type: "array",
items: [
{
enum: ["all"]
},
{
type: "object",
properties: {
conditionalAssign: { type: "boolean" },
nestedBinaryExpressions: { type: "boolean" },
returnAssign: { type: "boolean" },
ignoreJSX: { enum: ["none", "all", "single-line", "multi-line"] }
},
additionalProperties: false
}
],
minItems: 0,
maxItems: 2
}
]
}
},
create(context) {
const sourceCode = context.getSourceCode();
const tokensToIgnore = new WeakSet();
const isParenthesised = astUtils.isParenthesised.bind(astUtils, sourceCode);
const precedence = astUtils.getPrecedence;
const ALL_NODES = context.options[0] !== "functions";
const EXCEPT_COND_ASSIGN = ALL_NODES && context.options[1] && context.options[1].conditionalAssign === false;
const NESTED_BINARY = ALL_NODES && context.options[1] && context.options[1].nestedBinaryExpressions === false;
const EXCEPT_RETURN_ASSIGN = ALL_NODES && context.options[1] && context.options[1].returnAssign === false;
const IGNORE_JSX = ALL_NODES && context.options[1] && context.options[1].ignoreJSX;
const PRECEDENCE_OF_ASSIGNMENT_EXPR = precedence({ type: "AssignmentExpression" });
const PRECEDENCE_OF_UPDATE_EXPR = precedence({ type: "UpdateExpression" });
/**
* Determines if this rule should be enforced for a node given the current configuration.
* @param {ASTNode} node - The node to be checked.
* @returns {boolean} True if the rule should be enforced for this node.
* @private
*/
function ruleApplies(node) {
if (node.type === "JSXElement") {
const isSingleLine = node.loc.start.line === node.loc.end.line;
switch (IGNORE_JSX) {
// Exclude this JSX element from linting
case "all":
return false;
// Exclude this JSX element if it is multi-line element
case "multi-line":
return isSingleLine;
// Exclude this JSX element if it is single-line element
case "single-line":
return !isSingleLine;
// Nothing special to be done for JSX elements
case "none":
break;
// no default
}
}
return ALL_NODES || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
}
/**
* Determines if a node is surrounded by parentheses twice.
* @param {ASTNode} node - The node to be checked.
* @returns {boolean} True if the node is doubly parenthesised.
* @private
*/
function isParenthesisedTwice(node) {
const previousToken = sourceCode.getTokenBefore(node, 1),
nextToken = sourceCode.getTokenAfter(node, 1);
return isParenthesised(node) && previousToken && nextToken &&
astUtils.isOpeningParenToken(previousToken) && previousToken.range[1] <= node.range[0] &&
astUtils.isClosingParenToken(nextToken) && nextToken.range[0] >= node.range[1];
}
/**
* Determines if a node is surrounded by (potentially) invalid parentheses.
* @param {ASTNode} node - The node to be checked.
* @returns {boolean} True if the node is incorrectly parenthesised.
* @private
*/
function hasExcessParens(node) {
return ruleApplies(node) && isParenthesised(node);
}
/**
* Determines if a node that is expected to be parenthesised is surrounded by
* (potentially) invalid extra parentheses.
* @param {ASTNode} node - The node to be checked.
* @returns {boolean} True if the node is has an unexpected extra pair of parentheses.
* @private
*/
function hasDoubleExcessParens(node) {
return ruleApplies(node) && isParenthesisedTwice(node);
}
/**
* Determines if a node test expression is allowed to have a parenthesised assignment
* @param {ASTNode} node - The node to be checked.
* @returns {boolean} True if the assignment can be parenthesised.
* @private
*/
function isCondAssignException(node) {
return EXCEPT_COND_ASSIGN && node.test.type === "AssignmentExpression";
}
/**
* Determines if a node is in a return statement
* @param {ASTNode} node - The node to be checked.
* @returns {boolean} True if the node is in a return statement.
* @private
*/
function isInReturnStatement(node) {
while (node) {
if (node.type === "ReturnStatement" ||
(node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement")) {
return true;
}
node = node.parent;
}
return false;
}
/**
* Determines if a constructor function is newed-up with parens
* @param {ASTNode} newExpression - The NewExpression node to be checked.
* @returns {boolean} True if the constructor is called with parens.
* @private
*/
function isNewExpressionWithParens(newExpression) {
const lastToken = sourceCode.getLastToken(newExpression);
const penultimateToken = sourceCode.getTokenBefore(lastToken);
return newExpression.arguments.length > 0 || astUtils.isOpeningParenToken(penultimateToken) && astUtils.isClosingParenToken(lastToken);
}
/**
* Determines if a node is or contains an assignment expression
* @param {ASTNode} node - The node to be checked.
* @returns {boolean} True if the node is or contains an assignment expression.
* @private
*/
function containsAssignment(node) {
if (node.type === "AssignmentExpression") {
return true;
} else if (node.type === "ConditionalExpression" &&
(node.consequent.type === "AssignmentExpression" || node.alternate.type === "AssignmentExpression")) {
return true;
} else if ((node.left && node.left.type === "AssignmentExpression") ||
(node.right && node.right.type === "AssignmentExpression")) {
return true;
}
return false;
}
/**
* Determines if a node is contained by or is itself a return statement and is allowed to have a parenthesised assignment
* @param {ASTNode} node - The node to be checked.
* @returns {boolean} True if the assignment can be parenthesised.
* @private
*/
function isReturnAssignException(node) {
if (!EXCEPT_RETURN_ASSIGN || !isInReturnStatement(node)) {
return false;
}
if (node.type === "ReturnStatement") {
return node.argument && containsAssignment(node.argument);
} else if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") {
return containsAssignment(node.body);
}
return containsAssignment(node);
}
/**
* Determines if a node following a [no LineTerminator here] restriction is
* surrounded by (potentially) invalid extra parentheses.
* @param {Token} token - The token preceding the [no LineTerminator here] restriction.
* @param {ASTNode} node - The node to be checked.
* @returns {boolean} True if the node is incorrectly parenthesised.
* @private
*/
function hasExcessParensNoLineTerminator(token, node) {
if (token.loc.end.line === node.loc.start.line) {
return hasExcessParens(node);
}
return hasDoubleExcessParens(node);
}
/**
* Determines whether a node should be preceded by an additional space when removing parens
* @param {ASTNode} node node to evaluate; must be surrounded by parentheses
* @returns {boolean} `true` if a space should be inserted before the node
* @private
*/
function requiresLeadingSpace(node) {
const leftParenToken = sourceCode.getTokenBefore(node);
const tokenBeforeLeftParen = sourceCode.getTokenBefore(node, 1);
const firstToken = sourceCode.getFirstToken(node);
// If there is already whitespace before the previous token, don't add more.
if (!tokenBeforeLeftParen || tokenBeforeLeftParen.end !== leftParenToken.start) {
return false;
}
// If the parens are preceded by a keyword (e.g. `typeof(0)`), a space should be inserted (`typeof 0`)
const precededByIdentiferPart = esUtils.code.isIdentifierPartES6(tokenBeforeLeftParen.value.slice(-1).charCodeAt(0));
// However, a space should not be inserted unless the first character of the token is an identifier part
// e.g. `typeof([])` should be fixed to `typeof[]`
const startsWithIdentifierPart = esUtils.code.isIdentifierPartES6(firstToken.value.charCodeAt(0));
// If the parens are preceded by and start with a unary plus/minus (e.g. `+(+foo)`), a space should be inserted (`+ +foo`)
const precededByUnaryPlus = tokenBeforeLeftParen.type === "Punctuator" && tokenBeforeLeftParen.value === "+";
const precededByUnaryMinus = tokenBeforeLeftParen.type === "Punctuator" && tokenBeforeLeftParen.value === "-";
const startsWithUnaryPlus = firstToken.type === "Punctuator" && firstToken.value === "+";
const startsWithUnaryMinus = firstToken.type === "Punctuator" && firstToken.value === "-";
return (precededByIdentiferPart && startsWithIdentifierPart) ||
(precededByUnaryPlus && startsWithUnaryPlus) ||
(precededByUnaryMinus && startsWithUnaryMinus);
}
/**
* Report the node
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function report(node) {
const leftParenToken = sourceCode.getTokenBefore(node);
const rightParenToken = sourceCode.getTokenAfter(node);
if (tokensToIgnore.has(sourceCode.getFirstToken(node)) && !isParenthesisedTwice(node)) {
return;
}
context.report({
node,
loc: leftParenToken.loc.start,
message: "Gratuitous parentheses around expression.",
fix(fixer) {
const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0]);
return fixer.replaceTextRange([
leftParenToken.range[0],
rightParenToken.range[1]
], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource);
}
});
}
/**
* Evaluate Unary update
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function checkUnaryUpdate(node) {
if (node.type === "UnaryExpression" && node.argument.type === "BinaryExpression" && node.argument.operator === "**") {
return;
}
if (hasExcessParens(node.argument) && precedence(node.argument) >= precedence(node)) {
report(node.argument);
}
}
/**
* Evaluate a new call
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function checkCallNew(node) {
if (hasExcessParens(node.callee) && precedence(node.callee) >= precedence(node) && !(
node.type === "CallExpression" &&
(node.callee.type === "FunctionExpression" ||
node.callee.type === "NewExpression" && !isNewExpressionWithParens(node.callee)) &&
// One set of parentheses are allowed for a function expression
!hasDoubleExcessParens(node.callee)
)) {
report(node.callee);
}
if (node.arguments.length === 1) {
if (hasDoubleExcessParens(node.arguments[0]) && precedence(node.arguments[0]) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
report(node.arguments[0]);
}
} else {
[].forEach.call(node.arguments, arg => {
if (hasExcessParens(arg) && precedence(arg) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
report(arg);
}
});
}
}
/**
* Evaluate binary logicals
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function checkBinaryLogical(node) {
const prec = precedence(node);
const leftPrecedence = precedence(node.left);
const rightPrecedence = precedence(node.right);
const isExponentiation = node.operator === "**";
const shouldSkipLeft = (NESTED_BINARY && (node.left.type === "BinaryExpression" || node.left.type === "LogicalExpression")) ||
node.left.type === "UnaryExpression" && isExponentiation;
const shouldSkipRight = NESTED_BINARY && (node.right.type === "BinaryExpression" || node.right.type === "LogicalExpression");
if (!shouldSkipLeft && hasExcessParens(node.left) && (leftPrecedence > prec || (leftPrecedence === prec && !isExponentiation))) {
report(node.left);
}
if (!shouldSkipRight && hasExcessParens(node.right) && (rightPrecedence > prec || (rightPrecedence === prec && isExponentiation))) {
report(node.right);
}
}
/**
* Check the parentheses around the super class of the given class definition.
* @param {ASTNode} node The node of class declarations to check.
* @returns {void}
*/
function checkClass(node) {
if (!node.superClass) {
return;
}
// If `node.superClass` is a LeftHandSideExpression, parentheses are extra.
// Otherwise, parentheses are needed.
const hasExtraParens = precedence(node.superClass) > PRECEDENCE_OF_UPDATE_EXPR
? hasExcessParens(node.superClass)
: hasDoubleExcessParens(node.superClass);
if (hasExtraParens) {
report(node.superClass);
}
}
/**
* Check the parentheses around the argument of the given spread operator.
* @param {ASTNode} node The node of spread elements/properties to check.
* @returns {void}
*/
function checkSpreadOperator(node) {
const hasExtraParens = precedence(node.argument) >= PRECEDENCE_OF_ASSIGNMENT_EXPR
? hasExcessParens(node.argument)
: hasDoubleExcessParens(node.argument);
if (hasExtraParens) {
report(node.argument);
}
}
/**
* Checks the parentheses for an ExpressionStatement or ExportDefaultDeclaration
* @param {ASTNode} node The ExpressionStatement.expression or ExportDefaultDeclaration.declaration node
* @returns {void}
*/
function checkExpressionOrExportStatement(node) {
const firstToken = isParenthesised(node) ? sourceCode.getTokenBefore(node) : sourceCode.getFirstToken(node);
const secondToken = sourceCode.getTokenAfter(firstToken, astUtils.isNotOpeningParenToken);
if (
astUtils.isOpeningParenToken(firstToken) &&
(
astUtils.isOpeningBraceToken(secondToken) ||
secondToken.type === "Keyword" && (
secondToken.value === "function" ||
secondToken.value === "class" ||
secondToken.value === "let" && astUtils.isOpeningBracketToken(sourceCode.getTokenAfter(secondToken))
)
)
) {
tokensToIgnore.add(secondToken);
}
if (hasExcessParens(node)) {
report(node);
}
}
return {
ArrayExpression(node) {
[].forEach.call(node.elements, e => {
if (e && hasExcessParens(e) && precedence(e) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
report(e);
}
});
},
ArrowFunctionExpression(node) {
if (isReturnAssignException(node)) {
return;
}
if (node.body.type !== "BlockStatement") {
const firstBodyToken = sourceCode.getFirstToken(node.body, astUtils.isNotOpeningParenToken);
const tokenBeforeFirst = sourceCode.getTokenBefore(firstBodyToken);
if (astUtils.isOpeningParenToken(tokenBeforeFirst) && astUtils.isOpeningBraceToken(firstBodyToken)) {
tokensToIgnore.add(firstBodyToken);
}
if (hasExcessParens(node.body) && precedence(node.body) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
report(node.body);
}
}
},
AssignmentExpression(node) {
if (isReturnAssignException(node)) {
return;
}
if (hasExcessParens(node.right) && precedence(node.right) >= precedence(node)) {
report(node.right);
}
},
BinaryExpression: checkBinaryLogical,
CallExpression: checkCallNew,
ConditionalExpression(node) {
if (isReturnAssignException(node)) {
return;
}
if (hasExcessParens(node.test) && precedence(node.test) >= precedence({ type: "LogicalExpression", operator: "||" })) {
report(node.test);
}
if (hasExcessParens(node.consequent) && precedence(node.consequent) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
report(node.consequent);
}
if (hasExcessParens(node.alternate) && precedence(node.alternate) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
report(node.alternate);
}
},
DoWhileStatement(node) {
if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) {
report(node.test);
}
},
ExportDefaultDeclaration: node => checkExpressionOrExportStatement(node.declaration),
ExpressionStatement: node => checkExpressionOrExportStatement(node.expression),
ForInStatement(node) {
if (hasExcessParens(node.right)) {
report(node.right);
}
},
ForOfStatement(node) {
if (hasExcessParens(node.right)) {
report(node.right);
}
},
ForStatement(node) {
if (node.init && hasExcessParens(node.init)) {
report(node.init);
}
if (node.test && hasExcessParens(node.test) && !isCondAssignException(node)) {
report(node.test);
}
if (node.update && hasExcessParens(node.update)) {
report(node.update);
}
},
IfStatement(node) {
if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) {
report(node.test);
}
},
LogicalExpression: checkBinaryLogical,
MemberExpression(node) {
if (
hasExcessParens(node.object) &&
precedence(node.object) >= precedence(node) &&
(
node.computed ||
!(
astUtils.isDecimalInteger(node.object) ||
// RegExp literal is allowed to have parens (#1589)
(node.object.type === "Literal" && node.object.regex)
)
)
) {
report(node.object);
}
if (node.computed && hasExcessParens(node.property)) {
report(node.property);
}
},
NewExpression: checkCallNew,
ObjectExpression(node) {
[].forEach.call(node.properties, e => {
const v = e.value;
if (v && hasExcessParens(v) && precedence(v) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
report(v);
}
});
},
ReturnStatement(node) {
const returnToken = sourceCode.getFirstToken(node);
if (isReturnAssignException(node)) {
return;
}
if (node.argument &&
hasExcessParensNoLineTerminator(returnToken, node.argument) &&
// RegExp literal is allowed to have parens (#1589)
!(node.argument.type === "Literal" && node.argument.regex)) {
report(node.argument);
}
},
SequenceExpression(node) {
[].forEach.call(node.expressions, e => {
if (hasExcessParens(e) && precedence(e) >= precedence(node)) {
report(e);
}
});
},
SwitchCase(node) {
if (node.test && hasExcessParens(node.test)) {
report(node.test);
}
},
SwitchStatement(node) {
if (hasDoubleExcessParens(node.discriminant)) {
report(node.discriminant);
}
},
ThrowStatement(node) {
const throwToken = sourceCode.getFirstToken(node);
if (hasExcessParensNoLineTerminator(throwToken, node.argument)) {
report(node.argument);
}
},
UnaryExpression: checkUnaryUpdate,
UpdateExpression: checkUnaryUpdate,
AwaitExpression: checkUnaryUpdate,
VariableDeclarator(node) {
if (node.init && hasExcessParens(node.init) &&
precedence(node.init) >= PRECEDENCE_OF_ASSIGNMENT_EXPR &&
// RegExp literal is allowed to have parens (#1589)
!(node.init.type === "Literal" && node.init.regex)) {
report(node.init);
}
},
WhileStatement(node) {
if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) {
report(node.test);
}
},
WithStatement(node) {
if (hasDoubleExcessParens(node.object)) {
report(node.object);
}
},
YieldExpression(node) {
if (node.argument) {
const yieldToken = sourceCode.getFirstToken(node);
if ((precedence(node.argument) >= precedence(node) &&
hasExcessParensNoLineTerminator(yieldToken, node.argument)) ||
hasDoubleExcessParens(node.argument)) {
report(node.argument);
}
}
},
ClassDeclaration: checkClass,
ClassExpression: checkClass,
SpreadElement: checkSpreadOperator,
SpreadProperty: checkSpreadOperator,
ExperimentalSpreadProperty: checkSpreadOperator
};
}
};

View File

@ -0,0 +1,118 @@
/**
* @fileoverview Rule to flag use of unnecessary semicolons
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const FixTracker = require("../util/fix-tracker");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow unnecessary semicolons",
category: "Possible Errors",
recommended: true
},
fixable: "code",
schema: []
},
create(context) {
const sourceCode = context.getSourceCode();
/**
* Reports an unnecessary semicolon error.
* @param {Node|Token} nodeOrToken - A node or a token to be reported.
* @returns {void}
*/
function report(nodeOrToken) {
context.report({
node: nodeOrToken,
message: "Unnecessary semicolon.",
fix(fixer) {
// Expand the replacement range to include the surrounding
// tokens to avoid conflicting with semi.
// https://github.com/eslint/eslint/issues/7928
return new FixTracker(fixer, context.getSourceCode())
.retainSurroundingTokens(nodeOrToken)
.remove(nodeOrToken);
}
});
}
/**
* Checks for a part of a class body.
* This checks tokens from a specified token to a next MethodDefinition or the end of class body.
*
* @param {Token} firstToken - The first token to check.
* @returns {void}
*/
function checkForPartOfClassBody(firstToken) {
for (let token = firstToken;
token.type === "Punctuator" && !astUtils.isClosingBraceToken(token);
token = sourceCode.getTokenAfter(token)
) {
if (astUtils.isSemicolonToken(token)) {
report(token);
}
}
}
return {
/**
* Reports this empty statement, except if the parent node is a loop.
* @param {Node} node - A EmptyStatement node to be reported.
* @returns {void}
*/
EmptyStatement(node) {
const parent = node.parent,
allowedParentTypes = [
"ForStatement",
"ForInStatement",
"ForOfStatement",
"WhileStatement",
"DoWhileStatement",
"IfStatement",
"LabeledStatement",
"WithStatement"
];
if (allowedParentTypes.indexOf(parent.type) === -1) {
report(node);
}
},
/**
* Checks tokens from the head of this class body to the first MethodDefinition or the end of this class body.
* @param {Node} node - A ClassBody node to check.
* @returns {void}
*/
ClassBody(node) {
checkForPartOfClassBody(sourceCode.getFirstToken(node, 1)); // 0 is `{`.
},
/**
* Checks tokens from this MethodDefinition to the next MethodDefinition or the end of this class body.
* @param {Node} node - A MethodDefinition node of the start point.
* @returns {void}
*/
MethodDefinition(node) {
checkForPartOfClassBody(sourceCode.getTokenAfter(node));
}
};
}
};

View File

@ -0,0 +1,135 @@
/**
* @fileoverview Rule to flag fall-through cases in switch statements.
* @author Matt DuVall <http://mattduvall.com/>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/i;
/**
* Checks whether or not a given node has a fallthrough comment.
* @param {ASTNode} node - A SwitchCase node to get comments.
* @param {RuleContext} context - A rule context which stores comments.
* @param {RegExp} fallthroughCommentPattern - A pattern to match comment to.
* @returns {boolean} `true` if the node has a valid fallthrough comment.
*/
function hasFallthroughComment(node, context, fallthroughCommentPattern) {
const sourceCode = context.getSourceCode();
const comment = lodash.last(sourceCode.getComments(node).leading);
return Boolean(comment && fallthroughCommentPattern.test(comment.value));
}
/**
* Checks whether or not a given code path segment is reachable.
* @param {CodePathSegment} segment - A CodePathSegment to check.
* @returns {boolean} `true` if the segment is reachable.
*/
function isReachable(segment) {
return segment.reachable;
}
/**
* Checks whether a node and a token are separated by blank lines
* @param {ASTNode} node - The node to check
* @param {Token} token - The token to compare against
* @returns {boolean} `true` if there are blank lines between node and token
*/
function hasBlankLinesBetween(node, token) {
return token.loc.start.line > node.loc.end.line + 1;
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow fallthrough of `case` statements",
category: "Best Practices",
recommended: true
},
schema: [
{
type: "object",
properties: {
commentPattern: {
type: "string"
}
},
additionalProperties: false
}
]
},
create(context) {
const options = context.options[0] || {};
let currentCodePath = null;
const sourceCode = context.getSourceCode();
/*
* We need to use leading comments of the next SwitchCase node because
* trailing comments is wrong if semicolons are omitted.
*/
let fallthroughCase = null;
let fallthroughCommentPattern = null;
if (options.commentPattern) {
fallthroughCommentPattern = new RegExp(options.commentPattern);
} else {
fallthroughCommentPattern = DEFAULT_FALLTHROUGH_COMMENT;
}
return {
onCodePathStart(codePath) {
currentCodePath = codePath;
},
onCodePathEnd() {
currentCodePath = currentCodePath.upper;
},
SwitchCase(node) {
/*
* Checks whether or not there is a fallthrough comment.
* And reports the previous fallthrough node if that does not exist.
*/
if (fallthroughCase && !hasFallthroughComment(node, context, fallthroughCommentPattern)) {
context.report({
message: "Expected a 'break' statement before '{{type}}'.",
data: { type: node.test ? "case" : "default" },
node
});
}
fallthroughCase = null;
},
"SwitchCase:exit"(node) {
const nextToken = sourceCode.getTokenAfter(node);
/*
* `reachable` meant fall through because statements preceded by
* `break`, `return`, or `throw` are unreachable.
* And allows empty cases and the last case.
*/
if (currentCodePath.currentSegments.some(isReachable) &&
(node.consequent.length > 0 || hasBlankLinesBetween(node, nextToken)) &&
lodash.last(node.parent.cases) !== node) {
fallthroughCase = node;
}
}
};
}
};

View File

@ -0,0 +1,50 @@
/**
* @fileoverview Rule to flag use of a leading/trailing decimal point in a numeric literal
* @author James Allardice
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow leading or trailing decimal points in numeric literals",
category: "Best Practices",
recommended: false
},
schema: [],
fixable: "code"
},
create(context) {
return {
Literal(node) {
if (typeof node.value === "number") {
if (node.raw.indexOf(".") === 0) {
context.report({
node,
message: "A leading decimal point can be confused with a dot.",
fix: fixer => fixer.insertTextBefore(node, "0")
});
}
if (node.raw.indexOf(".") === node.raw.length - 1) {
context.report({
node,
message: "A trailing decimal point can be confused with a dot.",
fix: fixer => fixer.insertTextAfter(node, "0")
});
}
}
}
};
}
};

Some files were not shown because too many files have changed in this diff Show More