dev-api: add developer portal, OpenAPI spec, and Flatenbadet case study
- New /developers/ page with API docs, SDKs, pricing, use cases - OpenAPI 3.0 spec for Orders, Missions, Photos, Analytics - Case study: Glasskiosken i Flatenbadet — complete ROI analysis - Updated /order/ with recurring missions and frequency dropdown
This commit is contained in:
+346
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce getter and setter pairs in objects and classes.
|
||||
* @author Gyandeep Singh
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Property name if it can be computed statically, otherwise the list of the tokens of the key node.
|
||||
* @typedef {string|Token[]} Key
|
||||
*/
|
||||
|
||||
/**
|
||||
* Accessor nodes with the same key.
|
||||
* @typedef {Object} AccessorData
|
||||
* @property {Key} key Accessor's key
|
||||
* @property {ASTNode[]} getters List of getter nodes.
|
||||
* @property {ASTNode[]} setters List of setter nodes.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not the given lists represent the equal tokens in the same order.
|
||||
* Tokens are compared by their properties, not by instance.
|
||||
* @param {Token[]} left First list of tokens.
|
||||
* @param {Token[]} right Second list of tokens.
|
||||
* @returns {boolean} `true` if the lists have same tokens.
|
||||
*/
|
||||
function areEqualTokenLists(left, right) {
|
||||
if (left.length !== right.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
const leftToken = left[i],
|
||||
rightToken = right[i];
|
||||
|
||||
if (leftToken.type !== rightToken.type || leftToken.value !== rightToken.value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not the given keys are equal.
|
||||
* @param {Key} left First key.
|
||||
* @param {Key} right Second key.
|
||||
* @returns {boolean} `true` if the keys are equal.
|
||||
*/
|
||||
function areEqualKeys(left, right) {
|
||||
if (typeof left === "string" && typeof right === "string") {
|
||||
|
||||
// Statically computed names.
|
||||
return left === right;
|
||||
}
|
||||
if (Array.isArray(left) && Array.isArray(right)) {
|
||||
|
||||
// Token lists.
|
||||
return areEqualTokenLists(left, right);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is of an accessor kind ('get' or 'set').
|
||||
* @param {ASTNode} node A node to check.
|
||||
* @returns {boolean} `true` if the node is of an accessor kind.
|
||||
*/
|
||||
function isAccessorKind(node) {
|
||||
return node.kind === "get" || node.kind === "set";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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" &&
|
||||
astUtils.isSpecificMemberAccess(parent.callee, object, 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: ...}})
|
||||
*/
|
||||
const grandparent = node.parent.parent;
|
||||
|
||||
return grandparent.type === "ObjectExpression" && (
|
||||
isArgumentOfMethodCall(grandparent, 1, "Object", "create") ||
|
||||
isArgumentOfMethodCall(grandparent, 1, "Object", "defineProperties")
|
||||
);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce getter and setter pairs in objects and classes",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/accessor-pairs"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
getWithoutSet: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
setWithoutGet: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
},
|
||||
enforceForClassMembers: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
missingGetterInPropertyDescriptor: "Getter is not present in property descriptor.",
|
||||
missingSetterInPropertyDescriptor: "Setter is not present in property descriptor.",
|
||||
missingGetterInObjectLiteral: "Getter is not present for {{ name }}.",
|
||||
missingSetterInObjectLiteral: "Setter is not present for {{ name }}.",
|
||||
missingGetterInClass: "Getter is not present for class {{ name }}.",
|
||||
missingSetterInClass: "Setter is not present for class {{ name }}."
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
const config = context.options[0] || {};
|
||||
const checkGetWithoutSet = config.getWithoutSet === true;
|
||||
const checkSetWithoutGet = config.setWithoutGet !== false;
|
||||
const enforceForClassMembers = config.enforceForClassMembers !== false;
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Reports the given node.
|
||||
* @param {ASTNode} node The node to report.
|
||||
* @param {string} messageKind "missingGetter" or "missingSetter".
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function report(node, messageKind) {
|
||||
if (node.type === "Property") {
|
||||
context.report({
|
||||
node,
|
||||
messageId: `${messageKind}InObjectLiteral`,
|
||||
loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
|
||||
data: { name: astUtils.getFunctionNameWithKind(node.value) }
|
||||
});
|
||||
} else if (node.type === "MethodDefinition") {
|
||||
context.report({
|
||||
node,
|
||||
messageId: `${messageKind}InClass`,
|
||||
loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
|
||||
data: { name: astUtils.getFunctionNameWithKind(node.value) }
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
node,
|
||||
messageId: `${messageKind}InPropertyDescriptor`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports each of the nodes in the given list using the same messageId.
|
||||
* @param {ASTNode[]} nodes Nodes to report.
|
||||
* @param {string} messageKind "missingGetter" or "missingSetter".
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function reportList(nodes, messageKind) {
|
||||
for (const node of nodes) {
|
||||
report(node, messageKind);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks accessor pairs in the given list of nodes.
|
||||
* @param {ASTNode[]} nodes The list to check.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkList(nodes) {
|
||||
const accessors = [];
|
||||
let found = false;
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
|
||||
if (isAccessorKind(node)) {
|
||||
|
||||
// Creates a new `AccessorData` object for the given getter or setter node.
|
||||
const name = astUtils.getStaticPropertyName(node);
|
||||
const key = (name !== null) ? name : sourceCode.getTokens(node.key);
|
||||
|
||||
// Merges the given `AccessorData` object into the given accessors list.
|
||||
for (let j = 0; j < accessors.length; j++) {
|
||||
const accessor = accessors[j];
|
||||
|
||||
if (areEqualKeys(accessor.key, key)) {
|
||||
accessor.getters.push(...node.kind === "get" ? [node] : []);
|
||||
accessor.setters.push(...node.kind === "set" ? [node] : []);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
accessors.push({
|
||||
key,
|
||||
getters: node.kind === "get" ? [node] : [],
|
||||
setters: node.kind === "set" ? [node] : []
|
||||
});
|
||||
}
|
||||
found = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const { getters, setters } of accessors) {
|
||||
if (checkSetWithoutGet && setters.length && !getters.length) {
|
||||
reportList(setters, "missingGetter");
|
||||
}
|
||||
if (checkGetWithoutSet && getters.length && !setters.length) {
|
||||
reportList(getters, "missingSetter");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks accessor pairs in an object literal.
|
||||
* @param {ASTNode} node `ObjectExpression` node to check.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkObjectLiteral(node) {
|
||||
checkList(node.properties.filter(p => p.type === "Property"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks accessor pairs in a property descriptor.
|
||||
* @param {ASTNode} node Property descriptor `ObjectExpression` node to check.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkPropertyDescriptor(node) {
|
||||
const namesToCheck = new Set(node.properties
|
||||
.filter(p => p.type === "Property" && p.kind === "init" && !p.computed)
|
||||
.map(({ key }) => key.name));
|
||||
|
||||
const hasGetter = namesToCheck.has("get");
|
||||
const hasSetter = namesToCheck.has("set");
|
||||
|
||||
if (checkSetWithoutGet && hasSetter && !hasGetter) {
|
||||
report(node, "missingGetter");
|
||||
}
|
||||
if (checkGetWithoutSet && hasGetter && !hasSetter) {
|
||||
report(node, "missingSetter");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given object expression as an object literal and as a possible property descriptor.
|
||||
* @param {ASTNode} node `ObjectExpression` node to check.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkObjectExpression(node) {
|
||||
checkObjectLiteral(node);
|
||||
if (isPropertyDescriptor(node)) {
|
||||
checkPropertyDescriptor(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given class body.
|
||||
* @param {ASTNode} node `ClassBody` node to check.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkClassBody(node) {
|
||||
const methodDefinitions = node.body.filter(m => m.type === "MethodDefinition");
|
||||
|
||||
checkList(methodDefinitions.filter(m => m.static));
|
||||
checkList(methodDefinitions.filter(m => !m.static));
|
||||
}
|
||||
|
||||
const listeners = {};
|
||||
|
||||
if (checkSetWithoutGet || checkGetWithoutSet) {
|
||||
listeners.ObjectExpression = checkObjectExpression;
|
||||
if (enforceForClassMembers) {
|
||||
listeners.ClassBody = checkClassBody;
|
||||
}
|
||||
}
|
||||
|
||||
return listeners;
|
||||
}
|
||||
};
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce linebreaks after open and before close array brackets
|
||||
* @author Jan Peer Stöcklmair <https://github.com/JPeer264>
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce linebreaks after opening and before closing array brackets",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/array-bracket-newline"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["always", "never", "consistent"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
multiline: {
|
||||
type: "boolean"
|
||||
},
|
||||
minItems: {
|
||||
type: ["integer", "null"],
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedOpeningLinebreak: "There should be no linebreak after '['.",
|
||||
unexpectedClosingLinebreak: "There should be no linebreak before ']'.",
|
||||
missingOpeningLinebreak: "A linebreak is required after '['.",
|
||||
missingClosingLinebreak: "A linebreak is required before ']'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Helpers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Normalizes a given option value.
|
||||
* @param {string|Object|undefined} option An option value to parse.
|
||||
* @returns {{multiline: boolean, minItems: number}} Normalized option object.
|
||||
*/
|
||||
function normalizeOptionValue(option) {
|
||||
let consistent = false;
|
||||
let multiline = false;
|
||||
let minItems = 0;
|
||||
|
||||
if (option) {
|
||||
if (option === "consistent") {
|
||||
consistent = true;
|
||||
minItems = Number.POSITIVE_INFINITY;
|
||||
} else if (option === "always" || option.minItems === 0) {
|
||||
minItems = 0;
|
||||
} else if (option === "never") {
|
||||
minItems = Number.POSITIVE_INFINITY;
|
||||
} else {
|
||||
multiline = Boolean(option.multiline);
|
||||
minItems = option.minItems || Number.POSITIVE_INFINITY;
|
||||
}
|
||||
} else {
|
||||
consistent = false;
|
||||
multiline = true;
|
||||
minItems = Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
return { consistent, multiline, minItems };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a given option value.
|
||||
* @param {string|Object|undefined} options An option value to parse.
|
||||
* @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
|
||||
*/
|
||||
function normalizeOptions(options) {
|
||||
const value = normalizeOptionValue(options);
|
||||
|
||||
return { ArrayExpression: value, ArrayPattern: value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a linebreak 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 reportNoBeginningLinebreak(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc,
|
||||
messageId: "unexpectedOpeningLinebreak",
|
||||
fix(fixer) {
|
||||
const nextToken = sourceCode.getTokenAfter(token, { includeComments: true });
|
||||
|
||||
if (astUtils.isCommentToken(nextToken)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fixer.removeRange([token.range[1], nextToken.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a linebreak 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 reportNoEndingLinebreak(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc,
|
||||
messageId: "unexpectedClosingLinebreak",
|
||||
fix(fixer) {
|
||||
const previousToken = sourceCode.getTokenBefore(token, { includeComments: true });
|
||||
|
||||
if (astUtils.isCommentToken(previousToken)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fixer.removeRange([previousToken.range[1], token.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there should be a linebreak 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 reportRequiredBeginningLinebreak(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc,
|
||||
messageId: "missingOpeningLinebreak",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(token, "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there should be a linebreak 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 reportRequiredEndingLinebreak(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc,
|
||||
messageId: "missingClosingLinebreak",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(token, "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given node if it violated this rule.
|
||||
* @param {ASTNode} node A node to check. This is an ArrayExpression node or an ArrayPattern node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function check(node) {
|
||||
const elements = node.elements;
|
||||
const normalizedOptions = normalizeOptions(context.options[0]);
|
||||
const options = normalizedOptions[node.type];
|
||||
const openBracket = sourceCode.getFirstToken(node);
|
||||
const closeBracket = sourceCode.getLastToken(node);
|
||||
const firstIncComment = sourceCode.getTokenAfter(openBracket, { includeComments: true });
|
||||
const lastIncComment = sourceCode.getTokenBefore(closeBracket, { includeComments: true });
|
||||
const first = sourceCode.getTokenAfter(openBracket);
|
||||
const last = sourceCode.getTokenBefore(closeBracket);
|
||||
|
||||
const needsLinebreaks = (
|
||||
elements.length >= options.minItems ||
|
||||
(
|
||||
options.multiline &&
|
||||
elements.length > 0 &&
|
||||
firstIncComment.loc.start.line !== lastIncComment.loc.end.line
|
||||
) ||
|
||||
(
|
||||
elements.length === 0 &&
|
||||
firstIncComment.type === "Block" &&
|
||||
firstIncComment.loc.start.line !== lastIncComment.loc.end.line &&
|
||||
firstIncComment === lastIncComment
|
||||
) ||
|
||||
(
|
||||
options.consistent &&
|
||||
openBracket.loc.end.line !== first.loc.start.line
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
* Use tokens or comments to check multiline or not.
|
||||
* But use only tokens to check whether linebreaks are needed.
|
||||
* This allows:
|
||||
* var arr = [ // eslint-disable-line foo
|
||||
* 'a'
|
||||
* ]
|
||||
*/
|
||||
|
||||
if (needsLinebreaks) {
|
||||
if (astUtils.isTokenOnSameLine(openBracket, first)) {
|
||||
reportRequiredBeginningLinebreak(node, openBracket);
|
||||
}
|
||||
if (astUtils.isTokenOnSameLine(last, closeBracket)) {
|
||||
reportRequiredEndingLinebreak(node, closeBracket);
|
||||
}
|
||||
} else {
|
||||
if (!astUtils.isTokenOnSameLine(openBracket, first)) {
|
||||
reportNoBeginningLinebreak(node, openBracket);
|
||||
}
|
||||
if (!astUtils.isTokenOnSameLine(last, closeBracket)) {
|
||||
reportNoEndingLinebreak(node, closeBracket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
ArrayPattern: check,
|
||||
ArrayExpression: check
|
||||
};
|
||||
}
|
||||
};
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* @fileoverview Disallows or enforces spaces inside of array brackets.
|
||||
* @author Jamund Ferguson
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent spacing inside array brackets",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/array-bracket-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "never"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
singleValue: {
|
||||
type: "boolean"
|
||||
},
|
||||
objectsInArrays: {
|
||||
type: "boolean"
|
||||
},
|
||||
arraysInArrays: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedSpaceAfter: "There should be no space after '{{tokenValue}}'.",
|
||||
unexpectedSpaceBefore: "There should be no space before '{{tokenValue}}'.",
|
||||
missingSpaceAfter: "A space is required after '{{tokenValue}}'.",
|
||||
missingSpaceBefore: "A space is required before '{{tokenValue}}'."
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
const spaced = context.options[0] === "always",
|
||||
sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const nextToken = sourceCode.getTokenAfter(token);
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: { start: token.loc.end, end: nextToken.loc.start },
|
||||
messageId: "unexpectedSpaceAfter",
|
||||
data: {
|
||||
tokenValue: token.value
|
||||
},
|
||||
fix(fixer) {
|
||||
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) {
|
||||
const previousToken = sourceCode.getTokenBefore(token);
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: { start: previousToken.loc.end, end: token.loc.start },
|
||||
messageId: "unexpectedSpaceBefore",
|
||||
data: {
|
||||
tokenValue: token.value
|
||||
},
|
||||
fix(fixer) {
|
||||
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,
|
||||
messageId: "missingSpaceAfter",
|
||||
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,
|
||||
messageId: "missingSpaceBefore",
|
||||
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
|
||||
};
|
||||
}
|
||||
};
|
||||
+446
@@ -0,0 +1,446 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce return statements in callbacks of array's methods
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u;
|
||||
const TARGET_METHODS = /^(?:every|filter|find(?:Last)?(?:Index)?|flatMap|forEach|map|reduce(?:Right)?|some|sort|toSorted)$/u;
|
||||
|
||||
/**
|
||||
* Checks a given node is a member access which has the specified name's
|
||||
* property.
|
||||
* @param {ASTNode} node A node to check.
|
||||
* @returns {boolean} `true` if the node is a member access which has
|
||||
* the specified name's property. The node may be a `(Chain|Member)Expression` node.
|
||||
*/
|
||||
function isTargetMethod(node) {
|
||||
return astUtils.isSpecificMemberAccess(node, null, TARGET_METHODS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks all segments in a set and returns true if any are reachable.
|
||||
* @param {Set<CodePathSegment>} segments The segments to check.
|
||||
* @returns {boolean} True if any segment is reachable; false otherwise.
|
||||
*/
|
||||
function isAnySegmentReachable(segments) {
|
||||
|
||||
for (const segment of segments) {
|
||||
if (segment.reachable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-legible description of an array method
|
||||
* @param {string} arrayMethodName A method name to fully qualify
|
||||
* @returns {string} the method name prefixed with `Array.` if it is a class method,
|
||||
* or else `Array.prototype.` if it is an instance method.
|
||||
*/
|
||||
function fullMethodName(arrayMethodName) {
|
||||
if (["from", "of", "isArray"].includes(arrayMethodName)) {
|
||||
return "Array.".concat(arrayMethodName);
|
||||
}
|
||||
return "Array.prototype.".concat(arrayMethodName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is a function expression which is the
|
||||
* callback of an array method, returning the method name.
|
||||
* @param {ASTNode} node A node to check. This is one of
|
||||
* FunctionExpression or ArrowFunctionExpression.
|
||||
* @returns {string} The method name if the node is a callback method,
|
||||
* null otherwise.
|
||||
*/
|
||||
function getArrayMethodName(node) {
|
||||
let currentNode = node;
|
||||
|
||||
while (currentNode) {
|
||||
const parent = currentNode.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
|
||||
/*
|
||||
* Looks up the destination. e.g.,
|
||||
* foo.every(nativeFoo || function foo() { ... });
|
||||
*/
|
||||
case "LogicalExpression":
|
||||
case "ConditionalExpression":
|
||||
case "ChainExpression":
|
||||
currentNode = 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 null;
|
||||
}
|
||||
currentNode = func.parent;
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* e.g.
|
||||
* Array.from([], function() {});
|
||||
* list.every(function() {});
|
||||
*/
|
||||
case "CallExpression":
|
||||
if (astUtils.isArrayFromMethod(parent.callee)) {
|
||||
if (
|
||||
parent.arguments.length >= 2 &&
|
||||
parent.arguments[1] === currentNode
|
||||
) {
|
||||
return "from";
|
||||
}
|
||||
}
|
||||
if (isTargetMethod(parent.callee)) {
|
||||
if (
|
||||
parent.arguments.length >= 1 &&
|
||||
parent.arguments[0] === currentNode
|
||||
) {
|
||||
return astUtils.getStaticPropertyName(parent.callee);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
// Otherwise this node is not target.
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* c8 ignore next */
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given node is a void expression.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} - `true` if the node is a void expression
|
||||
*/
|
||||
function isExpressionVoid(node) {
|
||||
return node.type === "UnaryExpression" && node.operator === "void";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes the linting error by prepending "void " to the given node
|
||||
* @param {Object} sourceCode context given by context.sourceCode
|
||||
* @param {ASTNode} node The node to fix.
|
||||
* @param {Object} fixer The fixer object provided by ESLint.
|
||||
* @returns {Array<Object>} - An array of fix objects to apply to the node.
|
||||
*/
|
||||
function voidPrependFixer(sourceCode, node, fixer) {
|
||||
|
||||
const requiresParens =
|
||||
|
||||
// prepending `void ` will fail if the node has a lower precedence than void
|
||||
astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression", operator: "void" }) &&
|
||||
|
||||
// check if there are parentheses around the node to avoid redundant parentheses
|
||||
!astUtils.isParenthesised(sourceCode, node);
|
||||
|
||||
// avoid parentheses issues
|
||||
const returnOrArrowToken = sourceCode.getTokenBefore(
|
||||
node,
|
||||
node.parent.type === "ArrowFunctionExpression"
|
||||
? astUtils.isArrowToken
|
||||
|
||||
// isReturnToken
|
||||
: token => token.type === "Keyword" && token.value === "return"
|
||||
);
|
||||
|
||||
const firstToken = sourceCode.getTokenAfter(returnOrArrowToken);
|
||||
|
||||
const prependSpace =
|
||||
|
||||
// is return token, as => allows void to be adjacent
|
||||
returnOrArrowToken.value === "return" &&
|
||||
|
||||
// If two tokens (return and "(") are adjacent
|
||||
returnOrArrowToken.range[1] === firstToken.range[0];
|
||||
|
||||
return [
|
||||
fixer.insertTextBefore(firstToken, `${prependSpace ? " " : ""}void ${requiresParens ? "(" : ""}`),
|
||||
fixer.insertTextAfter(node, requiresParens ? ")" : "")
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes the linting error by `wrapping {}` around the given node's body.
|
||||
* @param {Object} sourceCode context given by context.sourceCode
|
||||
* @param {ASTNode} node The node to fix.
|
||||
* @param {Object} fixer The fixer object provided by ESLint.
|
||||
* @returns {Array<Object>} - An array of fix objects to apply to the node.
|
||||
*/
|
||||
function curlyWrapFixer(sourceCode, node, fixer) {
|
||||
const arrowToken = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken);
|
||||
const firstToken = sourceCode.getTokenAfter(arrowToken);
|
||||
const lastToken = sourceCode.getLastToken(node);
|
||||
|
||||
return [
|
||||
fixer.insertTextBefore(firstToken, "{"),
|
||||
fixer.insertTextAfter(lastToken, "}")
|
||||
];
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Enforce `return` statements in callbacks of array methods",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/array-callback-return"
|
||||
},
|
||||
|
||||
// eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- false positive
|
||||
hasSuggestions: true,
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowImplicit: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
checkForEach: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
allowVoid: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
expectedAtEnd: "{{arrayMethodName}}() expects a value to be returned at the end of {{name}}.",
|
||||
expectedInside: "{{arrayMethodName}}() expects a return value from {{name}}.",
|
||||
expectedReturnValue: "{{arrayMethodName}}() expects a return value from {{name}}.",
|
||||
expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}.",
|
||||
wrapBraces: "Wrap the expression in `{}`.",
|
||||
prependVoid: "Prepend `void` to the expression."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const options = context.options[0] || { allowImplicit: false, checkForEach: false, allowVoid: false };
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
let funcInfo = {
|
||||
arrayMethodName: null,
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageAndSuggestions = { messageId: "", suggest: [] };
|
||||
|
||||
if (funcInfo.arrayMethodName === "forEach") {
|
||||
if (options.checkForEach && node.type === "ArrowFunctionExpression" && node.expression) {
|
||||
|
||||
if (options.allowVoid) {
|
||||
if (isExpressionVoid(node.body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
messageAndSuggestions.messageId = "expectedNoReturnValue";
|
||||
messageAndSuggestions.suggest = [
|
||||
{
|
||||
messageId: "wrapBraces",
|
||||
fix(fixer) {
|
||||
return curlyWrapFixer(sourceCode, node, fixer);
|
||||
}
|
||||
},
|
||||
{
|
||||
messageId: "prependVoid",
|
||||
fix(fixer) {
|
||||
return voidPrependFixer(sourceCode, node.body, fixer);
|
||||
}
|
||||
}
|
||||
];
|
||||
} else {
|
||||
messageAndSuggestions.messageId = "expectedNoReturnValue";
|
||||
messageAndSuggestions.suggest = [{
|
||||
messageId: "wrapBraces",
|
||||
fix(fixer) {
|
||||
return curlyWrapFixer(sourceCode, node, fixer);
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (node.body.type === "BlockStatement" && isAnySegmentReachable(funcInfo.currentSegments)) {
|
||||
messageAndSuggestions.messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside";
|
||||
}
|
||||
}
|
||||
|
||||
if (messageAndSuggestions.messageId) {
|
||||
const name = astUtils.getFunctionNameWithKind(node);
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
|
||||
messageId: messageAndSuggestions.messageId,
|
||||
data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) },
|
||||
suggest: messageAndSuggestions.suggest.length !== 0 ? messageAndSuggestions.suggest : null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
// Stacks this function's information.
|
||||
onCodePathStart(codePath, node) {
|
||||
|
||||
let methodName = null;
|
||||
|
||||
if (TARGET_NODE_TYPE.test(node.type)) {
|
||||
methodName = getArrayMethodName(node);
|
||||
}
|
||||
|
||||
funcInfo = {
|
||||
arrayMethodName: methodName,
|
||||
upper: funcInfo,
|
||||
codePath,
|
||||
hasReturn: false,
|
||||
shouldCheck:
|
||||
methodName &&
|
||||
!node.async &&
|
||||
!node.generator,
|
||||
node,
|
||||
currentSegments: new Set()
|
||||
};
|
||||
},
|
||||
|
||||
// Pops this function's information.
|
||||
onCodePathEnd() {
|
||||
funcInfo = funcInfo.upper;
|
||||
},
|
||||
|
||||
onUnreachableCodePathSegmentStart(segment) {
|
||||
funcInfo.currentSegments.add(segment);
|
||||
},
|
||||
|
||||
onUnreachableCodePathSegmentEnd(segment) {
|
||||
funcInfo.currentSegments.delete(segment);
|
||||
},
|
||||
|
||||
onCodePathSegmentStart(segment) {
|
||||
funcInfo.currentSegments.add(segment);
|
||||
},
|
||||
|
||||
onCodePathSegmentEnd(segment) {
|
||||
funcInfo.currentSegments.delete(segment);
|
||||
},
|
||||
|
||||
|
||||
// Checks the return statement is valid.
|
||||
ReturnStatement(node) {
|
||||
|
||||
if (!funcInfo.shouldCheck) {
|
||||
return;
|
||||
}
|
||||
|
||||
funcInfo.hasReturn = true;
|
||||
|
||||
const messageAndSuggestions = { messageId: "", suggest: [] };
|
||||
|
||||
if (funcInfo.arrayMethodName === "forEach") {
|
||||
|
||||
// if checkForEach: true, returning a value at any path inside a forEach is not allowed
|
||||
if (options.checkForEach && node.argument) {
|
||||
|
||||
if (options.allowVoid) {
|
||||
if (isExpressionVoid(node.argument)) {
|
||||
return;
|
||||
}
|
||||
|
||||
messageAndSuggestions.messageId = "expectedNoReturnValue";
|
||||
messageAndSuggestions.suggest = [{
|
||||
messageId: "prependVoid",
|
||||
fix(fixer) {
|
||||
return voidPrependFixer(sourceCode, node.argument, fixer);
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
messageAndSuggestions.messageId = "expectedNoReturnValue";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// if allowImplicit: false, should also check node.argument
|
||||
if (!options.allowImplicit && !node.argument) {
|
||||
messageAndSuggestions.messageId = "expectedReturnValue";
|
||||
}
|
||||
}
|
||||
|
||||
if (messageAndSuggestions.messageId) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: messageAndSuggestions.messageId,
|
||||
data: {
|
||||
name: astUtils.getFunctionNameWithKind(funcInfo.node),
|
||||
arrayMethodName: fullMethodName(funcInfo.arrayMethodName)
|
||||
},
|
||||
suggest: messageAndSuggestions.suggest.length !== 0 ? messageAndSuggestions.suggest : null
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Reports a given function if the last path is reachable.
|
||||
"FunctionExpression:exit": checkLastSegment,
|
||||
"ArrowFunctionExpression:exit": checkLastSegment
|
||||
};
|
||||
}
|
||||
};
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce line breaks after each array element
|
||||
* @author Jan Peer Stöcklmair <https://github.com/JPeer264>
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce line breaks after each array element",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/array-element-newline"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: {
|
||||
definitions: {
|
||||
basicConfig: {
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["always", "never", "consistent"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
multiline: {
|
||||
type: "boolean"
|
||||
},
|
||||
minItems: {
|
||||
type: ["integer", "null"],
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
$ref: "#/definitions/basicConfig"
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
ArrayExpression: {
|
||||
$ref: "#/definitions/basicConfig"
|
||||
},
|
||||
ArrayPattern: {
|
||||
$ref: "#/definitions/basicConfig"
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
minProperties: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
messages: {
|
||||
unexpectedLineBreak: "There should be no linebreak here.",
|
||||
missingLineBreak: "There should be a linebreak after this element."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Helpers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Normalizes a given option value.
|
||||
* @param {string|Object|undefined} providedOption An option value to parse.
|
||||
* @returns {{multiline: boolean, minItems: number}} Normalized option object.
|
||||
*/
|
||||
function normalizeOptionValue(providedOption) {
|
||||
let consistent = false;
|
||||
let multiline = false;
|
||||
let minItems;
|
||||
|
||||
const option = providedOption || "always";
|
||||
|
||||
if (!option || option === "always" || option.minItems === 0) {
|
||||
minItems = 0;
|
||||
} else if (option === "never") {
|
||||
minItems = Number.POSITIVE_INFINITY;
|
||||
} else if (option === "consistent") {
|
||||
consistent = true;
|
||||
minItems = Number.POSITIVE_INFINITY;
|
||||
} else {
|
||||
multiline = Boolean(option.multiline);
|
||||
minItems = option.minItems || Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
return { consistent, multiline, minItems };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a given option value.
|
||||
* @param {string|Object|undefined} options An option value to parse.
|
||||
* @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
|
||||
*/
|
||||
function normalizeOptions(options) {
|
||||
if (options && (options.ArrayExpression || options.ArrayPattern)) {
|
||||
let expressionOptions, patternOptions;
|
||||
|
||||
if (options.ArrayExpression) {
|
||||
expressionOptions = normalizeOptionValue(options.ArrayExpression);
|
||||
}
|
||||
|
||||
if (options.ArrayPattern) {
|
||||
patternOptions = normalizeOptionValue(options.ArrayPattern);
|
||||
}
|
||||
|
||||
return { ArrayExpression: expressionOptions, ArrayPattern: patternOptions };
|
||||
}
|
||||
|
||||
const value = normalizeOptionValue(options);
|
||||
|
||||
return { ArrayExpression: value, ArrayPattern: value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a line break after the first token
|
||||
* @param {Token} token The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportNoLineBreak(token) {
|
||||
const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true });
|
||||
|
||||
context.report({
|
||||
loc: {
|
||||
start: tokenBefore.loc.end,
|
||||
end: token.loc.start
|
||||
},
|
||||
messageId: "unexpectedLineBreak",
|
||||
fix(fixer) {
|
||||
if (astUtils.isCommentToken(tokenBefore)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!astUtils.isTokenOnSameLine(tokenBefore, token)) {
|
||||
return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], " ");
|
||||
}
|
||||
|
||||
/*
|
||||
* This will check if the comma is on the same line as the next element
|
||||
* Following array:
|
||||
* [
|
||||
* 1
|
||||
* , 2
|
||||
* , 3
|
||||
* ]
|
||||
*
|
||||
* will be fixed to:
|
||||
* [
|
||||
* 1, 2, 3
|
||||
* ]
|
||||
*/
|
||||
const twoTokensBefore = sourceCode.getTokenBefore(tokenBefore, { includeComments: true });
|
||||
|
||||
if (astUtils.isCommentToken(twoTokensBefore)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fixer.replaceTextRange([twoTokensBefore.range[1], tokenBefore.range[0]], "");
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there should be a line break after the first token
|
||||
* @param {Token} token The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportRequiredLineBreak(token) {
|
||||
const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true });
|
||||
|
||||
context.report({
|
||||
loc: {
|
||||
start: tokenBefore.loc.end,
|
||||
end: token.loc.start
|
||||
},
|
||||
messageId: "missingLineBreak",
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given node if it violated this rule.
|
||||
* @param {ASTNode} node A node to check. This is an ObjectExpression node or an ObjectPattern node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function check(node) {
|
||||
const elements = node.elements;
|
||||
const normalizedOptions = normalizeOptions(context.options[0]);
|
||||
const options = normalizedOptions[node.type];
|
||||
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
|
||||
let elementBreak = false;
|
||||
|
||||
/*
|
||||
* MULTILINE: true
|
||||
* loop through every element and check
|
||||
* if at least one element has linebreaks inside
|
||||
* this ensures that following is not valid (due to elements are on the same line):
|
||||
*
|
||||
* [
|
||||
* 1,
|
||||
* 2,
|
||||
* 3
|
||||
* ]
|
||||
*/
|
||||
if (options.multiline) {
|
||||
elementBreak = elements
|
||||
.filter(element => element !== null)
|
||||
.some(element => element.loc.start.line !== element.loc.end.line);
|
||||
}
|
||||
|
||||
let linebreaksCount = 0;
|
||||
|
||||
for (let i = 0; i < node.elements.length; i++) {
|
||||
const element = node.elements[i];
|
||||
|
||||
const previousElement = elements[i - 1];
|
||||
|
||||
if (i === 0 || element === null || previousElement === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken);
|
||||
const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken);
|
||||
const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken);
|
||||
|
||||
if (!astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) {
|
||||
linebreaksCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const needsLinebreaks = (
|
||||
elements.length >= options.minItems ||
|
||||
(
|
||||
options.multiline &&
|
||||
elementBreak
|
||||
) ||
|
||||
(
|
||||
options.consistent &&
|
||||
linebreaksCount > 0 &&
|
||||
linebreaksCount < node.elements.length
|
||||
)
|
||||
);
|
||||
|
||||
elements.forEach((element, i) => {
|
||||
const previousElement = elements[i - 1];
|
||||
|
||||
if (i === 0 || element === null || previousElement === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken);
|
||||
const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken);
|
||||
const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken);
|
||||
|
||||
if (needsLinebreaks) {
|
||||
if (astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) {
|
||||
reportRequiredLineBreak(firstTokenOfCurrentElement);
|
||||
}
|
||||
} else {
|
||||
if (!astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) {
|
||||
reportNoLineBreak(firstTokenOfCurrentElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
ArrayPattern: check,
|
||||
ArrayExpression: check
|
||||
};
|
||||
}
|
||||
};
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* @fileoverview Rule to require braces in arrow function body.
|
||||
* @author Alberto Rodríguez
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require braces around arrow function bodies",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/arrow-body-style"
|
||||
},
|
||||
|
||||
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",
|
||||
|
||||
messages: {
|
||||
unexpectedOtherBlock: "Unexpected block statement surrounding arrow body.",
|
||||
unexpectedEmptyBlock: "Unexpected block statement surrounding arrow body; put a value of `undefined` immediately after the `=>`.",
|
||||
unexpectedObjectBlock: "Unexpected block statement surrounding arrow body; parenthesize the returned value and move it immediately after the `=>`.",
|
||||
unexpectedSingleBlock: "Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`.",
|
||||
expectedBlock: "Expected block statement surrounding arrow body."
|
||||
}
|
||||
},
|
||||
|
||||
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.sourceCode;
|
||||
let funcInfo = null;
|
||||
|
||||
/**
|
||||
* Checks whether the given node has ASI problem or not.
|
||||
* @param {Token} token The token to check.
|
||||
* @returns {boolean} `true` if it changes semantics if `;` or `}` followed by the token are removed.
|
||||
*/
|
||||
function hasASIProblem(token) {
|
||||
return token && token.type === "Punctuator" && /^[([/`+-]/u.test(token.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the closing parenthesis by the given node.
|
||||
* @param {ASTNode} node first node after an opening parenthesis.
|
||||
* @returns {Token} The found closing parenthesis token.
|
||||
*/
|
||||
function findClosingParen(node) {
|
||||
let nodeToCheck = node;
|
||||
|
||||
while (!astUtils.isParenthesised(sourceCode, nodeToCheck)) {
|
||||
nodeToCheck = nodeToCheck.parent;
|
||||
}
|
||||
return sourceCode.getTokenAfter(nodeToCheck);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the node is inside of a for loop's init
|
||||
* @param {ASTNode} node node is inside for loop
|
||||
* @returns {boolean} `true` if the node is inside of a for loop, else `false`
|
||||
*/
|
||||
function isInsideForLoopInitializer(node) {
|
||||
if (node && node.parent) {
|
||||
if (node.parent.type === "ForStatement" && node.parent.init === node) {
|
||||
return true;
|
||||
}
|
||||
return isInsideForLoopInitializer(node.parent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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") {
|
||||
let messageId;
|
||||
|
||||
if (blockBody.length === 0) {
|
||||
messageId = "unexpectedEmptyBlock";
|
||||
} else if (blockBody.length > 1) {
|
||||
messageId = "unexpectedOtherBlock";
|
||||
} else if (blockBody[0].argument === null) {
|
||||
messageId = "unexpectedSingleBlock";
|
||||
} else if (astUtils.isOpeningBraceToken(sourceCode.getFirstToken(blockBody[0], { skip: 1 }))) {
|
||||
messageId = "unexpectedObjectBlock";
|
||||
} else {
|
||||
messageId = "unexpectedSingleBlock";
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: arrowBody.loc,
|
||||
messageId,
|
||||
fix(fixer) {
|
||||
const fixes = [];
|
||||
|
||||
if (blockBody.length !== 1 ||
|
||||
blockBody[0].type !== "ReturnStatement" ||
|
||||
!blockBody[0].argument ||
|
||||
hasASIProblem(sourceCode.getTokenAfter(arrowBody))
|
||||
) {
|
||||
return fixes;
|
||||
}
|
||||
|
||||
const openingBrace = sourceCode.getFirstToken(arrowBody);
|
||||
const closingBrace = sourceCode.getLastToken(arrowBody);
|
||||
const firstValueToken = sourceCode.getFirstToken(blockBody[0], 1);
|
||||
const lastValueToken = sourceCode.getLastToken(blockBody[0]);
|
||||
const commentsExist =
|
||||
sourceCode.commentsExistBetween(openingBrace, firstValueToken) ||
|
||||
sourceCode.commentsExistBetween(lastValueToken, closingBrace);
|
||||
|
||||
/*
|
||||
* Remove tokens around the return value.
|
||||
* If comments don't exist, remove extra spaces as well.
|
||||
*/
|
||||
if (commentsExist) {
|
||||
fixes.push(
|
||||
fixer.remove(openingBrace),
|
||||
fixer.remove(closingBrace),
|
||||
fixer.remove(sourceCode.getTokenAfter(openingBrace)) // return keyword
|
||||
);
|
||||
} else {
|
||||
fixes.push(
|
||||
fixer.removeRange([openingBrace.range[0], firstValueToken.range[0]]),
|
||||
fixer.removeRange([lastValueToken.range[1], closingBrace.range[1]])
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* If the first token of the return value is `{` or the return value is a sequence expression,
|
||||
* enclose the return value by parentheses to avoid syntax error.
|
||||
*/
|
||||
if (astUtils.isOpeningBraceToken(firstValueToken) || blockBody[0].argument.type === "SequenceExpression" || (funcInfo.hasInOperator && isInsideForLoopInitializer(node))) {
|
||||
if (!astUtils.isParenthesised(sourceCode, blockBody[0].argument)) {
|
||||
fixes.push(
|
||||
fixer.insertTextBefore(firstValueToken, "("),
|
||||
fixer.insertTextAfter(lastValueToken, ")")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If the last token of the return statement is semicolon, remove it.
|
||||
* Non-block arrow body is an expression, not a statement.
|
||||
*/
|
||||
if (astUtils.isSemicolonToken(lastValueToken)) {
|
||||
fixes.push(fixer.remove(lastValueToken));
|
||||
}
|
||||
|
||||
return fixes;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (always || (asNeeded && requireReturnForObjectLiteral && arrowBody.type === "ObjectExpression")) {
|
||||
context.report({
|
||||
node,
|
||||
loc: arrowBody.loc,
|
||||
messageId: "expectedBlock",
|
||||
fix(fixer) {
|
||||
const fixes = [];
|
||||
const arrowToken = sourceCode.getTokenBefore(arrowBody, astUtils.isArrowToken);
|
||||
const [firstTokenAfterArrow, secondTokenAfterArrow] = sourceCode.getTokensAfter(arrowToken, { count: 2 });
|
||||
const lastToken = sourceCode.getLastToken(node);
|
||||
|
||||
let parenthesisedObjectLiteral = null;
|
||||
|
||||
if (
|
||||
astUtils.isOpeningParenToken(firstTokenAfterArrow) &&
|
||||
astUtils.isOpeningBraceToken(secondTokenAfterArrow)
|
||||
) {
|
||||
const braceNode = sourceCode.getNodeByRangeIndex(secondTokenAfterArrow.range[0]);
|
||||
|
||||
if (braceNode.type === "ObjectExpression") {
|
||||
parenthesisedObjectLiteral = braceNode;
|
||||
}
|
||||
}
|
||||
|
||||
// If the value is object literal, remove parentheses which were forced by syntax.
|
||||
if (parenthesisedObjectLiteral) {
|
||||
const openingParenToken = firstTokenAfterArrow;
|
||||
const openingBraceToken = secondTokenAfterArrow;
|
||||
|
||||
if (astUtils.isTokenOnSameLine(openingParenToken, openingBraceToken)) {
|
||||
fixes.push(fixer.replaceText(openingParenToken, "{return "));
|
||||
} else {
|
||||
|
||||
// Avoid ASI
|
||||
fixes.push(
|
||||
fixer.replaceText(openingParenToken, "{"),
|
||||
fixer.insertTextBefore(openingBraceToken, "return ")
|
||||
);
|
||||
}
|
||||
|
||||
// Closing paren for the object doesn't have to be lastToken, e.g.: () => ({}).foo()
|
||||
fixes.push(fixer.remove(findClosingParen(parenthesisedObjectLiteral)));
|
||||
fixes.push(fixer.insertTextAfter(lastToken, "}"));
|
||||
|
||||
} else {
|
||||
fixes.push(fixer.insertTextBefore(firstTokenAfterArrow, "{return "));
|
||||
fixes.push(fixer.insertTextAfter(lastToken, "}"));
|
||||
}
|
||||
|
||||
return fixes;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"BinaryExpression[operator='in']"() {
|
||||
let info = funcInfo;
|
||||
|
||||
while (info) {
|
||||
info.hasInOperator = true;
|
||||
info = info.upper;
|
||||
}
|
||||
},
|
||||
ArrowFunctionExpression() {
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
hasInOperator: false
|
||||
};
|
||||
},
|
||||
"ArrowFunctionExpression:exit"(node) {
|
||||
validate(node);
|
||||
funcInfo = funcInfo.upper;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* @fileoverview Rule to require parens in arrow function arguments.
|
||||
* @author Jxck
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines if the given arrow function has block body.
|
||||
* @param {ASTNode} node `ArrowFunctionExpression` node.
|
||||
* @returns {boolean} `true` if the function has block body.
|
||||
*/
|
||||
function hasBlockBody(node) {
|
||||
return node.body.type === "BlockStatement";
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Require parentheses around arrow function arguments",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/arrow-parens"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "as-needed"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
requireForBlockBody: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedParens: "Unexpected parentheses around single function argument.",
|
||||
expectedParens: "Expected parentheses around arrow function argument.",
|
||||
|
||||
unexpectedParensInline: "Unexpected parentheses around single function argument having a body with no curly braces.",
|
||||
expectedParensBlock: "Expected parentheses around arrow function argument having a body with curly braces."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const asNeeded = context.options[0] === "as-needed";
|
||||
const requireForBlockBody = asNeeded && context.options[1] && context.options[1].requireForBlockBody === true;
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Finds opening paren of parameters for the given arrow function, if it exists.
|
||||
* It is assumed that the given arrow function has exactly one parameter.
|
||||
* @param {ASTNode} node `ArrowFunctionExpression` node.
|
||||
* @returns {Token|null} the opening paren, or `null` if the given arrow function doesn't have parens of parameters.
|
||||
*/
|
||||
function findOpeningParenOfParams(node) {
|
||||
const tokenBeforeParams = sourceCode.getTokenBefore(node.params[0]);
|
||||
|
||||
if (
|
||||
tokenBeforeParams &&
|
||||
astUtils.isOpeningParenToken(tokenBeforeParams) &&
|
||||
node.range[0] <= tokenBeforeParams.range[0]
|
||||
) {
|
||||
return tokenBeforeParams;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds closing paren of parameters for the given arrow function.
|
||||
* It is assumed that the given arrow function has parens of parameters and that it has exactly one parameter.
|
||||
* @param {ASTNode} node `ArrowFunctionExpression` node.
|
||||
* @returns {Token} the closing paren of parameters.
|
||||
*/
|
||||
function getClosingParenOfParams(node) {
|
||||
return sourceCode.getTokenAfter(node.params[0], astUtils.isClosingParenToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given arrow function has comments inside parens of parameters.
|
||||
* It is assumed that the given arrow function has parens of parameters.
|
||||
* @param {ASTNode} node `ArrowFunctionExpression` node.
|
||||
* @param {Token} openingParen Opening paren of parameters.
|
||||
* @returns {boolean} `true` if the function has at least one comment inside of parens of parameters.
|
||||
*/
|
||||
function hasCommentsInParensOfParams(node, openingParen) {
|
||||
return sourceCode.commentsExistBetween(openingParen, getClosingParenOfParams(node));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given arrow function has unexpected tokens before opening paren of parameters,
|
||||
* in which case it will be assumed that the existing parens of parameters are necessary.
|
||||
* Only tokens within the range of the arrow function (tokens that are part of the arrow function) are taken into account.
|
||||
* Example: <T>(a) => b
|
||||
* @param {ASTNode} node `ArrowFunctionExpression` node.
|
||||
* @param {Token} openingParen Opening paren of parameters.
|
||||
* @returns {boolean} `true` if the function has at least one unexpected token.
|
||||
*/
|
||||
function hasUnexpectedTokensBeforeOpeningParen(node, openingParen) {
|
||||
const expectedCount = node.async ? 1 : 0;
|
||||
|
||||
return sourceCode.getFirstToken(node, { skip: expectedCount }) !== openingParen;
|
||||
}
|
||||
|
||||
return {
|
||||
"ArrowFunctionExpression[params.length=1]"(node) {
|
||||
const shouldHaveParens = !asNeeded || requireForBlockBody && hasBlockBody(node);
|
||||
const openingParen = findOpeningParenOfParams(node);
|
||||
const hasParens = openingParen !== null;
|
||||
const [param] = node.params;
|
||||
|
||||
if (shouldHaveParens && !hasParens) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: requireForBlockBody ? "expectedParensBlock" : "expectedParens",
|
||||
loc: param.loc,
|
||||
*fix(fixer) {
|
||||
yield fixer.insertTextBefore(param, "(");
|
||||
yield fixer.insertTextAfter(param, ")");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!shouldHaveParens &&
|
||||
hasParens &&
|
||||
param.type === "Identifier" &&
|
||||
!param.typeAnnotation &&
|
||||
!node.returnType &&
|
||||
!hasCommentsInParensOfParams(node, openingParen) &&
|
||||
!hasUnexpectedTokensBeforeOpeningParen(node, openingParen)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: requireForBlockBody ? "unexpectedParensInline" : "unexpectedParens",
|
||||
loc: param.loc,
|
||||
*fix(fixer) {
|
||||
const tokenBeforeOpeningParen = sourceCode.getTokenBefore(openingParen);
|
||||
const closingParen = getClosingParenOfParams(node);
|
||||
|
||||
if (
|
||||
tokenBeforeOpeningParen &&
|
||||
tokenBeforeOpeningParen.range[1] === openingParen.range[0] &&
|
||||
!astUtils.canTokensBeAdjacent(tokenBeforeOpeningParen, sourceCode.getFirstToken(param))
|
||||
) {
|
||||
yield fixer.insertTextBefore(openingParen, " ");
|
||||
}
|
||||
|
||||
// remove parens, whitespace inside parens, and possible trailing comma
|
||||
yield fixer.removeRange([openingParen.range[0], param.range[0]]);
|
||||
yield fixer.removeRange([param.range[1], closingParen.range[1]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* @fileoverview Rule to define spacing before/after arrow function's arrow.
|
||||
* @author Jxck
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent spacing before and after the arrow in arrow functions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/arrow-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
before: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
},
|
||||
after: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
expectedBefore: "Missing space before =>.",
|
||||
unexpectedBefore: "Unexpected space before =>.",
|
||||
|
||||
expectedAfter: "Missing space after =>.",
|
||||
unexpectedAfter: "Unexpected space after =>."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
// merge rules with default
|
||||
const rule = Object.assign({}, context.options[0]);
|
||||
|
||||
rule.before = rule.before !== false;
|
||||
rule.after = rule.after !== false;
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
messageId: "expectedBefore",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(tokens.arrow, " ");
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
// should be no space before arrow
|
||||
if (countSpace.before > 0) {
|
||||
context.report({
|
||||
node: tokens.before,
|
||||
messageId: "unexpectedBefore",
|
||||
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,
|
||||
messageId: "expectedAfter",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(tokens.arrow, " ");
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
// should be no space after arrow
|
||||
if (countSpace.after > 0) {
|
||||
context.report({
|
||||
node: tokens.after,
|
||||
messageId: "unexpectedAfter",
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([tokens.arrow.range[1], tokens.after.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ArrowFunctionExpression: spaces
|
||||
};
|
||||
}
|
||||
};
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @fileoverview Rule to check for "block scoped" variables by binding context
|
||||
* @author Matt DuVall <http://www.mattduvall.com>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce the use of variables within the scope they are defined",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/block-scoped-var"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
outOfScope: "'{{name}}' declared on line {{definitionLine}} column {{definitionColumn}} is used outside of binding context."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
let stack = [];
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* 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 {eslint-scope.Reference} reference A reference to report.
|
||||
* @param {eslint-scope.Definition} definition A definition for which to report reference.
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(reference, definition) {
|
||||
const identifier = reference.identifier;
|
||||
const definitionPosition = definition.name.loc.start;
|
||||
|
||||
context.report({
|
||||
node: identifier,
|
||||
messageId: "outOfScope",
|
||||
data: {
|
||||
name: identifier.name,
|
||||
definitionLine: definitionPosition.line,
|
||||
definitionColumn: definitionPosition.column + 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = sourceCode.getDeclaredVariables(node);
|
||||
|
||||
for (let i = 0; i < variables.length; ++i) {
|
||||
|
||||
// Reports.
|
||||
variables[i]
|
||||
.references
|
||||
.filter(isOutsideOfScope)
|
||||
.forEach(ref => report(ref, variables[i].defs.find(def => def.parent === node)));
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
StaticBlock: enterScope,
|
||||
"StaticBlock:exit": exitScope,
|
||||
|
||||
// Finds and reports references which are outside of valid scope.
|
||||
VariableDeclaration: checkForVariables
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @fileoverview A rule to disallow or enforce spaces inside of single line blocks.
|
||||
* @author Toru Nagashima
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const util = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Disallow or enforce spaces inside of blocks after opening block and before closing block",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/block-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{ enum: ["always", "never"] }
|
||||
],
|
||||
|
||||
messages: {
|
||||
missing: "Requires a space {{location}} '{{token}}'.",
|
||||
extra: "Unexpected space(s) {{location}} '{{token}}'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const always = (context.options[0] !== "never"),
|
||||
messageId = always ? "missing" : "extra",
|
||||
sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Gets the open brace token from a given node.
|
||||
* @param {ASTNode} node A BlockStatement/StaticBlock/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);
|
||||
}
|
||||
|
||||
if (node.type === "StaticBlock") {
|
||||
return sourceCode.getFirstToken(node, { skip: 1 }); // skip the `static` token
|
||||
}
|
||||
|
||||
// "BlockStatement"
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and reports invalid spacing style inside braces.
|
||||
* @param {ASTNode} node A BlockStatement/StaticBlock/SwitchStatement node to check.
|
||||
* @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)) {
|
||||
let loc = openBrace.loc;
|
||||
|
||||
if (messageId === "extra") {
|
||||
loc = {
|
||||
start: openBrace.loc.end,
|
||||
end: firstToken.loc.start
|
||||
};
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc,
|
||||
messageId,
|
||||
data: {
|
||||
location: "after",
|
||||
token: openBrace.value
|
||||
},
|
||||
fix(fixer) {
|
||||
if (always) {
|
||||
return fixer.insertTextBefore(firstToken, " ");
|
||||
}
|
||||
|
||||
return fixer.removeRange([openBrace.range[1], firstToken.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!isValid(lastToken, closeBrace)) {
|
||||
let loc = closeBrace.loc;
|
||||
|
||||
if (messageId === "extra") {
|
||||
loc = {
|
||||
start: lastToken.loc.end,
|
||||
end: closeBrace.loc.start
|
||||
};
|
||||
}
|
||||
context.report({
|
||||
node,
|
||||
loc,
|
||||
messageId,
|
||||
data: {
|
||||
location: "before",
|
||||
token: closeBrace.value
|
||||
},
|
||||
fix(fixer) {
|
||||
if (always) {
|
||||
return fixer.insertTextAfter(lastToken, " ");
|
||||
}
|
||||
|
||||
return fixer.removeRange([lastToken.range[1], closeBrace.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
BlockStatement: checkSpacingInsideBraces,
|
||||
StaticBlock: checkSpacingInsideBraces,
|
||||
SwitchStatement: checkSpacingInsideBraces
|
||||
};
|
||||
}
|
||||
};
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag block statements that do not use the one true brace style
|
||||
* @author Ian Christian Myers
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent brace style for blocks",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/brace-style"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["1tbs", "stroustrup", "allman"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowSingleLine: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
messages: {
|
||||
nextLineOpen: "Opening curly brace does not appear on the same line as controlling statement.",
|
||||
sameLineOpen: "Opening curly brace appears on the same line as controlling statement.",
|
||||
blockSameLine: "Statement inside of curly braces should be on next line.",
|
||||
nextLineClose: "Closing curly brace does not appear on the same line as the subsequent block.",
|
||||
singleLineClose: "Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.",
|
||||
sameLineClose: "Closing curly brace appears on the same line as the subsequent block."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const style = context.options[0] || "1tbs",
|
||||
params = context.options[1] || {},
|
||||
sourceCode = context.sourceCode;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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]);
|
||||
|
||||
// Don't do a fix if there is a comment between the tokens
|
||||
if (textBetween.trim()) {
|
||||
return null;
|
||||
}
|
||||
return fixer => fixer.replaceTextRange(textRange, " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
messageId: "nextLineOpen",
|
||||
fix: removeNewlineBetween(tokenBeforeOpeningCurly, openingCurly)
|
||||
});
|
||||
}
|
||||
|
||||
if (style === "allman" && astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly) && !singleLineException) {
|
||||
context.report({
|
||||
node: openingCurly,
|
||||
messageId: "sameLineOpen",
|
||||
fix: fixer => fixer.insertTextBefore(openingCurly, "\n")
|
||||
});
|
||||
}
|
||||
|
||||
if (astUtils.isTokenOnSameLine(openingCurly, tokenAfterOpeningCurly) && tokenAfterOpeningCurly !== closingCurly && !singleLineException) {
|
||||
context.report({
|
||||
node: openingCurly,
|
||||
messageId: "blockSameLine",
|
||||
fix: fixer => fixer.insertTextAfter(openingCurly, "\n")
|
||||
});
|
||||
}
|
||||
|
||||
if (tokenBeforeClosingCurly !== openingCurly && !singleLineException && astUtils.isTokenOnSameLine(tokenBeforeClosingCurly, closingCurly)) {
|
||||
context.report({
|
||||
node: closingCurly,
|
||||
messageId: "singleLineClose",
|
||||
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,
|
||||
messageId: "nextLineClose",
|
||||
fix: removeNewlineBetween(curlyToken, keywordToken)
|
||||
});
|
||||
}
|
||||
|
||||
if (style !== "1tbs" && astUtils.isTokenOnSameLine(curlyToken, keywordToken)) {
|
||||
context.report({
|
||||
node: curlyToken,
|
||||
messageId: "sameLineClose",
|
||||
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));
|
||||
}
|
||||
},
|
||||
StaticBlock(node) {
|
||||
validateCurlyPair(
|
||||
sourceCode.getFirstToken(node, { skip: 1 }), // skip the `static` token
|
||||
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));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* @fileoverview Enforce return after a callback.
|
||||
* @author Jamund Ferguson
|
||||
* @deprecated in ESLint v7.0.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: [],
|
||||
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require `return` statements after callbacks",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/callback-return"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "array",
|
||||
items: { type: "string" }
|
||||
}],
|
||||
|
||||
messages: {
|
||||
missingReturn: "Expected return with your callback function."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const callbacks = context.options[0] || ["callback", "cb", "next"],
|
||||
sourceCode = context.sourceCode;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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.includes(node.parent.type)) {
|
||||
return findClosestParentOfType(node.parent, types);
|
||||
}
|
||||
return node.parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if a node contains only identifiers
|
||||
* @param {ASTNode} node The node to check
|
||||
* @returns {boolean} Whether or not the node contains only identifiers
|
||||
*/
|
||||
function containsOnlyIdentifiers(node) {
|
||||
if (node.type === "Identifier") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.type === "MemberExpression") {
|
||||
if (node.object.type === "Identifier") {
|
||||
return true;
|
||||
}
|
||||
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.includes(sourceCode.getText(node.callee));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, messageId: "missingReturn" });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
};
|
||||
+399
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag non-camelcased identifiers
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce camelcase naming convention",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/camelcase"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
ignoreDestructuring: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
ignoreImports: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
ignoreGlobals: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
properties: {
|
||||
enum: ["always", "never"]
|
||||
},
|
||||
allow: {
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
type: "string"
|
||||
}
|
||||
],
|
||||
minItems: 0,
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
notCamelCase: "Identifier '{{name}}' is not in camel case.",
|
||||
notCamelCasePrivate: "#{{name}} is not in camel case."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const properties = options.properties === "never" ? "never" : "always";
|
||||
const ignoreDestructuring = options.ignoreDestructuring;
|
||||
const ignoreImports = options.ignoreImports;
|
||||
const ignoreGlobals = options.ignoreGlobals;
|
||||
const allow = options.allow || [];
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// contains reported nodes to avoid reporting twice on destructuring with shorthand notation
|
||||
const reported = new Set();
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const nameBody = name.replace(/^_+|_+$/gu, "");
|
||||
|
||||
// if there's an underscore, it might be A_CONSTANT, which is okay
|
||||
return nameBody.includes("_") && nameBody !== nameBody.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string match the ignore list
|
||||
* @param {string} name The string to check.
|
||||
* @returns {boolean} if the string is ignored
|
||||
* @private
|
||||
*/
|
||||
function isAllowed(name) {
|
||||
return allow.some(
|
||||
entry => name === entry || name.match(new RegExp(entry, "u"))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given name is good or not.
|
||||
* @param {string} name The name to check.
|
||||
* @returns {boolean} `true` if the name is good.
|
||||
* @private
|
||||
*/
|
||||
function isGoodName(name) {
|
||||
return !isUnderscored(name) || isAllowed(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given identifier reference or member expression is an assignment
|
||||
* target.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} `true` if the node is an assignment target.
|
||||
*/
|
||||
function isAssignmentTarget(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
case "AssignmentExpression":
|
||||
case "AssignmentPattern":
|
||||
return parent.left === node;
|
||||
|
||||
case "Property":
|
||||
return (
|
||||
parent.parent.type === "ObjectPattern" &&
|
||||
parent.value === node
|
||||
);
|
||||
case "ArrayPattern":
|
||||
case "RestElement":
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given binding identifier uses the original name as-is.
|
||||
* - If it's in object destructuring or object expression, the original name is its property name.
|
||||
* - If it's in import declaration, the original name is its exported name.
|
||||
* @param {ASTNode} node The `Identifier` node to check.
|
||||
* @returns {boolean} `true` if the identifier uses the original name as-is.
|
||||
*/
|
||||
function equalsToOriginalName(node) {
|
||||
const localName = node.name;
|
||||
const valueNode = node.parent.type === "AssignmentPattern"
|
||||
? node.parent
|
||||
: node;
|
||||
const parent = valueNode.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
case "Property":
|
||||
return (
|
||||
(parent.parent.type === "ObjectPattern" || parent.parent.type === "ObjectExpression") &&
|
||||
parent.value === valueNode &&
|
||||
!parent.computed &&
|
||||
parent.key.type === "Identifier" &&
|
||||
parent.key.name === localName
|
||||
);
|
||||
|
||||
case "ImportSpecifier":
|
||||
return (
|
||||
parent.local === node &&
|
||||
astUtils.getModuleExportName(parent.imported) === localName
|
||||
);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports an AST node as a rule violation.
|
||||
* @param {ASTNode} node The node to report.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function report(node) {
|
||||
if (reported.has(node.range[0])) {
|
||||
return;
|
||||
}
|
||||
reported.add(node.range[0]);
|
||||
|
||||
// Report it.
|
||||
context.report({
|
||||
node,
|
||||
messageId: node.type === "PrivateIdentifier"
|
||||
? "notCamelCasePrivate"
|
||||
: "notCamelCase",
|
||||
data: { name: node.name }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports an identifier reference or a binding identifier.
|
||||
* @param {ASTNode} node The `Identifier` node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportReferenceId(node) {
|
||||
|
||||
/*
|
||||
* For backward compatibility, if it's in callings then ignore it.
|
||||
* Not sure why it is.
|
||||
*/
|
||||
if (
|
||||
node.parent.type === "CallExpression" ||
|
||||
node.parent.type === "NewExpression"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* For backward compatibility, if it's a default value of
|
||||
* destructuring/parameters then ignore it.
|
||||
* Not sure why it is.
|
||||
*/
|
||||
if (
|
||||
node.parent.type === "AssignmentPattern" &&
|
||||
node.parent.right === node
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* The `ignoreDestructuring` flag skips the identifiers that uses
|
||||
* the property name as-is.
|
||||
*/
|
||||
if (ignoreDestructuring && equalsToOriginalName(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
report(node);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
// Report camelcase of global variable references ------------------
|
||||
Program(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
|
||||
if (!ignoreGlobals) {
|
||||
|
||||
// Defined globals in config files or directive comments.
|
||||
for (const variable of scope.variables) {
|
||||
if (
|
||||
variable.identifiers.length > 0 ||
|
||||
isGoodName(variable.name)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (const reference of variable.references) {
|
||||
|
||||
/*
|
||||
* For backward compatibility, this rule reports read-only
|
||||
* references as well.
|
||||
*/
|
||||
reportReferenceId(reference.identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Undefined globals.
|
||||
for (const reference of scope.through) {
|
||||
const id = reference.identifier;
|
||||
|
||||
if (isGoodName(id.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* For backward compatibility, this rule reports read-only
|
||||
* references as well.
|
||||
*/
|
||||
reportReferenceId(id);
|
||||
}
|
||||
},
|
||||
|
||||
// Report camelcase of declared variables --------------------------
|
||||
[[
|
||||
"VariableDeclaration",
|
||||
"FunctionDeclaration",
|
||||
"FunctionExpression",
|
||||
"ArrowFunctionExpression",
|
||||
"ClassDeclaration",
|
||||
"ClassExpression",
|
||||
"CatchClause"
|
||||
]](node) {
|
||||
for (const variable of sourceCode.getDeclaredVariables(node)) {
|
||||
if (isGoodName(variable.name)) {
|
||||
continue;
|
||||
}
|
||||
const id = variable.identifiers[0];
|
||||
|
||||
// Report declaration.
|
||||
if (!(ignoreDestructuring && equalsToOriginalName(id))) {
|
||||
report(id);
|
||||
}
|
||||
|
||||
/*
|
||||
* For backward compatibility, report references as well.
|
||||
* It looks unnecessary because declarations are reported.
|
||||
*/
|
||||
for (const reference of variable.references) {
|
||||
if (reference.init) {
|
||||
continue; // Skip the write references of initializers.
|
||||
}
|
||||
reportReferenceId(reference.identifier);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Report camelcase in properties ----------------------------------
|
||||
[[
|
||||
"ObjectExpression > Property[computed!=true] > Identifier.key",
|
||||
"MethodDefinition[computed!=true] > Identifier.key",
|
||||
"PropertyDefinition[computed!=true] > Identifier.key",
|
||||
"MethodDefinition > PrivateIdentifier.key",
|
||||
"PropertyDefinition > PrivateIdentifier.key"
|
||||
]](node) {
|
||||
if (properties === "never" || isGoodName(node.name)) {
|
||||
return;
|
||||
}
|
||||
report(node);
|
||||
},
|
||||
"MemberExpression[computed!=true] > Identifier.property"(node) {
|
||||
if (
|
||||
properties === "never" ||
|
||||
!isAssignmentTarget(node.parent) || // ← ignore read-only references.
|
||||
isGoodName(node.name)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
report(node);
|
||||
},
|
||||
|
||||
// Report camelcase in import --------------------------------------
|
||||
ImportDeclaration(node) {
|
||||
for (const variable of sourceCode.getDeclaredVariables(node)) {
|
||||
if (isGoodName(variable.name)) {
|
||||
continue;
|
||||
}
|
||||
const id = variable.identifiers[0];
|
||||
|
||||
// Report declaration.
|
||||
if (!(ignoreImports && equalsToOriginalName(id))) {
|
||||
report(id);
|
||||
}
|
||||
|
||||
/*
|
||||
* For backward compatibility, report references as well.
|
||||
* It looks unnecessary because declarations are reported.
|
||||
*/
|
||||
for (const reference of variable.references) {
|
||||
reportReferenceId(reference.identifier);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Report camelcase in re-export -----------------------------------
|
||||
[[
|
||||
"ExportAllDeclaration > Identifier.exported",
|
||||
"ExportSpecifier > Identifier.exported"
|
||||
]](node) {
|
||||
if (isGoodName(node.name)) {
|
||||
return;
|
||||
}
|
||||
report(node);
|
||||
},
|
||||
|
||||
// Report camelcase in labels --------------------------------------
|
||||
[[
|
||||
"LabeledStatement > Identifier.label",
|
||||
|
||||
/*
|
||||
* For backward compatibility, report references as well.
|
||||
* It looks unnecessary because declarations are reported.
|
||||
*/
|
||||
"BreakStatement > Identifier.label",
|
||||
"ContinueStatement > Identifier.label"
|
||||
]](node) {
|
||||
if (isGoodName(node.name)) {
|
||||
return;
|
||||
}
|
||||
report(node);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* @fileoverview enforce or disallow capitalization of the first letter of a comment
|
||||
* @author Kevin Partington
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const LETTER_PATTERN = require("./utils/patterns/letters");
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
|
||||
WHITESPACE = /\s/gu,
|
||||
MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/u; // TODO: Combine w/ max-len pattern?
|
||||
|
||||
/*
|
||||
* 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
|
||||
};
|
||||
const DEFAULTS = {
|
||||
ignorePattern: "",
|
||||
ignoreInlineComments: false,
|
||||
ignoreConsecutiveComments: 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) {
|
||||
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})`, "u");
|
||||
|
||||
normalizedOptions[key].ignorePatternRegExp = regExp;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce or disallow capitalization of the first letter of a comment",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/capitalized-comments"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
schema: [
|
||||
{ enum: ["always", "never"] },
|
||||
{
|
||||
oneOf: [
|
||||
SCHEMA_BODY,
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
line: SCHEMA_BODY,
|
||||
block: SCHEMA_BODY
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedLowercaseComment: "Comments should not begin with a lowercase character.",
|
||||
unexpectedUppercaseComment: "Comments should not begin with an uppercase character."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const capitalize = context.options[0] || "always",
|
||||
normalizedOptions = getAllNormalizedOptions(context.options[1]),
|
||||
sourceCode = context.sourceCode;
|
||||
|
||||
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"].includes(previousTokenOrComment.type)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(/\*/gu, "");
|
||||
|
||||
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;
|
||||
}
|
||||
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 messageId = capitalize === "always"
|
||||
? "unexpectedLowercaseComment"
|
||||
: "unexpectedUppercaseComment";
|
||||
|
||||
context.report({
|
||||
node: null, // Intentionally using loc instead
|
||||
loc: comment.loc,
|
||||
messageId,
|
||||
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.filter(token => token.type !== "Shebang").forEach(processComment);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce that all class methods use 'this'.
|
||||
* @author Patrick Williams
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce that class methods utilize `this`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/class-methods-use-this"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
exceptMethods: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
enforceForClassFields: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
missingThis: "Expected 'this' to be used by class {{name}}."
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
const config = Object.assign({}, context.options[0]);
|
||||
const enforceForClassFields = config.enforceForClassFields !== false;
|
||||
const exceptMethods = new Set(config.exceptMethods || []);
|
||||
|
||||
const stack = [];
|
||||
|
||||
/**
|
||||
* Push `this` used flag initialized with `false` onto the stack.
|
||||
* @returns {void}
|
||||
*/
|
||||
function pushContext() {
|
||||
stack.push(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop `this` used flag from the stack.
|
||||
* @returns {boolean | undefined} `this` used flag
|
||||
*/
|
||||
function popContext() {
|
||||
return stack.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
pushContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
switch (node.type) {
|
||||
case "MethodDefinition":
|
||||
return !node.static && node.kind !== "constructor";
|
||||
case "PropertyDefinition":
|
||||
return !node.static && enforceForClassFields;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (isInstanceMethod(node)) {
|
||||
if (node.computed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hashIfNeeded = node.key.type === "PrivateIdentifier" ? "#" : "";
|
||||
const name = node.key.type === "Literal"
|
||||
? astUtils.getStaticStringValue(node.key)
|
||||
: (node.key.name || "");
|
||||
|
||||
return !exceptMethods.has(hashIfNeeded + name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = popContext();
|
||||
|
||||
if (isIncludedInstanceMethod(node.parent) && !methodUsesThis) {
|
||||
context.report({
|
||||
node,
|
||||
loc: astUtils.getFunctionHeadLoc(node, context.sourceCode),
|
||||
messageId: "missingThis",
|
||||
data: {
|
||||
name: astUtils.getFunctionNameWithKind(node)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
/*
|
||||
* Class field value are implicit functions.
|
||||
*/
|
||||
"PropertyDefinition > *.key:exit": pushContext,
|
||||
"PropertyDefinition:exit": popContext,
|
||||
|
||||
/*
|
||||
* Class static blocks are implicit functions. They aren't required to use `this`,
|
||||
* but we have to push context so that it captures any use of `this` in the static block
|
||||
* separately from enclosing contexts, because static blocks have their own `this` and it
|
||||
* shouldn't count as used `this` in enclosing contexts.
|
||||
*/
|
||||
StaticBlock: pushContext,
|
||||
"StaticBlock:exit": popContext,
|
||||
|
||||
ThisExpression: markThisUsed,
|
||||
Super: markThisUsed,
|
||||
...(
|
||||
enforceForClassFields && {
|
||||
"PropertyDefinition > ArrowFunctionExpression.value": enterFunction,
|
||||
"PropertyDefinition > ArrowFunctionExpression.value:exit": exitFunction
|
||||
}
|
||||
)
|
||||
};
|
||||
}
|
||||
};
|
||||
+373
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* @fileoverview Rule to forbid or enforce dangling commas.
|
||||
* @author Ian Christian Myers
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULT_OPTIONS = Object.freeze({
|
||||
arrays: "never",
|
||||
objects: "never",
|
||||
imports: "never",
|
||||
exports: "never",
|
||||
functions: "never"
|
||||
});
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {number} ecmaVersion The normalized ECMAScript version.
|
||||
* @returns {Object} The normalized option value.
|
||||
*/
|
||||
function normalizeOptions(optionValue, ecmaVersion) {
|
||||
if (typeof optionValue === "string") {
|
||||
return {
|
||||
arrays: optionValue,
|
||||
objects: optionValue,
|
||||
imports: optionValue,
|
||||
exports: optionValue,
|
||||
functions: ecmaVersion < 2017 ? "ignore" : optionValue
|
||||
};
|
||||
}
|
||||
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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Require or disallow trailing commas",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/comma-dangle"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
schema: {
|
||||
definitions: {
|
||||
value: {
|
||||
enum: [
|
||||
"always-multiline",
|
||||
"always",
|
||||
"never",
|
||||
"only-multiline"
|
||||
]
|
||||
},
|
||||
valueWithIgnore: {
|
||||
enum: [
|
||||
"always-multiline",
|
||||
"always",
|
||||
"ignore",
|
||||
"never",
|
||||
"only-multiline"
|
||||
]
|
||||
}
|
||||
},
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
$ref: "#/definitions/value"
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
arrays: { $ref: "#/definitions/valueWithIgnore" },
|
||||
objects: { $ref: "#/definitions/valueWithIgnore" },
|
||||
imports: { $ref: "#/definitions/valueWithIgnore" },
|
||||
exports: { $ref: "#/definitions/valueWithIgnore" },
|
||||
functions: { $ref: "#/definitions/valueWithIgnore" }
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
additionalItems: false
|
||||
},
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected trailing comma.",
|
||||
missing: "Missing trailing comma."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = normalizeOptions(context.options[0], context.languageOptions.ecmaVersion);
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
/**
|
||||
* Returns the last element of an array
|
||||
* @param {any[]} array The input array
|
||||
* @returns {any} The last element
|
||||
*/
|
||||
function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case "ObjectExpression":
|
||||
case "ObjectPattern":
|
||||
return last(node.properties);
|
||||
case "ArrayExpression":
|
||||
case "ArrayPattern":
|
||||
return last(node.elements);
|
||||
case "ImportDeclaration":
|
||||
case "ExportNamedDeclaration":
|
||||
return last(node.specifiers);
|
||||
case "FunctionDeclaration":
|
||||
case "FunctionExpression":
|
||||
case "ArrowFunctionExpression":
|
||||
return last(node.params);
|
||||
case "CallExpression":
|
||||
case "NewExpression":
|
||||
return 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,
|
||||
messageId: "unexpected",
|
||||
*fix(fixer) {
|
||||
yield fixer.remove(trailingToken);
|
||||
|
||||
/*
|
||||
* Extend the range of the fix to include surrounding tokens to ensure
|
||||
* that the element after which the comma is removed stays _last_.
|
||||
* This intentionally makes conflicts in fix ranges with rules that may be
|
||||
* adding or removing elements in the same autofix pass.
|
||||
* https://github.com/eslint/eslint/issues/15660
|
||||
*/
|
||||
yield fixer.insertTextBefore(sourceCode.getTokenBefore(trailingToken), "");
|
||||
yield fixer.insertTextAfter(sourceCode.getTokenAfter(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: {
|
||||
start: trailingToken.loc.end,
|
||||
end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end)
|
||||
},
|
||||
messageId: "missing",
|
||||
*fix(fixer) {
|
||||
yield fixer.insertTextAfter(trailingToken, ",");
|
||||
|
||||
/*
|
||||
* Extend the range of the fix to include surrounding tokens to ensure
|
||||
* that the element after which the comma is inserted stays _last_.
|
||||
* This intentionally makes conflicts in fix ranges with rules that may be
|
||||
* adding or removing elements in the same autofix pass.
|
||||
* https://github.com/eslint/eslint/issues/15660
|
||||
*/
|
||||
yield fixer.insertTextBefore(trailingToken, "");
|
||||
yield fixer.insertTextAfter(sourceCode.getTokenAfter(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() {}
|
||||
};
|
||||
|
||||
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]
|
||||
};
|
||||
}
|
||||
};
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* @fileoverview Comma spacing - validates spacing before and after comma
|
||||
* @author Vignesh Anand aka vegetableman.
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent spacing before and after commas",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/comma-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
before: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
after: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
missing: "A space is required {{loc}} ','.",
|
||||
unexpected: "There should be no space {{loc}} ','."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
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} loc 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, loc, otherNode) {
|
||||
context.report({
|
||||
node,
|
||||
fix(fixer) {
|
||||
if (options[loc]) {
|
||||
if (loc === "before") {
|
||||
return fixer.insertTextBefore(node, " ");
|
||||
}
|
||||
return fixer.insertTextAfter(node, " ");
|
||||
|
||||
}
|
||||
let start, end;
|
||||
const newText = "";
|
||||
|
||||
if (loc === "before") {
|
||||
start = otherNode.range[1];
|
||||
end = node.range[0];
|
||||
} else {
|
||||
start = node.range[1];
|
||||
end = otherNode.range[0];
|
||||
}
|
||||
|
||||
return fixer.replaceTextRange([start, end], newText);
|
||||
|
||||
},
|
||||
messageId: options[loc] ? "missing" : "unexpected",
|
||||
data: {
|
||||
loc
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
const previousToken = tokensAndComments[i - 1];
|
||||
const nextToken = tokensAndComments[i + 1];
|
||||
|
||||
if (
|
||||
previousToken &&
|
||||
!astUtils.isCommaToken(previousToken) && // ignore spacing between two commas
|
||||
|
||||
/*
|
||||
* `commaTokensToIgnore` are ending commas of `null` elements (array holes/elisions).
|
||||
* In addition to spacing between two commas, this can also ignore:
|
||||
*
|
||||
* - Spacing after `[` (controlled by array-bracket-spacing)
|
||||
* Example: [ , ]
|
||||
* ^
|
||||
* - Spacing after a comment (for backwards compatibility, this was possibly unintentional)
|
||||
* Example: [a, /* * / ,]
|
||||
* ^
|
||||
*/
|
||||
!commaTokensToIgnore.includes(token) &&
|
||||
|
||||
astUtils.isTokenOnSameLine(previousToken, token) &&
|
||||
options.before !== sourceCode.isSpaceBetweenTokens(previousToken, token)
|
||||
) {
|
||||
report(token, "before", previousToken);
|
||||
}
|
||||
|
||||
if (
|
||||
nextToken &&
|
||||
!astUtils.isCommaToken(nextToken) && // ignore spacing between two commas
|
||||
!astUtils.isClosingParenToken(nextToken) && // controlled by space-in-parens
|
||||
!astUtils.isClosingBracketToken(nextToken) && // controlled by array-bracket-spacing
|
||||
!astUtils.isClosingBraceToken(nextToken) && // controlled by object-curly-spacing
|
||||
!(!options.after && nextToken.type === "Line") && // special case, allow space before line comment
|
||||
astUtils.isTokenOnSameLine(token, nextToken) &&
|
||||
options.after !== sourceCode.isSpaceBetweenTokens(token, nextToken)
|
||||
) {
|
||||
report(token, "after", nextToken);
|
||||
}
|
||||
});
|
||||
},
|
||||
ArrayExpression: addNullElementsToIgnoreList,
|
||||
ArrayPattern: addNullElementsToIgnoreList
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* @fileoverview Comma style - enforces comma styles of two types: last and first
|
||||
* @author Vignesh Anand aka vegetableman
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent comma style",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/comma-style"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["first", "last"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
exceptions: {
|
||||
type: "object",
|
||||
additionalProperties: {
|
||||
type: "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedLineBeforeAndAfterComma: "Bad line breaking before and after ','.",
|
||||
expectedCommaFirst: "',' should be placed first.",
|
||||
expectedCommaLast: "',' should be placed last."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const style = context.options[0] || "last",
|
||||
sourceCode = context.sourceCode;
|
||||
const exceptions = {
|
||||
ArrayPattern: true,
|
||||
ArrowFunctionExpression: true,
|
||||
CallExpression: true,
|
||||
FunctionDeclaration: true,
|
||||
FunctionExpression: true,
|
||||
ImportDeclaration: true,
|
||||
ObjectPattern: true,
|
||||
NewExpression: true
|
||||
};
|
||||
|
||||
if (context.options.length === 2 && Object.prototype.hasOwnProperty.call(context.options[1], "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(astUtils.LINEBREAK_MATCHER, "")}`;
|
||||
|
||||
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)) {
|
||||
|
||||
const comment = sourceCode.getCommentsAfter(commaToken)[0];
|
||||
const styleType = comment && comment.type === "Block" && astUtils.isTokenOnSameLine(commaToken, comment)
|
||||
? style
|
||||
: "between";
|
||||
|
||||
// lone comma
|
||||
context.report({
|
||||
node: reportItem,
|
||||
loc: commaToken.loc,
|
||||
messageId: "unexpectedLineBeforeAndAfterComma",
|
||||
fix: getFixerFunction(styleType, previousItemToken, commaToken, currentItemToken)
|
||||
});
|
||||
|
||||
} else if (style === "first" && !astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
|
||||
|
||||
context.report({
|
||||
node: reportItem,
|
||||
loc: commaToken.loc,
|
||||
messageId: "expectedCommaFirst",
|
||||
fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken)
|
||||
});
|
||||
|
||||
} else if (style === "last" && astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
|
||||
|
||||
context.report({
|
||||
node: reportItem,
|
||||
loc: commaToken.loc,
|
||||
messageId: "expectedCommaLast",
|
||||
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;
|
||||
|
||||
/*
|
||||
* 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];
|
||||
} else {
|
||||
previousItemToken = currentItemToken;
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* 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");
|
||||
};
|
||||
}
|
||||
if (!exceptions.NewExpression) {
|
||||
nodes.NewExpression = function(node) {
|
||||
validateComma(node, "arguments");
|
||||
};
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
};
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* @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, while, try, switch/case,
|
||||
* @author Patrick Brosset
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
const { upperCaseFirst } = require("../shared/string-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce a maximum cyclomatic complexity allowed in a program",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/complexity"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
maximum: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
max: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
complex: "{{name}} has a complexity of {{complexity}}. Maximum allowed is {{max}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const option = context.options[0];
|
||||
let THRESHOLD = 20;
|
||||
|
||||
if (
|
||||
typeof option === "object" &&
|
||||
(Object.prototype.hasOwnProperty.call(option, "maximum") || Object.prototype.hasOwnProperty.call(option, "max"))
|
||||
) {
|
||||
THRESHOLD = option.maximum || option.max;
|
||||
} else if (typeof option === "number") {
|
||||
THRESHOLD = option;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// Using a stack to store complexity per code path
|
||||
const complexities = [];
|
||||
|
||||
/**
|
||||
* Increase the complexity of the code path in context
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function increaseComplexity() {
|
||||
complexities[complexities.length - 1]++;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
|
||||
onCodePathStart() {
|
||||
|
||||
// The initial complexity is 1, representing one execution path in the CodePath
|
||||
complexities.push(1);
|
||||
},
|
||||
|
||||
// Each branching in the code adds 1 to the complexity
|
||||
CatchClause: increaseComplexity,
|
||||
ConditionalExpression: increaseComplexity,
|
||||
LogicalExpression: increaseComplexity,
|
||||
ForStatement: increaseComplexity,
|
||||
ForInStatement: increaseComplexity,
|
||||
ForOfStatement: increaseComplexity,
|
||||
IfStatement: increaseComplexity,
|
||||
WhileStatement: increaseComplexity,
|
||||
DoWhileStatement: increaseComplexity,
|
||||
|
||||
// Avoid `default`
|
||||
"SwitchCase[test]": increaseComplexity,
|
||||
|
||||
// Logical assignment operators have short-circuiting behavior
|
||||
AssignmentExpression(node) {
|
||||
if (astUtils.isLogicalAssignmentOperator(node.operator)) {
|
||||
increaseComplexity();
|
||||
}
|
||||
},
|
||||
|
||||
onCodePathEnd(codePath, node) {
|
||||
const complexity = complexities.pop();
|
||||
|
||||
/*
|
||||
* This rule only evaluates complexity of functions, so "program" is excluded.
|
||||
* Class field initializers and class static blocks are implicit functions. Therefore,
|
||||
* they shouldn't contribute to the enclosing function's complexity, but their
|
||||
* own complexity should be evaluated.
|
||||
*/
|
||||
if (
|
||||
codePath.origin !== "function" &&
|
||||
codePath.origin !== "class-field-initializer" &&
|
||||
codePath.origin !== "class-static-block"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (complexity > THRESHOLD) {
|
||||
let name;
|
||||
|
||||
if (codePath.origin === "class-field-initializer") {
|
||||
name = "class field initializer";
|
||||
} else if (codePath.origin === "class-static-block") {
|
||||
name = "class static block";
|
||||
} else {
|
||||
name = astUtils.getFunctionNameWithKind(node);
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "complex",
|
||||
data: {
|
||||
name: upperCaseFirst(name),
|
||||
complexity,
|
||||
max: THRESHOLD
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+208
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @fileoverview Disallows or enforces spaces inside computed properties.
|
||||
* @author Jamund Ferguson
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent spacing inside computed property brackets",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/computed-property-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "never"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
enforceForClassMembers: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedSpaceBefore: "There should be no space before '{{tokenValue}}'.",
|
||||
unexpectedSpaceAfter: "There should be no space after '{{tokenValue}}'.",
|
||||
|
||||
missingSpaceBefore: "A space is required before '{{tokenValue}}'.",
|
||||
missingSpaceAfter: "A space is required after '{{tokenValue}}'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const propertyNameMustBeSpaced = context.options[0] === "always"; // default is "never"
|
||||
const enforceForClassMembers = !context.options[1] || context.options[1].enforceForClassMembers;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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: { start: token.loc.end, end: tokenAfter.loc.start },
|
||||
messageId: "unexpectedSpaceAfter",
|
||||
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: { start: tokenBefore.loc.end, end: token.loc.start },
|
||||
messageId: "unexpectedSpaceBefore",
|
||||
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,
|
||||
messageId: "missingSpaceAfter",
|
||||
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,
|
||||
messageId: "missingSpaceBefore",
|
||||
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, astUtils.isOpeningBracketToken),
|
||||
first = sourceCode.getTokenAfter(before, { includeComments: true }),
|
||||
after = sourceCode.getTokenAfter(property, astUtils.isClosingBracketToken),
|
||||
last = sourceCode.getTokenBefore(after, { includeComments: true });
|
||||
|
||||
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
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
const listeners = {
|
||||
Property: checkSpacing("key"),
|
||||
MemberExpression: checkSpacing("property")
|
||||
};
|
||||
|
||||
if (enforceForClassMembers) {
|
||||
listeners.MethodDefinition =
|
||||
listeners.PropertyDefinition = listeners.Property;
|
||||
}
|
||||
|
||||
return listeners;
|
||||
|
||||
}
|
||||
};
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag consistent return values
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
const { upperCaseFirst } = require("../shared/string-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks all segments in a set and returns true if all are unreachable.
|
||||
* @param {Set<CodePathSegment>} segments The segments to check.
|
||||
* @returns {boolean} True if all segments are unreachable; false otherwise.
|
||||
*/
|
||||
function areAllSegmentsUnreachable(segments) {
|
||||
|
||||
for (const segment of segments) {
|
||||
if (segment.reachable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require `return` statements to either always or never specify values",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/consistent-return"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
treatUndefinedAsUnspecified: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
missingReturn: "Expected to return a value at the end of {{name}}.",
|
||||
missingReturnValue: "{{name}} expected a return value.",
|
||||
unexpectedReturnValue: "{{name}} expected no return value."
|
||||
}
|
||||
},
|
||||
|
||||
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 ||
|
||||
areAllSegmentsUnreachable(funcInfo.currentSegments) ||
|
||||
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.sourceCode.getTokenBefore(node.body, astUtils.isArrowToken).loc;
|
||||
} else if (
|
||||
node.parent.type === "MethodDefinition" ||
|
||||
(node.parent.type === "Property" && node.parent.method)
|
||||
) {
|
||||
|
||||
// Method name.
|
||||
loc = node.parent.key.loc;
|
||||
} else {
|
||||
|
||||
// Function name or `function` keyword.
|
||||
loc = (node.id || context.sourceCode.getFirstToken(node)).loc;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
name = astUtils.getFunctionNameWithKind(node);
|
||||
}
|
||||
|
||||
// Reports.
|
||||
context.report({
|
||||
node,
|
||||
loc,
|
||||
messageId: "missingReturn",
|
||||
data: { name }
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
// Initializes/Disposes state of each code path.
|
||||
onCodePathStart(codePath, node) {
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
codePath,
|
||||
hasReturn: false,
|
||||
hasReturnValue: false,
|
||||
messageId: "",
|
||||
node,
|
||||
currentSegments: new Set()
|
||||
};
|
||||
},
|
||||
onCodePathEnd() {
|
||||
funcInfo = funcInfo.upper;
|
||||
},
|
||||
|
||||
onUnreachableCodePathSegmentStart(segment) {
|
||||
funcInfo.currentSegments.add(segment);
|
||||
},
|
||||
|
||||
onUnreachableCodePathSegmentEnd(segment) {
|
||||
funcInfo.currentSegments.delete(segment);
|
||||
},
|
||||
|
||||
onCodePathSegmentStart(segment) {
|
||||
funcInfo.currentSegments.add(segment);
|
||||
},
|
||||
|
||||
onCodePathSegmentEnd(segment) {
|
||||
funcInfo.currentSegments.delete(segment);
|
||||
},
|
||||
|
||||
|
||||
// Reports a given return statement if it's inconsistent.
|
||||
ReturnStatement(node) {
|
||||
const argument = node.argument;
|
||||
let hasReturnValue = Boolean(argument);
|
||||
|
||||
if (treatUndefinedAsUnspecified && hasReturnValue) {
|
||||
hasReturnValue = !astUtils.isSpecificId(argument, "undefined") && argument.operator !== "void";
|
||||
}
|
||||
|
||||
if (!funcInfo.hasReturn) {
|
||||
funcInfo.hasReturn = true;
|
||||
funcInfo.hasReturnValue = hasReturnValue;
|
||||
funcInfo.messageId = hasReturnValue ? "missingReturnValue" : "unexpectedReturnValue";
|
||||
funcInfo.data = {
|
||||
name: funcInfo.node.type === "Program"
|
||||
? "Program"
|
||||
: upperCaseFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
|
||||
};
|
||||
} else if (funcInfo.hasReturnValue !== hasReturnValue) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: funcInfo.messageId,
|
||||
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
|
||||
};
|
||||
}
|
||||
};
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce consistent naming of "this" context variables
|
||||
* @author Raphael Pigulla
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent naming when capturing the current execution context",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/consistent-this"
|
||||
},
|
||||
|
||||
schema: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
minLength: 1
|
||||
},
|
||||
uniqueItems: true
|
||||
},
|
||||
|
||||
messages: {
|
||||
aliasNotAssignedToThis: "Designated alias '{{name}}' is not assigned to 'this'.",
|
||||
unexpectedAlias: "Unexpected alias '{{name}}' for 'this'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
let aliases = [];
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
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} name the name of the alias that was incorrectly used.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportBadAssignment(node, name) {
|
||||
context.report({ node, messageId: "aliasNotAssignedToThis", data: { name } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.includes(name)) {
|
||||
if (!isThis || node.operator && node.operator !== "=") {
|
||||
reportBadAssignment(node, name);
|
||||
}
|
||||
} else if (isThis) {
|
||||
context.report({ node, messageId: "unexpectedAlias", 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 assigned to the correct value.
|
||||
* @param {ASTNode} node The node that represents the scope to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function ensureWasAssigned(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+446
@@ -0,0 +1,446 @@
|
||||
/**
|
||||
* @fileoverview A rule to verify `super()` callings in constructor.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks all segments in a set and returns true if any are reachable.
|
||||
* @param {Set<CodePathSegment>} segments The segments to check.
|
||||
* @returns {boolean} True if any segment is reachable; false otherwise.
|
||||
*/
|
||||
function isAnySegmentReachable(segments) {
|
||||
|
||||
for (const segment of segments) {
|
||||
if (segment.reachable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 "ChainExpression":
|
||||
case "YieldExpression":
|
||||
case "TaggedTemplateExpression":
|
||||
case "MetaProperty":
|
||||
return true;
|
||||
|
||||
case "Identifier":
|
||||
return node.name !== "undefined";
|
||||
|
||||
case "AssignmentExpression":
|
||||
if (["=", "&&="].includes(node.operator)) {
|
||||
return isPossibleConstructor(node.right);
|
||||
}
|
||||
|
||||
if (["||=", "??="].includes(node.operator)) {
|
||||
return (
|
||||
isPossibleConstructor(node.left) ||
|
||||
isPossibleConstructor(node.right)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* All other assignment operators are mathematical assignment operators (arithmetic or bitwise).
|
||||
* An assignment expression with a mathematical operator can either evaluate to a primitive value,
|
||||
* or throw, depending on the operands. Thus, it cannot evaluate to a constructor function.
|
||||
*/
|
||||
return false;
|
||||
|
||||
case "LogicalExpression":
|
||||
|
||||
/*
|
||||
* If the && operator short-circuits, the left side was falsy and therefore not a constructor, and if
|
||||
* it doesn't short-circuit, it takes the value from the right side, so the right side must always be a
|
||||
* possible constructor. A future improvement could verify that the left side could be truthy by
|
||||
* excluding falsy literals.
|
||||
*/
|
||||
if (node.operator === "&&") {
|
||||
return isPossibleConstructor(node.right);
|
||||
}
|
||||
|
||||
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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Require `super()` calls in constructors",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/constructor-super"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
missingSome: "Lacked a call of 'super()' in some code paths.",
|
||||
missingAll: "Expected to call 'super()'.",
|
||||
|
||||
duplicate: "Unexpected duplicate 'super()'.",
|
||||
badSuper: "Unexpected 'super()' because 'super' is not a constructor.",
|
||||
unexpected: "Unexpected 'super()'."
|
||||
}
|
||||
},
|
||||
|
||||
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,
|
||||
currentSegments: new Set()
|
||||
};
|
||||
} else {
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
isConstructor: false,
|
||||
hasExtends: false,
|
||||
superIsConstructor: false,
|
||||
codePath,
|
||||
currentSegments: new Set()
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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({
|
||||
messageId: calledInSomePaths
|
||||
? "missingSome"
|
||||
: "missingAll",
|
||||
node: node.parent
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize information of a given code path segment.
|
||||
* @param {CodePathSegment} segment A code path segment to initialize.
|
||||
* @returns {void}
|
||||
*/
|
||||
onCodePathSegmentStart(segment) {
|
||||
|
||||
funcInfo.currentSegments.add(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);
|
||||
}
|
||||
},
|
||||
|
||||
onUnreachableCodePathSegmentStart(segment) {
|
||||
funcInfo.currentSegments.add(segment);
|
||||
},
|
||||
|
||||
onUnreachableCodePathSegmentEnd(segment) {
|
||||
funcInfo.currentSegments.delete(segment);
|
||||
},
|
||||
|
||||
onCodePathSegmentEnd(segment) {
|
||||
funcInfo.currentSegments.delete(segment);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 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({
|
||||
messageId: "duplicate",
|
||||
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.currentSegments;
|
||||
let duplicate = false;
|
||||
let info = null;
|
||||
|
||||
for (const segment of segments) {
|
||||
|
||||
if (segment.reachable) {
|
||||
info = segInfoMap[segment.id];
|
||||
|
||||
duplicate = duplicate || info.calledInSomePaths;
|
||||
info.calledInSomePaths = info.calledInEveryPaths = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (info) {
|
||||
if (duplicate) {
|
||||
context.report({
|
||||
messageId: "duplicate",
|
||||
node
|
||||
});
|
||||
} else if (!funcInfo.superIsConstructor) {
|
||||
context.report({
|
||||
messageId: "badSuper",
|
||||
node
|
||||
});
|
||||
} else {
|
||||
info.validNodes.push(node);
|
||||
}
|
||||
}
|
||||
} else if (isAnySegmentReachable(funcInfo.currentSegments)) {
|
||||
context.report({
|
||||
messageId: "unexpected",
|
||||
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.currentSegments;
|
||||
|
||||
for (const segment of segments) {
|
||||
|
||||
if (segment.reachable) {
|
||||
const info = segInfoMap[segment.id];
|
||||
|
||||
info.calledInSomePaths = info.calledInEveryPaths = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets state.
|
||||
* @returns {void}
|
||||
*/
|
||||
"Program:exit"() {
|
||||
segInfoMap = Object.create(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+486
@@ -0,0 +1,486 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag statements without curly braces
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent brace style for all control statements",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/curly"
|
||||
},
|
||||
|
||||
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",
|
||||
|
||||
messages: {
|
||||
missingCurlyAfter: "Expected { after '{{name}}'.",
|
||||
missingCurlyAfterCondition: "Expected { after '{{name}}' condition.",
|
||||
unexpectedCurlyAfter: "Unnecessary { after '{{name}}'.",
|
||||
unexpectedCurlyAfterCondition: "Unnecessary { after '{{name}}' condition."
|
||||
}
|
||||
},
|
||||
|
||||
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.sourceCode;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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) {
|
||||
if (node.type === "EmptyStatement") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const first = sourceCode.getFirstToken(node);
|
||||
const last = sourceCode.getLastToken(node);
|
||||
const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last;
|
||||
|
||||
return first.loc.start.line === lastExcludingSemicolon.loc.end.line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given node is a lexical declaration (let, const, function, or class)
|
||||
* @param {ASTNode} node The node to check
|
||||
* @returns {boolean} True if the node is a lexical declaration
|
||||
* @private
|
||||
*/
|
||||
function isLexicalDeclaration(node) {
|
||||
if (node.type === "VariableDeclaration") {
|
||||
return node.kind === "const" || node.kind === "let";
|
||||
}
|
||||
|
||||
return node.type === "FunctionDeclaration" || node.type === "ClassDeclaration";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given node has an `else` keyword token as the first token after.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} `true` if the node is followed by an `else` keyword token.
|
||||
*/
|
||||
function isFollowedByElseKeyword(node) {
|
||||
const nextToken = sourceCode.getTokenAfter(node);
|
||||
|
||||
return Boolean(nextToken) && isElseKeywordToken(nextToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (/^[([/`+-]/u.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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the code represented by the given node contains an `if` statement
|
||||
* that would become associated with an `else` keyword directly appended to that code.
|
||||
*
|
||||
* Examples where it returns `true`:
|
||||
*
|
||||
* if (a)
|
||||
* foo();
|
||||
*
|
||||
* if (a) {
|
||||
* foo();
|
||||
* }
|
||||
*
|
||||
* if (a)
|
||||
* foo();
|
||||
* else if (b)
|
||||
* bar();
|
||||
*
|
||||
* while (a)
|
||||
* if (b)
|
||||
* if(c)
|
||||
* foo();
|
||||
* else
|
||||
* bar();
|
||||
*
|
||||
* Examples where it returns `false`:
|
||||
*
|
||||
* if (a)
|
||||
* foo();
|
||||
* else
|
||||
* bar();
|
||||
*
|
||||
* while (a) {
|
||||
* if (b)
|
||||
* if(c)
|
||||
* foo();
|
||||
* else
|
||||
* bar();
|
||||
* }
|
||||
*
|
||||
* while (a)
|
||||
* if (b) {
|
||||
* if(c)
|
||||
* foo();
|
||||
* }
|
||||
* else
|
||||
* bar();
|
||||
* @param {ASTNode} node Node representing the code to check.
|
||||
* @returns {boolean} `true` if an `if` statement within the code would become associated with an `else` appended to that code.
|
||||
*/
|
||||
function hasUnsafeIf(node) {
|
||||
switch (node.type) {
|
||||
case "IfStatement":
|
||||
if (!node.alternate) {
|
||||
return true;
|
||||
}
|
||||
return hasUnsafeIf(node.alternate);
|
||||
case "ForStatement":
|
||||
case "ForInStatement":
|
||||
case "ForOfStatement":
|
||||
case "LabeledStatement":
|
||||
case "WithStatement":
|
||||
case "WhileStatement":
|
||||
return hasUnsafeIf(node.body);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the existing curly braces around the single statement are necessary to preserve the semantics of the code.
|
||||
* The braces, which make the given block body, are necessary in either of the following situations:
|
||||
*
|
||||
* 1. The statement is a lexical declaration.
|
||||
* 2. Without the braces, an `if` within the statement would become associated with an `else` after the closing brace:
|
||||
*
|
||||
* if (a) {
|
||||
* if (b)
|
||||
* foo();
|
||||
* }
|
||||
* else
|
||||
* bar();
|
||||
*
|
||||
* if (a)
|
||||
* while (b)
|
||||
* while (c) {
|
||||
* while (d)
|
||||
* if (e)
|
||||
* while(f)
|
||||
* foo();
|
||||
* }
|
||||
* else
|
||||
* bar();
|
||||
* @param {ASTNode} node `BlockStatement` body with exactly one statement directly inside. The statement can have its own nested statements.
|
||||
* @returns {boolean} `true` if the braces are necessary - removing them (replacing the given `BlockStatement` body with its single statement content)
|
||||
* would change the semantics of the code or produce a syntax error.
|
||||
*/
|
||||
function areBracesNecessary(node) {
|
||||
const statement = node.body[0];
|
||||
|
||||
return isLexicalDeclaration(statement) ||
|
||||
hasUnsafeIf(statement) && isFollowedByElseKeyword(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {{ condition: boolean }} opts Options to pass to the report functions
|
||||
* @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, opts) {
|
||||
const hasBlock = (body.type === "BlockStatement");
|
||||
let expected = null;
|
||||
|
||||
if (hasBlock && (body.body.length !== 1 || areBracesNecessary(body))) {
|
||||
expected = true;
|
||||
} else if (multiOnly) {
|
||||
expected = false;
|
||||
} else if (multiLine) {
|
||||
if (!isCollapsedOneLiner(body)) {
|
||||
expected = true;
|
||||
}
|
||||
|
||||
// otherwise, the body is allowed to have braces or not to have braces
|
||||
|
||||
} else if (multiOrNest) {
|
||||
if (hasBlock) {
|
||||
const statement = body.body[0];
|
||||
const leadingCommentsInBlock = sourceCode.getCommentsBefore(statement);
|
||||
|
||||
expected = !isOneLiner(statement) || leadingCommentsInBlock.length > 0;
|
||||
} else {
|
||||
expected = !isOneLiner(body);
|
||||
}
|
||||
} else {
|
||||
|
||||
// default "all"
|
||||
expected = true;
|
||||
}
|
||||
|
||||
return {
|
||||
actual: hasBlock,
|
||||
expected,
|
||||
check() {
|
||||
if (this.expected !== null && this.expected !== this.actual) {
|
||||
if (this.expected) {
|
||||
context.report({
|
||||
node,
|
||||
loc: body.loc,
|
||||
messageId: opts && opts.condition ? "missingCurlyAfterCondition" : "missingCurlyAfter",
|
||||
data: {
|
||||
name
|
||||
},
|
||||
fix: fixer => fixer.replaceText(body, `{${sourceCode.getText(body)}}`)
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
node,
|
||||
loc: body.loc,
|
||||
messageId: opts && opts.condition ? "unexpectedCurlyAfterCondition" : "unexpectedCurlyAfter",
|
||||
data: {
|
||||
name
|
||||
},
|
||||
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(body).range[1] === body.range[0] &&
|
||||
!astUtils.canTokensBeAdjacent("do", sourceCode.getFirstToken(body, { skip: 1 }));
|
||||
|
||||
const openingBracket = sourceCode.getFirstToken(body);
|
||||
const closingBracket = sourceCode.getLastToken(body);
|
||||
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(body, (needsPrecedingSpace ? " " : "") + resultingBodyText);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
for (let currentNode = node; currentNode; currentNode = currentNode.alternate) {
|
||||
preparedChecks.push(prepareCheck(currentNode, currentNode.consequent, "if", { condition: true }));
|
||||
if (currentNode.alternate && currentNode.alternate.type !== "IfStatement") {
|
||||
preparedChecks.push(prepareCheck(currentNode, currentNode.alternate, "else"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const parent = node.parent;
|
||||
const isElseIf = parent.type === "IfStatement" && parent.alternate === node;
|
||||
|
||||
if (!isElseIf) {
|
||||
|
||||
// This is a top `if`, check the whole `if-else-if` chain
|
||||
prepareIfChecks(node).forEach(preparedCheck => {
|
||||
preparedCheck.check();
|
||||
});
|
||||
}
|
||||
|
||||
// Skip `else if`, it's already checked (when the top `if` was visited)
|
||||
},
|
||||
|
||||
WhileStatement(node) {
|
||||
prepareCheck(node, node.body, "while", { condition: true }).check();
|
||||
},
|
||||
|
||||
DoWhileStatement(node) {
|
||||
prepareCheck(node, node.body, "do").check();
|
||||
},
|
||||
|
||||
ForStatement(node) {
|
||||
prepareCheck(node, node.body, "for", { condition: true }).check();
|
||||
},
|
||||
|
||||
ForInStatement(node) {
|
||||
prepareCheck(node, node.body, "for-in").check();
|
||||
},
|
||||
|
||||
ForOfStatement(node) {
|
||||
prepareCheck(node, node.body, "for-of").check();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce default clauses in switch statements to be last
|
||||
* @author Milos Djermanovic
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce default clauses in switch statements to be last",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/default-case-last"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
notLast: "Default clause should be the last clause."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
SwitchStatement(node) {
|
||||
const cases = node.cases,
|
||||
indexOfDefault = cases.findIndex(c => c.test === null);
|
||||
|
||||
if (indexOfDefault !== -1 && indexOfDefault !== cases.length - 1) {
|
||||
const defaultClause = cases[indexOfDefault];
|
||||
|
||||
context.report({ node: defaultClause, messageId: "notLast" });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @fileoverview require default case in switch statements
|
||||
* @author Aliaksei Shytkin
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const DEFAULT_COMMENT_PATTERN = /^no default$/iu;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require `default` cases in `switch` statements",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/default-case"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
commentPattern: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
missingDefaultCase: "Expected a default case."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const commentPattern = options.commentPattern
|
||||
? new RegExp(options.commentPattern, "u")
|
||||
: DEFAULT_COMMENT_PATTERN;
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Shortcut to get last element of array
|
||||
* @param {*[]} collection Array
|
||||
* @returns {any} 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.getCommentsAfter(lastCase);
|
||||
|
||||
if (comments.length) {
|
||||
comment = last(comments);
|
||||
}
|
||||
|
||||
if (!comment || !commentPattern.test(comment.value.trim())) {
|
||||
context.report({ node, messageId: "missingDefaultCase" });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @fileoverview enforce default parameters to be last
|
||||
* @author Chiawen Chen
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce default parameters to be last",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/default-param-last"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
shouldBeLast: "Default parameters should be last."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/**
|
||||
* Handler for function contexts.
|
||||
* @param {ASTNode} node function node
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleFunction(node) {
|
||||
let hasSeenPlainParam = false;
|
||||
|
||||
for (let i = node.params.length - 1; i >= 0; i -= 1) {
|
||||
const param = node.params[i];
|
||||
|
||||
if (
|
||||
param.type !== "AssignmentPattern" &&
|
||||
param.type !== "RestElement"
|
||||
) {
|
||||
hasSeenPlainParam = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasSeenPlainParam && param.type === "AssignmentPattern") {
|
||||
context.report({
|
||||
node: param,
|
||||
messageId: "shouldBeLast"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
FunctionDeclaration: handleFunction,
|
||||
FunctionExpression: handleFunction,
|
||||
ArrowFunctionExpression: handleFunction
|
||||
};
|
||||
}
|
||||
};
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @fileoverview Validates newlines before and after dots
|
||||
* @author Greg Cochard
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent newlines before and after dots",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/dot-location"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["object", "property"]
|
||||
}
|
||||
],
|
||||
|
||||
fixable: "code",
|
||||
|
||||
messages: {
|
||||
expectedDotAfterObject: "Expected dot to be on same line as object.",
|
||||
expectedDotBeforeProperty: "Expected dot to be on same line as property."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const config = context.options[0];
|
||||
|
||||
// default to onObject if no preference is passed
|
||||
const onObject = config === "object" || !config;
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Reports if the dot between object and property is on the correct location.
|
||||
* @param {ASTNode} node The `MemberExpression` node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkDotLocation(node) {
|
||||
const property = node.property;
|
||||
const dotToken = sourceCode.getTokenBefore(property);
|
||||
|
||||
if (onObject) {
|
||||
|
||||
// `obj` expression can be parenthesized, but those paren tokens are not a part of the `obj` node.
|
||||
const tokenBeforeDot = sourceCode.getTokenBefore(dotToken);
|
||||
|
||||
if (!astUtils.isTokenOnSameLine(tokenBeforeDot, dotToken)) {
|
||||
context.report({
|
||||
node,
|
||||
loc: dotToken.loc,
|
||||
messageId: "expectedDotAfterObject",
|
||||
*fix(fixer) {
|
||||
if (dotToken.value.startsWith(".") && astUtils.isDecimalIntegerNumericToken(tokenBeforeDot)) {
|
||||
yield fixer.insertTextAfter(tokenBeforeDot, ` ${dotToken.value}`);
|
||||
} else {
|
||||
yield fixer.insertTextAfter(tokenBeforeDot, dotToken.value);
|
||||
}
|
||||
yield fixer.remove(dotToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (!astUtils.isTokenOnSameLine(dotToken, property)) {
|
||||
context.report({
|
||||
node,
|
||||
loc: dotToken.loc,
|
||||
messageId: "expectedDotBeforeProperty",
|
||||
*fix(fixer) {
|
||||
yield fixer.remove(dotToken);
|
||||
yield fixer.insertTextBefore(property, dotToken.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the spacing of the dot within a member expression.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkNode(node) {
|
||||
if (!node.computed) {
|
||||
checkDotLocation(node);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
MemberExpression: checkNode
|
||||
};
|
||||
}
|
||||
};
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* @fileoverview Rule to warn about using dot notation instead of square bracket notation when possible.
|
||||
* @author Josh Perez
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
const keywords = require("./utils/keywords");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/u;
|
||||
|
||||
// `null` literal must be handled separately.
|
||||
const literalTypesToCheck = new Set(["string", "boolean"]);
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce dot notation whenever possible",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/dot-notation"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowKeywords: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
},
|
||||
allowPattern: {
|
||||
type: "string",
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
fixable: "code",
|
||||
|
||||
messages: {
|
||||
useDot: "[{{key}}] is better written in dot notation.",
|
||||
useBrackets: ".{{key}} is a syntax error."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const allowKeywords = options.allowKeywords === void 0 || options.allowKeywords;
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
let allowPattern;
|
||||
|
||||
if (options.allowPattern) {
|
||||
allowPattern = new RegExp(options.allowPattern, "u");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the property is valid dot notation
|
||||
* @param {ASTNode} node The dot notation node
|
||||
* @param {string} value Value which is to be checked
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkComputedProperty(node, value) {
|
||||
if (
|
||||
validIdentifier.test(value) &&
|
||||
(allowKeywords || !keywords.includes(String(value))) &&
|
||||
!(allowPattern && allowPattern.test(value))
|
||||
) {
|
||||
const formattedValue = node.property.type === "Literal" ? JSON.stringify(value) : `\`${value}\``;
|
||||
|
||||
context.report({
|
||||
node: node.property,
|
||||
messageId: "useDot",
|
||||
data: {
|
||||
key: formattedValue
|
||||
},
|
||||
*fix(fixer) {
|
||||
const leftBracket = sourceCode.getTokenAfter(node.object, astUtils.isOpeningBracketToken);
|
||||
const rightBracket = sourceCode.getLastToken(node);
|
||||
const nextToken = sourceCode.getTokenAfter(node);
|
||||
|
||||
// Don't perform any fixes if there are comments inside the brackets.
|
||||
if (sourceCode.commentsExistBetween(leftBracket, rightBracket)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the brackets by an identifier.
|
||||
if (!node.optional) {
|
||||
yield fixer.insertTextBefore(
|
||||
leftBracket,
|
||||
astUtils.isDecimalInteger(node.object) ? " ." : "."
|
||||
);
|
||||
}
|
||||
yield fixer.replaceTextRange(
|
||||
[leftBracket.range[0], rightBracket.range[1]],
|
||||
value
|
||||
);
|
||||
|
||||
// Insert a space after the property if it will be connected to the next token.
|
||||
if (
|
||||
nextToken &&
|
||||
rightBracket.range[1] === nextToken.range[0] &&
|
||||
!astUtils.canTokensBeAdjacent(String(value), nextToken)
|
||||
) {
|
||||
yield fixer.insertTextAfter(node, " ");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
if (
|
||||
node.computed &&
|
||||
node.property.type === "Literal" &&
|
||||
(literalTypesToCheck.has(typeof node.property.value) || astUtils.isNullLiteral(node.property))
|
||||
) {
|
||||
checkComputedProperty(node, node.property.value);
|
||||
}
|
||||
if (
|
||||
node.computed &&
|
||||
astUtils.isStaticTemplateLiteral(node.property)
|
||||
) {
|
||||
checkComputedProperty(node, node.property.quasis[0].value.cooked);
|
||||
}
|
||||
if (
|
||||
!allowKeywords &&
|
||||
!node.computed &&
|
||||
node.property.type === "Identifier" &&
|
||||
keywords.includes(String(node.property.name))
|
||||
) {
|
||||
context.report({
|
||||
node: node.property,
|
||||
messageId: "useBrackets",
|
||||
data: {
|
||||
key: node.property.name
|
||||
},
|
||||
*fix(fixer) {
|
||||
const dotToken = sourceCode.getTokenBefore(node.property);
|
||||
|
||||
// A statement that starts with `let[` is parsed as a destructuring variable declaration, not a MemberExpression.
|
||||
if (node.object.type === "Identifier" && node.object.name === "let" && !node.optional) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't perform any fixes if there are comments between the dot and the property name.
|
||||
if (sourceCode.commentsExistBetween(dotToken, node.property)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the identifier to brackets.
|
||||
if (!node.optional) {
|
||||
yield fixer.remove(dotToken);
|
||||
}
|
||||
yield fixer.replaceText(node.property, `["${node.property.name}"]`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @fileoverview Require or disallow newline at the end of files
|
||||
* @author Nodeca Team <https://github.com/nodeca>
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Require or disallow newline at the end of files",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/eol-last"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "never", "unix", "windows"]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
missing: "Newline required at end of file but not found.",
|
||||
unexpected: "Newline not allowed at end of file."
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program: function checkBadEOF(node) {
|
||||
const sourceCode = context.sourceCode,
|
||||
src = sourceCode.getText(),
|
||||
lastLine = sourceCode.lines[sourceCode.lines.length - 1],
|
||||
location = {
|
||||
column: lastLine.length,
|
||||
line: sourceCode.lines.length
|
||||
},
|
||||
LF = "\n",
|
||||
CRLF = `\r${LF}`,
|
||||
endsWithNewline = src.endsWith(LF);
|
||||
|
||||
/*
|
||||
* Empty source is always valid: No content in file so we don't
|
||||
* need to lint for a newline on the last line of content.
|
||||
*/
|
||||
if (!src.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
messageId: "missing",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfterRange([0, src.length], appendCRLF ? CRLF : LF);
|
||||
}
|
||||
});
|
||||
} else if (mode === "never" && endsWithNewline) {
|
||||
|
||||
const secondLastLine = sourceCode.lines[sourceCode.lines.length - 2];
|
||||
|
||||
// File is newline-terminated, but shouldn't be
|
||||
context.report({
|
||||
node,
|
||||
loc: {
|
||||
start: { line: sourceCode.lines.length - 1, column: secondLastLine.length },
|
||||
end: { line: sourceCode.lines.length, column: 0 }
|
||||
},
|
||||
messageId: "unexpected",
|
||||
fix(fixer) {
|
||||
const finalEOLs = /(?:\r?\n)+$/u,
|
||||
match = finalEOLs.exec(sourceCode.text),
|
||||
start = match.index,
|
||||
end = sourceCode.text.length;
|
||||
|
||||
return fixer.replaceTextRange([start, end], "");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag statements that use != and == instead of !== and ===
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require the use of `===` and `!==`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/eqeqeq"
|
||||
},
|
||||
|
||||
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",
|
||||
|
||||
messages: {
|
||||
unexpected: "Expected '{{expectedOperator}}' and instead saw '{{actualOperator}}'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0] || "always";
|
||||
const options = context.options[1] || {};
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const operatorToken = sourceCode.getFirstTokenBetween(
|
||||
node.left,
|
||||
node.right,
|
||||
token => token.value === node.operator
|
||||
);
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: operatorToken.loc,
|
||||
messageId: "unexpected",
|
||||
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)) {
|
||||
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}=`);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @fileoverview enforce "for" loop update clause moving the counter in the right direction.(for-direction)
|
||||
* @author Aladdin-ADD<hh_2013@foxmail.com>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const { getStaticValue } = require("@eslint-community/eslint-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Enforce \"for\" loop update clause moving the counter in the right direction",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/for-direction"
|
||||
},
|
||||
|
||||
fixable: null,
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
incorrectDirection: "The update clause in this loop moves the variable in the wrong direction."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
|
||||
/**
|
||||
* report an error.
|
||||
* @param {ASTNode} node the node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "incorrectDirection"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* check the right side of the assignment
|
||||
* @param {ASTNode} update UpdateExpression to check
|
||||
* @param {int} dir expected direction that could either be turned around or invalidated
|
||||
* @returns {int} return dir, the negated dir, or zero if the counter does not change or the direction is not clear
|
||||
*/
|
||||
function getRightDirection(update, dir) {
|
||||
const staticValue = getStaticValue(update.right, sourceCode.getScope(update));
|
||||
|
||||
if (staticValue && ["bigint", "boolean", "number"].includes(typeof staticValue.value)) {
|
||||
const sign = Math.sign(Number(staticValue.value)) || 0; // convert NaN to 0
|
||||
|
||||
return dir * sign;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* check UpdateExpression add/sub the counter
|
||||
* @param {ASTNode} update UpdateExpression to check
|
||||
* @param {string} counter variable name to check
|
||||
* @returns {int} if add return 1, if sub return -1, if nochange, return 0
|
||||
*/
|
||||
function getUpdateDirection(update, counter) {
|
||||
if (update.argument.type === "Identifier" && update.argument.name === counter) {
|
||||
if (update.operator === "++") {
|
||||
return 1;
|
||||
}
|
||||
if (update.operator === "--") {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* check AssignmentExpression add/sub the counter
|
||||
* @param {ASTNode} update AssignmentExpression to check
|
||||
* @param {string} counter variable name to check
|
||||
* @returns {int} if add return 1, if sub return -1, if nochange, return 0
|
||||
*/
|
||||
function getAssignmentDirection(update, counter) {
|
||||
if (update.left.name === counter) {
|
||||
if (update.operator === "+=") {
|
||||
return getRightDirection(update, 1);
|
||||
}
|
||||
if (update.operator === "-=") {
|
||||
return getRightDirection(update, -1);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return {
|
||||
ForStatement(node) {
|
||||
|
||||
if (node.test && node.test.type === "BinaryExpression" && node.update) {
|
||||
for (const counterPosition of ["left", "right"]) {
|
||||
if (node.test[counterPosition].type !== "Identifier") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const counter = node.test[counterPosition].name;
|
||||
const operator = node.test.operator;
|
||||
const update = node.update;
|
||||
|
||||
let wrongDirection;
|
||||
|
||||
if (operator === "<" || operator === "<=") {
|
||||
wrongDirection = counterPosition === "left" ? -1 : 1;
|
||||
} else if (operator === ">" || operator === ">=") {
|
||||
wrongDirection = counterPosition === "left" ? 1 : -1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (update.type === "UpdateExpression") {
|
||||
if (getUpdateDirection(update, counter) === wrongDirection) {
|
||||
report(node);
|
||||
}
|
||||
} else if (update.type === "AssignmentExpression" && getAssignmentDirection(update, counter) === wrongDirection) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* @fileoverview Rule to control spacing within function calls
|
||||
* @author Matt DuVall <http://www.mattduvall.com>
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Require or disallow spacing between function identifiers and their invocations",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/func-call-spacing"
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
messages: {
|
||||
unexpectedWhitespace: "Unexpected whitespace between function name and paren.",
|
||||
unexpectedNewline: "Unexpected newline between function name and paren.",
|
||||
missing: "Missing space between function name and paren."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const never = context.options[0] !== "always";
|
||||
const allowNewlines = !never && context.options[1] && context.options[1].allowNewlines;
|
||||
const sourceCode = context.sourceCode;
|
||||
const text = sourceCode.getText();
|
||||
|
||||
/**
|
||||
* Check if open space is present in a function name
|
||||
* @param {ASTNode} node node to evaluate
|
||||
* @param {Token} leftToken The last token of the callee. This may be the closing parenthesis that encloses the callee.
|
||||
* @param {Token} rightToken Tha first token of the arguments. this is the opening parenthesis that encloses the arguments.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkSpacing(node, leftToken, rightToken) {
|
||||
const textBetweenTokens = text.slice(leftToken.range[1], rightToken.range[0]).replace(/\/\*.*?\*\//gu, "");
|
||||
const hasWhitespace = /\s/u.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: {
|
||||
start: leftToken.loc.end,
|
||||
end: {
|
||||
line: rightToken.loc.start.line,
|
||||
column: rightToken.loc.start.column - 1
|
||||
}
|
||||
},
|
||||
messageId: "unexpectedWhitespace",
|
||||
fix(fixer) {
|
||||
|
||||
// Don't remove comments.
|
||||
if (sourceCode.commentsExistBetween(leftToken, rightToken)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If `?.` exists, it doesn't hide no-unexpected-multiline errors
|
||||
if (node.optional) {
|
||||
return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], "?.");
|
||||
}
|
||||
|
||||
/*
|
||||
* Only autofix if there is no newline
|
||||
* https://github.com/eslint/eslint/issues/7787
|
||||
*/
|
||||
if (hasNewline) {
|
||||
return null;
|
||||
}
|
||||
return fixer.removeRange([leftToken.range[1], rightToken.range[0]]);
|
||||
}
|
||||
});
|
||||
} else if (!never && !hasWhitespace) {
|
||||
context.report({
|
||||
node,
|
||||
loc: {
|
||||
start: {
|
||||
line: leftToken.loc.end.line,
|
||||
column: leftToken.loc.end.column - 1
|
||||
},
|
||||
end: rightToken.loc.start
|
||||
},
|
||||
messageId: "missing",
|
||||
fix(fixer) {
|
||||
if (node.optional) {
|
||||
return null; // Not sure if inserting a space to either before/after `?.` token.
|
||||
}
|
||||
return fixer.insertTextBefore(rightToken, " ");
|
||||
}
|
||||
});
|
||||
} else if (!never && !allowNewlines && hasNewline) {
|
||||
context.report({
|
||||
node,
|
||||
loc: {
|
||||
start: leftToken.loc.end,
|
||||
end: rightToken.loc.start
|
||||
},
|
||||
messageId: "unexpectedNewline",
|
||||
fix(fixer) {
|
||||
|
||||
/*
|
||||
* Only autofix if there is no newline
|
||||
* https://github.com/eslint/eslint/issues/7787
|
||||
* But if `?.` exists, it doesn't hide no-unexpected-multiline errors
|
||||
*/
|
||||
if (!node.optional) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't remove comments.
|
||||
if (sourceCode.commentsExistBetween(leftToken, rightToken)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const range = [leftToken.range[1], rightToken.range[0]];
|
||||
const qdToken = sourceCode.getTokenAfter(leftToken);
|
||||
|
||||
if (qdToken.range[0] === leftToken.range[1]) {
|
||||
return fixer.replaceTextRange(range, "?. ");
|
||||
}
|
||||
if (qdToken.range[1] === rightToken.range[0]) {
|
||||
return fixer.replaceTextRange(range, " ?.");
|
||||
}
|
||||
return fixer.replaceTextRange(range, " ?. ");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"CallExpression, NewExpression"(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, astUtils.isNotQuestionDotToken);
|
||||
|
||||
// Parens in NewExpression are optional
|
||||
if (!(parenToken && parenToken.range[1] < node.range[1])) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkSpacing(node, prevToken, parenToken);
|
||||
},
|
||||
|
||||
ImportExpression(node) {
|
||||
const leftToken = sourceCode.getFirstToken(node);
|
||||
const rightToken = sourceCode.getTokenAfter(leftToken);
|
||||
|
||||
checkSpacing(node, leftToken, rightToken);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* @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("./utils/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 >= 2015) {
|
||||
return esutils.keyword.isIdentifierES6(name);
|
||||
}
|
||||
return esutils.keyword.isIdentifierES5(name);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const alwaysOrNever = { enum: ["always", "never"] };
|
||||
const optionsObject = {
|
||||
type: "object",
|
||||
properties: {
|
||||
considerPropertyDescriptor: {
|
||||
type: "boolean"
|
||||
},
|
||||
includeCommonJSModuleExports: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
};
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require function names to match the name of the variable or property to which they are assigned",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/func-name-matching"
|
||||
},
|
||||
|
||||
schema: {
|
||||
anyOf: [{
|
||||
type: "array",
|
||||
additionalItems: false,
|
||||
items: [alwaysOrNever, optionsObject]
|
||||
}, {
|
||||
type: "array",
|
||||
additionalItems: false,
|
||||
items: [optionsObject]
|
||||
}]
|
||||
},
|
||||
|
||||
messages: {
|
||||
matchProperty: "Function name `{{funcName}}` should match property name `{{name}}`.",
|
||||
matchVariable: "Function name `{{funcName}}` should match variable name `{{name}}`.",
|
||||
notMatchProperty: "Function name `{{funcName}}` should not match property name `{{name}}`.",
|
||||
notMatchVariable: "Function name `{{funcName}}` should not match variable name `{{name}}`."
|
||||
}
|
||||
},
|
||||
|
||||
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 considerPropertyDescriptor = options.considerPropertyDescriptor;
|
||||
const includeModuleExports = options.includeCommonJSModuleExports;
|
||||
const ecmaVersion = context.languageOptions.ecmaVersion;
|
||||
|
||||
/**
|
||||
* Check whether node is a certain CallExpression.
|
||||
* @param {string} objName object name
|
||||
* @param {string} funcName function name
|
||||
* @param {ASTNode} node The node to check
|
||||
* @returns {boolean} `true` if node matches CallExpression
|
||||
*/
|
||||
function isPropertyCall(objName, funcName, node) {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
return node.type === "CallExpression" && astUtils.isSpecificMemberAccess(node.callee, objName, funcName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 messageId;
|
||||
|
||||
if (nameMatches === "always" && isProp) {
|
||||
messageId = "matchProperty";
|
||||
} else if (nameMatches === "always") {
|
||||
messageId = "matchVariable";
|
||||
} else if (isProp) {
|
||||
messageId = "notMatchProperty";
|
||||
} else {
|
||||
messageId = "notMatchVariable";
|
||||
}
|
||||
context.report({
|
||||
node,
|
||||
messageId,
|
||||
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 && name && isIdentifier(name) && shouldWarn(name, node.right.id.name)) {
|
||||
report(node, name, node.right.id.name, isProp);
|
||||
}
|
||||
},
|
||||
|
||||
"Property, PropertyDefinition[value]"(node) {
|
||||
if (!(node.value.type === "FunctionExpression" && node.value.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.key.type === "Identifier" && !node.computed) {
|
||||
const functionName = node.value.id.name;
|
||||
let propertyName = node.key.name;
|
||||
|
||||
if (
|
||||
considerPropertyDescriptor &&
|
||||
propertyName === "value" &&
|
||||
node.parent.type === "ObjectExpression"
|
||||
) {
|
||||
if (isPropertyCall("Object", "defineProperty", node.parent.parent) || isPropertyCall("Reflect", "defineProperty", node.parent.parent)) {
|
||||
const property = node.parent.parent.arguments[1];
|
||||
|
||||
if (isStringLiteral(property) && shouldWarn(property.value, functionName)) {
|
||||
report(node, property.value, functionName, true);
|
||||
}
|
||||
} else if (isPropertyCall("Object", "defineProperties", node.parent.parent.parent.parent)) {
|
||||
propertyName = node.parent.parent.key.name;
|
||||
if (!node.parent.parent.computed && shouldWarn(propertyName, functionName)) {
|
||||
report(node, propertyName, functionName, true);
|
||||
}
|
||||
} else if (isPropertyCall("Object", "create", node.parent.parent.parent.parent)) {
|
||||
propertyName = node.parent.parent.key.name;
|
||||
if (!node.parent.parent.computed && shouldWarn(propertyName, functionName)) {
|
||||
report(node, propertyName, functionName, true);
|
||||
}
|
||||
} else if (shouldWarn(propertyName, functionName)) {
|
||||
report(node, propertyName, functionName, true);
|
||||
}
|
||||
} else if (shouldWarn(propertyName, functionName)) {
|
||||
report(node, propertyName, functionName, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* @fileoverview Rule to warn when a function expression does not have a name.
|
||||
* @author Kyle T. Nunery
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
/**
|
||||
* Checks whether or not a given variable is a function name.
|
||||
* @param {eslint-scope.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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require or disallow named `function` expressions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/func-names"
|
||||
},
|
||||
|
||||
schema: {
|
||||
definitions: {
|
||||
value: {
|
||||
enum: [
|
||||
"always",
|
||||
"as-needed",
|
||||
"never"
|
||||
]
|
||||
}
|
||||
},
|
||||
items: [
|
||||
{
|
||||
$ref: "#/definitions/value"
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
generators: {
|
||||
$ref: "#/definitions/value"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
messages: {
|
||||
unnamed: "Unexpected unnamed {{name}}.",
|
||||
named: "Unexpected named {{name}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Returns the config option for the given node.
|
||||
* @param {ASTNode} node A node to get the config for.
|
||||
* @returns {string} The config option.
|
||||
*/
|
||||
function getConfigForNode(node) {
|
||||
if (
|
||||
node.generator &&
|
||||
context.options.length > 1 &&
|
||||
context.options[1].generators
|
||||
) {
|
||||
return context.options[1].generators;
|
||||
}
|
||||
|
||||
return context.options[0] || "always";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 === "PropertyDefinition" && parent.value === node) ||
|
||||
(parent.type === "AssignmentExpression" && parent.left.type === "Identifier" && parent.right === node) ||
|
||||
(parent.type === "AssignmentPattern" && parent.left.type === "Identifier" && parent.right === node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that an unnamed function should be named
|
||||
* @param {ASTNode} node The node to report in the event of an error.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportUnexpectedUnnamedFunction(node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unnamed",
|
||||
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
|
||||
data: { name: astUtils.getFunctionNameWithKind(node) }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that a named function should be unnamed
|
||||
* @param {ASTNode} node The node to report in the event of an error.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportUnexpectedNamedFunction(node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "named",
|
||||
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
|
||||
data: { name: astUtils.getFunctionNameWithKind(node) }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener for function nodes.
|
||||
* @param {ASTNode} node function node
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleFunction(node) {
|
||||
|
||||
// Skip recursive functions.
|
||||
const nameVar = sourceCode.getDeclaredVariables(node)[0];
|
||||
|
||||
if (isFunctionName(nameVar) && nameVar.references.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasName = Boolean(node.id && node.id.name);
|
||||
const config = getConfigForNode(node);
|
||||
|
||||
if (config === "never") {
|
||||
if (hasName && node.type !== "FunctionDeclaration") {
|
||||
reportUnexpectedNamedFunction(node);
|
||||
}
|
||||
} else if (config === "as-needed") {
|
||||
if (!hasName && !hasInferredName(node)) {
|
||||
reportUnexpectedUnnamedFunction(node);
|
||||
}
|
||||
} else {
|
||||
if (!hasName && !isObjectOrClassMethod(node)) {
|
||||
reportUnexpectedUnnamedFunction(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"FunctionExpression:exit": handleFunction,
|
||||
"ExportDefaultDeclaration > FunctionDeclaration": handleFunction
|
||||
};
|
||||
}
|
||||
};
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce a particular function style
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce the consistent use of either `function` declarations or expressions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/func-style"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["declaration", "expression"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowArrowFunctions: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
expression: "Expected a function expression.",
|
||||
declaration: "Expected a function declaration."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const style = context.options[0],
|
||||
allowArrowFunctions = context.options[1] && context.options[1].allowArrowFunctions,
|
||||
enforceDeclarations = (style === "declaration"),
|
||||
stack = [];
|
||||
|
||||
const nodesToCheck = {
|
||||
FunctionDeclaration(node) {
|
||||
stack.push(false);
|
||||
|
||||
if (!enforceDeclarations && node.parent.type !== "ExportDefaultDeclaration") {
|
||||
context.report({ node, messageId: "expression" });
|
||||
}
|
||||
},
|
||||
"FunctionDeclaration:exit"() {
|
||||
stack.pop();
|
||||
},
|
||||
|
||||
FunctionExpression(node) {
|
||||
stack.push(false);
|
||||
|
||||
if (enforceDeclarations && node.parent.type === "VariableDeclarator") {
|
||||
context.report({ node: node.parent, messageId: "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, messageId: "declaration" });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return nodesToCheck;
|
||||
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+125
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce line breaks between arguments of a function call
|
||||
* @author Alexey Gonchar <https://github.com/finico>
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce line breaks between arguments of a function call",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/function-call-argument-newline"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "never", "consistent"]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedLineBreak: "There should be no line break here.",
|
||||
missingLineBreak: "There should be a line break after this argument."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
const checkers = {
|
||||
unexpected: {
|
||||
messageId: "unexpectedLineBreak",
|
||||
check: (prevToken, currentToken) => prevToken.loc.end.line !== currentToken.loc.start.line,
|
||||
createFix: (token, tokenBefore) => fixer =>
|
||||
fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], " ")
|
||||
},
|
||||
missing: {
|
||||
messageId: "missingLineBreak",
|
||||
check: (prevToken, currentToken) => prevToken.loc.end.line === currentToken.loc.start.line,
|
||||
createFix: (token, tokenBefore) => fixer =>
|
||||
fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], "\n")
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check all arguments for line breaks in the CallExpression
|
||||
* @param {CallExpression} node node to evaluate
|
||||
* @param {{ messageId: string, check: Function }} checker selected checker
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkArguments(node, checker) {
|
||||
for (let i = 1; i < node.arguments.length; i++) {
|
||||
const prevArgToken = sourceCode.getLastToken(node.arguments[i - 1]);
|
||||
const currentArgToken = sourceCode.getFirstToken(node.arguments[i]);
|
||||
|
||||
if (checker.check(prevArgToken, currentArgToken)) {
|
||||
const tokenBefore = sourceCode.getTokenBefore(
|
||||
currentArgToken,
|
||||
{ includeComments: true }
|
||||
);
|
||||
|
||||
const hasLineCommentBefore = tokenBefore.type === "Line";
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: {
|
||||
start: tokenBefore.loc.end,
|
||||
end: currentArgToken.loc.start
|
||||
},
|
||||
messageId: checker.messageId,
|
||||
fix: hasLineCommentBefore ? null : checker.createFix(currentArgToken, tokenBefore)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if open space is present in a function name
|
||||
* @param {CallExpression} node node to evaluate
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function check(node) {
|
||||
if (node.arguments.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const option = context.options[0] || "always";
|
||||
|
||||
if (option === "never") {
|
||||
checkArguments(node, checkers.unexpected);
|
||||
} else if (option === "always") {
|
||||
checkArguments(node, checkers.missing);
|
||||
} else if (option === "consistent") {
|
||||
const firstArgToken = sourceCode.getLastToken(node.arguments[0]);
|
||||
const secondArgToken = sourceCode.getFirstToken(node.arguments[1]);
|
||||
|
||||
if (firstArgToken.loc.end.line === secondArgToken.loc.start.line) {
|
||||
checkArguments(node, checkers.unexpected);
|
||||
} else {
|
||||
checkArguments(node, checkers.missing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression: check,
|
||||
NewExpression: check
|
||||
};
|
||||
}
|
||||
};
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* @fileoverview enforce consistent line breaks inside function parentheses
|
||||
* @author Teddy Katz
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent line breaks inside function parentheses",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/function-paren-newline"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["always", "never", "consistent", "multiline", "multiline-arguments"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
minItems: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
expectedBefore: "Expected newline before ')'.",
|
||||
expectedAfter: "Expected newline after '('.",
|
||||
expectedBetween: "Expected newline between arguments/params.",
|
||||
unexpectedBefore: "Unexpected newline before ')'.",
|
||||
unexpectedAfter: "Unexpected newline after '('."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const rawOption = context.options[0] || "multiline";
|
||||
const multilineOption = rawOption === "multiline";
|
||||
const multilineArgumentsOption = rawOption === "multiline-arguments";
|
||||
const consistentOption = rawOption === "consistent";
|
||||
let minItems;
|
||||
|
||||
if (typeof rawOption === "object") {
|
||||
minItems = rawOption.minItems;
|
||||
} else if (rawOption === "always") {
|
||||
minItems = 0;
|
||||
} else if (rawOption === "never") {
|
||||
minItems = Infinity;
|
||||
} else {
|
||||
minItems = null;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Helpers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines whether there should be newlines inside function parens
|
||||
* @param {ASTNode[]} elements The arguments or parameters in the list
|
||||
* @param {boolean} hasLeftNewline `true` if the left paren has a newline in the current code.
|
||||
* @returns {boolean} `true` if there should be newlines inside the function parens
|
||||
*/
|
||||
function shouldHaveNewlines(elements, hasLeftNewline) {
|
||||
if (multilineArgumentsOption && elements.length === 1) {
|
||||
return hasLeftNewline;
|
||||
}
|
||||
if (multilineOption || multilineArgumentsOption) {
|
||||
return elements.some((element, index) => index !== elements.length - 1 && element.loc.end.line !== elements[index + 1].loc.start.line);
|
||||
}
|
||||
if (consistentOption) {
|
||||
return hasLeftNewline;
|
||||
}
|
||||
return elements.length >= minItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates parens
|
||||
* @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
|
||||
* @param {ASTNode[]} elements The arguments or parameters in the list
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateParens(parens, elements) {
|
||||
const leftParen = parens.leftParen;
|
||||
const rightParen = parens.rightParen;
|
||||
const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
|
||||
const tokenBeforeRightParen = sourceCode.getTokenBefore(rightParen);
|
||||
const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen);
|
||||
const hasRightNewline = !astUtils.isTokenOnSameLine(tokenBeforeRightParen, rightParen);
|
||||
const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);
|
||||
|
||||
if (hasLeftNewline && !needsNewlines) {
|
||||
context.report({
|
||||
node: leftParen,
|
||||
messageId: "unexpectedAfter",
|
||||
fix(fixer) {
|
||||
return sourceCode.getText().slice(leftParen.range[1], tokenAfterLeftParen.range[0]).trim()
|
||||
|
||||
// If there is a comment between the ( and the first element, don't do a fix.
|
||||
? null
|
||||
: fixer.removeRange([leftParen.range[1], tokenAfterLeftParen.range[0]]);
|
||||
}
|
||||
});
|
||||
} else if (!hasLeftNewline && needsNewlines) {
|
||||
context.report({
|
||||
node: leftParen,
|
||||
messageId: "expectedAfter",
|
||||
fix: fixer => fixer.insertTextAfter(leftParen, "\n")
|
||||
});
|
||||
}
|
||||
|
||||
if (hasRightNewline && !needsNewlines) {
|
||||
context.report({
|
||||
node: rightParen,
|
||||
messageId: "unexpectedBefore",
|
||||
fix(fixer) {
|
||||
return sourceCode.getText().slice(tokenBeforeRightParen.range[1], rightParen.range[0]).trim()
|
||||
|
||||
// If there is a comment between the last element and the ), don't do a fix.
|
||||
? null
|
||||
: fixer.removeRange([tokenBeforeRightParen.range[1], rightParen.range[0]]);
|
||||
}
|
||||
});
|
||||
} else if (!hasRightNewline && needsNewlines) {
|
||||
context.report({
|
||||
node: rightParen,
|
||||
messageId: "expectedBefore",
|
||||
fix: fixer => fixer.insertTextBefore(rightParen, "\n")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a list of arguments or parameters
|
||||
* @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
|
||||
* @param {ASTNode[]} elements The arguments or parameters in the list
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateArguments(parens, elements) {
|
||||
const leftParen = parens.leftParen;
|
||||
const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
|
||||
const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen);
|
||||
const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);
|
||||
|
||||
for (let i = 0; i <= elements.length - 2; i++) {
|
||||
const currentElement = elements[i];
|
||||
const nextElement = elements[i + 1];
|
||||
const hasNewLine = currentElement.loc.end.line !== nextElement.loc.start.line;
|
||||
|
||||
if (!hasNewLine && needsNewlines) {
|
||||
context.report({
|
||||
node: currentElement,
|
||||
messageId: "expectedBetween",
|
||||
fix: fixer => fixer.insertTextBefore(nextElement, "\n")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the left paren and right paren tokens of a node.
|
||||
* @param {ASTNode} node The node with parens
|
||||
* @throws {TypeError} Unexpected node type.
|
||||
* @returns {Object} An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token.
|
||||
* Can also return `null` if an expression has no parens (e.g. a NewExpression with no arguments, or an ArrowFunctionExpression
|
||||
* with a single parameter)
|
||||
*/
|
||||
function getParenTokens(node) {
|
||||
switch (node.type) {
|
||||
case "NewExpression":
|
||||
if (!node.arguments.length &&
|
||||
!(
|
||||
astUtils.isOpeningParenToken(sourceCode.getLastToken(node, { skip: 1 })) &&
|
||||
astUtils.isClosingParenToken(sourceCode.getLastToken(node)) &&
|
||||
node.callee.range[1] < node.range[1]
|
||||
)
|
||||
) {
|
||||
|
||||
// If the NewExpression does not have parens (e.g. `new Foo`), return null.
|
||||
return null;
|
||||
}
|
||||
|
||||
// falls through
|
||||
|
||||
case "CallExpression":
|
||||
return {
|
||||
leftParen: sourceCode.getTokenAfter(node.callee, astUtils.isOpeningParenToken),
|
||||
rightParen: sourceCode.getLastToken(node)
|
||||
};
|
||||
|
||||
case "FunctionDeclaration":
|
||||
case "FunctionExpression": {
|
||||
const leftParen = sourceCode.getFirstToken(node, astUtils.isOpeningParenToken);
|
||||
const rightParen = node.params.length
|
||||
? sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isClosingParenToken)
|
||||
: sourceCode.getTokenAfter(leftParen);
|
||||
|
||||
return { leftParen, rightParen };
|
||||
}
|
||||
|
||||
case "ArrowFunctionExpression": {
|
||||
const firstToken = sourceCode.getFirstToken(node, { skip: (node.async ? 1 : 0) });
|
||||
|
||||
if (!astUtils.isOpeningParenToken(firstToken)) {
|
||||
|
||||
// If the ArrowFunctionExpression has a single param without parens, return null.
|
||||
return null;
|
||||
}
|
||||
|
||||
const rightParen = node.params.length
|
||||
? sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isClosingParenToken)
|
||||
: sourceCode.getTokenAfter(firstToken);
|
||||
|
||||
return {
|
||||
leftParen: firstToken,
|
||||
rightParen
|
||||
};
|
||||
}
|
||||
|
||||
case "ImportExpression": {
|
||||
const leftParen = sourceCode.getFirstToken(node, 1);
|
||||
const rightParen = sourceCode.getLastToken(node);
|
||||
|
||||
return { leftParen, rightParen };
|
||||
}
|
||||
|
||||
default:
|
||||
throw new TypeError(`unexpected node with type ${node.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
[[
|
||||
"ArrowFunctionExpression",
|
||||
"CallExpression",
|
||||
"FunctionDeclaration",
|
||||
"FunctionExpression",
|
||||
"ImportExpression",
|
||||
"NewExpression"
|
||||
]](node) {
|
||||
const parens = getParenTokens(node);
|
||||
let params;
|
||||
|
||||
if (node.type === "ImportExpression") {
|
||||
params = [node.source];
|
||||
} else if (astUtils.isFunction(node)) {
|
||||
params = node.params;
|
||||
} else {
|
||||
params = node.arguments;
|
||||
}
|
||||
|
||||
if (parens) {
|
||||
validateParens(parens, params);
|
||||
|
||||
if (multilineArgumentsOption) {
|
||||
validateArguments(parens, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* @fileoverview Rule to check the spacing around the * in generator functions.
|
||||
* @author Jamund Ferguson
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const OVERRIDE_SCHEMA = {
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["before", "after", "both", "neither"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
before: { type: "boolean" },
|
||||
after: { type: "boolean" }
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent spacing around `*` operators in generator functions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/generator-star-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["before", "after", "both", "neither"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
before: { type: "boolean" },
|
||||
after: { type: "boolean" },
|
||||
named: OVERRIDE_SCHEMA,
|
||||
anonymous: OVERRIDE_SCHEMA,
|
||||
method: OVERRIDE_SCHEMA
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
missingBefore: "Missing space before *.",
|
||||
missingAfter: "Missing space after *.",
|
||||
unexpectedBefore: "Unexpected space before *.",
|
||||
unexpectedAfter: "Unexpected space after *."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const optionDefinitions = {
|
||||
before: { before: true, after: false },
|
||||
after: { before: false, after: true },
|
||||
both: { before: true, after: true },
|
||||
neither: { before: false, after: false }
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns resolved option definitions based on an option and defaults
|
||||
* @param {any} option The option object or string value
|
||||
* @param {Object} defaults The defaults to use if options are not present
|
||||
* @returns {Object} the resolved object definition
|
||||
*/
|
||||
function optionToDefinition(option, defaults) {
|
||||
if (!option) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
return typeof option === "string"
|
||||
? optionDefinitions[option]
|
||||
: Object.assign({}, defaults, option);
|
||||
}
|
||||
|
||||
const modes = (function(option) {
|
||||
const defaults = optionToDefinition(option, optionDefinitions.before);
|
||||
|
||||
return {
|
||||
named: optionToDefinition(option.named, defaults),
|
||||
anonymous: optionToDefinition(option.anonymous, defaults),
|
||||
method: optionToDefinition(option.method, defaults)
|
||||
};
|
||||
}(context.options[0] || {}));
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* 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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* capitalize a given string.
|
||||
* @param {string} str the given string.
|
||||
* @returns {string} the capitalized string.
|
||||
*/
|
||||
function capitalize(str) {
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the spacing between two tokens before or after the star token.
|
||||
* @param {string} kind Either "named", "anonymous", or "method"
|
||||
* @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(kind, side, leftToken, rightToken) {
|
||||
if (!!(rightToken.range[0] - leftToken.range[1]) !== modes[kind][side]) {
|
||||
const after = leftToken.value === "*";
|
||||
const spaceRequired = modes[kind][side];
|
||||
const node = after ? leftToken : rightToken;
|
||||
const messageId = `${spaceRequired ? "missing" : "unexpected"}${capitalize(side)}`;
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId,
|
||||
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);
|
||||
const prevToken = sourceCode.getTokenBefore(starToken);
|
||||
const nextToken = sourceCode.getTokenAfter(starToken);
|
||||
|
||||
let kind = "named";
|
||||
|
||||
if (node.parent.type === "MethodDefinition" || (node.parent.type === "Property" && node.parent.method)) {
|
||||
kind = "method";
|
||||
} else if (!node.id) {
|
||||
kind = "anonymous";
|
||||
}
|
||||
|
||||
// Only check before when preceded by `function`|`static` keyword
|
||||
if (!(kind === "method" && starToken === sourceCode.getFirstToken(node.parent))) {
|
||||
checkSpacing(kind, "before", prevToken, starToken);
|
||||
}
|
||||
|
||||
checkSpacing(kind, "after", starToken, nextToken);
|
||||
}
|
||||
|
||||
return {
|
||||
FunctionDeclaration: checkFunction,
|
||||
FunctionExpression: checkFunction
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* @fileoverview Enforces that a return statement is present in property getters.
|
||||
* @author Aladdin-ADD(hh_2013@foxmail.com)
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u;
|
||||
|
||||
/**
|
||||
* Checks all segments in a set and returns true if any are reachable.
|
||||
* @param {Set<CodePathSegment>} segments The segments to check.
|
||||
* @returns {boolean} True if any segment is reachable; false otherwise.
|
||||
*/
|
||||
function isAnySegmentReachable(segments) {
|
||||
|
||||
for (const segment of segments) {
|
||||
if (segment.reachable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Enforce `return` statements in getters",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/getter-return"
|
||||
},
|
||||
|
||||
fixable: null,
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowImplicit: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
expected: "Expected to return a value in {{name}}.",
|
||||
expectedAlways: "Expected {{name}} to always return a value."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const options = context.options[0] || { allowImplicit: false };
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
let funcInfo = {
|
||||
upper: null,
|
||||
codePath: null,
|
||||
hasReturn: false,
|
||||
shouldCheck: false,
|
||||
node: null,
|
||||
currentSegments: []
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 &&
|
||||
isAnySegmentReachable(funcInfo.currentSegments)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
|
||||
messageId: funcInfo.hasReturn ? "expectedAlways" : "expected",
|
||||
data: {
|
||||
name: astUtils.getFunctionNameWithKind(funcInfo.node)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a node means a getter function.
|
||||
* @param {ASTNode} node a node to check.
|
||||
* @returns {boolean} if node means a getter, return true; else return false.
|
||||
*/
|
||||
function isGetter(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
if (TARGET_NODE_TYPE.test(node.type) && node.body.type === "BlockStatement") {
|
||||
if (parent.kind === "get") {
|
||||
return true;
|
||||
}
|
||||
if (parent.type === "Property" && astUtils.getStaticPropertyName(parent) === "get" && parent.parent.type === "ObjectExpression") {
|
||||
|
||||
// Object.defineProperty() or Reflect.defineProperty()
|
||||
if (parent.parent.parent.type === "CallExpression") {
|
||||
const callNode = parent.parent.parent.callee;
|
||||
|
||||
if (astUtils.isSpecificMemberAccess(callNode, "Object", "defineProperty") ||
|
||||
astUtils.isSpecificMemberAccess(callNode, "Reflect", "defineProperty")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Object.defineProperties() or Object.create()
|
||||
if (parent.parent.parent.type === "Property" &&
|
||||
parent.parent.parent.parent.type === "ObjectExpression" &&
|
||||
parent.parent.parent.parent.parent.type === "CallExpression") {
|
||||
const callNode = parent.parent.parent.parent.parent.callee;
|
||||
|
||||
return astUtils.isSpecificMemberAccess(callNode, "Object", "defineProperties") ||
|
||||
astUtils.isSpecificMemberAccess(callNode, "Object", "create");
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
|
||||
// Stacks this function's information.
|
||||
onCodePathStart(codePath, node) {
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
codePath,
|
||||
hasReturn: false,
|
||||
shouldCheck: isGetter(node),
|
||||
node,
|
||||
currentSegments: new Set()
|
||||
};
|
||||
},
|
||||
|
||||
// Pops this function's information.
|
||||
onCodePathEnd() {
|
||||
funcInfo = funcInfo.upper;
|
||||
},
|
||||
onUnreachableCodePathSegmentStart(segment) {
|
||||
funcInfo.currentSegments.add(segment);
|
||||
},
|
||||
|
||||
onUnreachableCodePathSegmentEnd(segment) {
|
||||
funcInfo.currentSegments.delete(segment);
|
||||
},
|
||||
|
||||
onCodePathSegmentStart(segment) {
|
||||
funcInfo.currentSegments.add(segment);
|
||||
},
|
||||
|
||||
onCodePathSegmentEnd(segment) {
|
||||
funcInfo.currentSegments.delete(segment);
|
||||
},
|
||||
|
||||
// Checks the return statement is valid.
|
||||
ReturnStatement(node) {
|
||||
if (funcInfo.shouldCheck) {
|
||||
funcInfo.hasReturn = true;
|
||||
|
||||
// if allowImplicit: false, should also check node.argument
|
||||
if (!options.allowImplicit && !node.argument) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "expected",
|
||||
data: {
|
||||
name: astUtils.getFunctionNameWithKind(funcInfo.node)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Reports a given function if the last path is reachable.
|
||||
"FunctionExpression:exit": checkLastSegment,
|
||||
"ArrowFunctionExpression:exit": checkLastSegment
|
||||
};
|
||||
}
|
||||
};
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* @fileoverview Rule for disallowing require() outside of the top-level module context
|
||||
* @author Jamund Ferguson
|
||||
* @deprecated in ESLint v7.0.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const ACCEPTABLE_PARENTS = new Set([
|
||||
"AssignmentExpression",
|
||||
"VariableDeclarator",
|
||||
"MemberExpression",
|
||||
"ExpressionStatement",
|
||||
"CallExpression",
|
||||
"ConditionalExpression",
|
||||
"Program",
|
||||
"VariableDeclaration",
|
||||
"ChainExpression"
|
||||
]);
|
||||
|
||||
/**
|
||||
* Finds the eslint-scope 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];
|
||||
}
|
||||
|
||||
/* c8 ignore next */
|
||||
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;
|
||||
}
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: [],
|
||||
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require `require()` calls to be placed at top-level module scope",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/global-require"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
messages: {
|
||||
unexpected: "Unexpected require()."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const currentScope = sourceCode.getScope(node);
|
||||
|
||||
if (node.callee.name === "require" && !isShadowed(currentScope, node.callee)) {
|
||||
const isGoodRequire = sourceCode.getAncestors(node).every(parent => ACCEPTABLE_PARENTS.has(parent.type));
|
||||
|
||||
if (!isGoodRequire) {
|
||||
context.report({ node, messageId: "unexpected" });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* @fileoverview Rule to require grouped accessor pairs in object literals and classes
|
||||
* @author Milos Djermanovic
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Property name if it can be computed statically, otherwise the list of the tokens of the key node.
|
||||
* @typedef {string|Token[]} Key
|
||||
*/
|
||||
|
||||
/**
|
||||
* Accessor nodes with the same key.
|
||||
* @typedef {Object} AccessorData
|
||||
* @property {Key} key Accessor's key
|
||||
* @property {ASTNode[]} getters List of getter nodes.
|
||||
* @property {ASTNode[]} setters List of setter nodes.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not the given lists represent the equal tokens in the same order.
|
||||
* Tokens are compared by their properties, not by instance.
|
||||
* @param {Token[]} left First list of tokens.
|
||||
* @param {Token[]} right Second list of tokens.
|
||||
* @returns {boolean} `true` if the lists have same tokens.
|
||||
*/
|
||||
function areEqualTokenLists(left, right) {
|
||||
if (left.length !== right.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
const leftToken = left[i],
|
||||
rightToken = right[i];
|
||||
|
||||
if (leftToken.type !== rightToken.type || leftToken.value !== rightToken.value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not the given keys are equal.
|
||||
* @param {Key} left First key.
|
||||
* @param {Key} right Second key.
|
||||
* @returns {boolean} `true` if the keys are equal.
|
||||
*/
|
||||
function areEqualKeys(left, right) {
|
||||
if (typeof left === "string" && typeof right === "string") {
|
||||
|
||||
// Statically computed names.
|
||||
return left === right;
|
||||
}
|
||||
if (Array.isArray(left) && Array.isArray(right)) {
|
||||
|
||||
// Token lists.
|
||||
return areEqualTokenLists(left, right);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is of an accessor kind ('get' or 'set').
|
||||
* @param {ASTNode} node A node to check.
|
||||
* @returns {boolean} `true` if the node is of an accessor kind.
|
||||
*/
|
||||
function isAccessorKind(node) {
|
||||
return node.kind === "get" || node.kind === "set";
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require grouped accessor pairs in object literals and classes",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/grouped-accessor-pairs"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["anyOrder", "getBeforeSet", "setBeforeGet"]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
notGrouped: "Accessor pair {{ formerName }} and {{ latterName }} should be grouped.",
|
||||
invalidOrder: "Expected {{ latterName }} to be before {{ formerName }}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const order = context.options[0] || "anyOrder";
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Reports the given accessor pair.
|
||||
* @param {string} messageId messageId to report.
|
||||
* @param {ASTNode} formerNode getter/setter node that is defined before `latterNode`.
|
||||
* @param {ASTNode} latterNode getter/setter node that is defined after `formerNode`.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function report(messageId, formerNode, latterNode) {
|
||||
context.report({
|
||||
node: latterNode,
|
||||
messageId,
|
||||
loc: astUtils.getFunctionHeadLoc(latterNode.value, sourceCode),
|
||||
data: {
|
||||
formerName: astUtils.getFunctionNameWithKind(formerNode.value),
|
||||
latterName: astUtils.getFunctionNameWithKind(latterNode.value)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks accessor pairs in the given list of nodes.
|
||||
* @param {ASTNode[]} nodes The list to check.
|
||||
* @param {Function} shouldCheck – Predicate that returns `true` if the node should be checked.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkList(nodes, shouldCheck) {
|
||||
const accessors = [];
|
||||
let found = false;
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
|
||||
if (shouldCheck(node) && isAccessorKind(node)) {
|
||||
|
||||
// Creates a new `AccessorData` object for the given getter or setter node.
|
||||
const name = astUtils.getStaticPropertyName(node);
|
||||
const key = (name !== null) ? name : sourceCode.getTokens(node.key);
|
||||
|
||||
// Merges the given `AccessorData` object into the given accessors list.
|
||||
for (let j = 0; j < accessors.length; j++) {
|
||||
const accessor = accessors[j];
|
||||
|
||||
if (areEqualKeys(accessor.key, key)) {
|
||||
accessor.getters.push(...node.kind === "get" ? [node] : []);
|
||||
accessor.setters.push(...node.kind === "set" ? [node] : []);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
accessors.push({
|
||||
key,
|
||||
getters: node.kind === "get" ? [node] : [],
|
||||
setters: node.kind === "set" ? [node] : []
|
||||
});
|
||||
}
|
||||
found = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const { getters, setters } of accessors) {
|
||||
|
||||
// Don't report accessor properties that have duplicate getters or setters.
|
||||
if (getters.length === 1 && setters.length === 1) {
|
||||
const [getter] = getters,
|
||||
[setter] = setters,
|
||||
getterIndex = nodes.indexOf(getter),
|
||||
setterIndex = nodes.indexOf(setter),
|
||||
formerNode = getterIndex < setterIndex ? getter : setter,
|
||||
latterNode = getterIndex < setterIndex ? setter : getter;
|
||||
|
||||
if (Math.abs(getterIndex - setterIndex) > 1) {
|
||||
report("notGrouped", formerNode, latterNode);
|
||||
} else if (
|
||||
(order === "getBeforeSet" && getterIndex > setterIndex) ||
|
||||
(order === "setBeforeGet" && getterIndex < setterIndex)
|
||||
) {
|
||||
report("invalidOrder", formerNode, latterNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ObjectExpression(node) {
|
||||
checkList(node.properties, n => n.type === "Property");
|
||||
},
|
||||
ClassBody(node) {
|
||||
checkList(node.body, n => n.type === "MethodDefinition" && !n.static);
|
||||
checkList(node.body, n => n.type === "MethodDefinition" && n.static);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag for-in loops without if statements inside
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require `for-in` loops to include an `if` statement",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/guard-for-in"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
messages: {
|
||||
wrap: "The body of a for-in should be wrapped in an if statement to filter unwanted properties from the prototype."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
|
||||
ForInStatement(node) {
|
||||
const body = node.body;
|
||||
|
||||
// empty statement
|
||||
if (body.type === "EmptyStatement") {
|
||||
return;
|
||||
}
|
||||
|
||||
// if statement
|
||||
if (body.type === "IfStatement") {
|
||||
return;
|
||||
}
|
||||
|
||||
// empty block
|
||||
if (body.type === "BlockStatement" && body.body.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// block with just if statement
|
||||
if (body.type === "BlockStatement" && body.body.length === 1 && body.body[0].type === "IfStatement") {
|
||||
return;
|
||||
}
|
||||
|
||||
// block that starts with if statement
|
||||
if (body.type === "BlockStatement" && body.body.length >= 1 && body.body[0].type === "IfStatement") {
|
||||
const i = body.body[0];
|
||||
|
||||
// ... whose consequent is a continue
|
||||
if (i.consequent.type === "ContinueStatement") {
|
||||
return;
|
||||
}
|
||||
|
||||
// ... whose consequent is a block that contains only a continue
|
||||
if (i.consequent.type === "BlockStatement" && i.consequent.body.length === 1 && i.consequent.body[0].type === "ContinueStatement") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
context.report({ node, messageId: "wrap" });
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @fileoverview Ensure handling of errors when we know they exist.
|
||||
* @author Jamund Ferguson
|
||||
* @deprecated in ESLint v7.0.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: [],
|
||||
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require error handling in callbacks",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/handle-callback-err"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "string"
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
expected: "Expected error to be handled."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const errorArgument = context.options[0] || "err";
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* 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, "u");
|
||||
|
||||
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 = sourceCode.getScope(node),
|
||||
parameters = getParameters(scope),
|
||||
firstParameter = parameters[0];
|
||||
|
||||
if (firstParameter && matchesConfiguredErrorName(firstParameter.name)) {
|
||||
if (firstParameter.references.length === 0) {
|
||||
context.report({ node, messageId: "expected" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
FunctionDeclaration: checkForError,
|
||||
FunctionExpression: checkForError,
|
||||
ArrowFunctionExpression: checkForError
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* @fileoverview Rule that warns when identifier names that are
|
||||
* specified in the configuration are used.
|
||||
* @author Keith Cirkel (http://keithcirkel.co.uk)
|
||||
* @deprecated in ESLint v7.5.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether the given node represents assignment target in a normal assignment or destructuring.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} `true` if the node is assignment target.
|
||||
*/
|
||||
function isAssignmentTarget(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
return (
|
||||
|
||||
// normal assignment
|
||||
(
|
||||
parent.type === "AssignmentExpression" &&
|
||||
parent.left === node
|
||||
) ||
|
||||
|
||||
// destructuring
|
||||
parent.type === "ArrayPattern" ||
|
||||
parent.type === "RestElement" ||
|
||||
(
|
||||
parent.type === "Property" &&
|
||||
parent.value === node &&
|
||||
parent.parent.type === "ObjectPattern"
|
||||
) ||
|
||||
(
|
||||
parent.type === "AssignmentPattern" &&
|
||||
parent.left === node
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given node represents an imported name that is renamed in the same import/export specifier.
|
||||
*
|
||||
* Examples:
|
||||
* import { a as b } from 'mod'; // node `a` is renamed import
|
||||
* export { a as b } from 'mod'; // node `a` is renamed import
|
||||
* @param {ASTNode} node `Identifier` node to check.
|
||||
* @returns {boolean} `true` if the node is a renamed import.
|
||||
*/
|
||||
function isRenamedImport(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
return (
|
||||
(
|
||||
parent.type === "ImportSpecifier" &&
|
||||
parent.imported !== parent.local &&
|
||||
parent.imported === node
|
||||
) ||
|
||||
(
|
||||
parent.type === "ExportSpecifier" &&
|
||||
parent.parent.source && // re-export
|
||||
parent.local !== parent.exported &&
|
||||
parent.local === node
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given node is a renamed identifier node in an ObjectPattern destructuring.
|
||||
*
|
||||
* Examples:
|
||||
* const { a : b } = foo; // node `a` is renamed node.
|
||||
* @param {ASTNode} node `Identifier` node to check.
|
||||
* @returns {boolean} `true` if the node is a renamed node in an ObjectPattern destructuring.
|
||||
*/
|
||||
function isRenamedInDestructuring(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
return (
|
||||
(
|
||||
!parent.computed &&
|
||||
parent.type === "Property" &&
|
||||
parent.parent.type === "ObjectPattern" &&
|
||||
parent.value !== node &&
|
||||
parent.key === node
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given node represents shorthand definition of a property in an object literal.
|
||||
* @param {ASTNode} node `Identifier` node to check.
|
||||
* @returns {boolean} `true` if the node is a shorthand property definition.
|
||||
*/
|
||||
function isShorthandPropertyDefinition(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
return (
|
||||
parent.type === "Property" &&
|
||||
parent.parent.type === "ObjectExpression" &&
|
||||
parent.shorthand
|
||||
);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: ["id-denylist"],
|
||||
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow specified identifiers",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/id-blacklist"
|
||||
},
|
||||
|
||||
schema: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
},
|
||||
uniqueItems: true
|
||||
},
|
||||
messages: {
|
||||
restricted: "Identifier '{{name}}' is restricted."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const denyList = new Set(context.options);
|
||||
const reportedNodes = new Set();
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
let globalScope;
|
||||
|
||||
/**
|
||||
* Checks whether the given name is restricted.
|
||||
* @param {string} name The name to check.
|
||||
* @returns {boolean} `true` if the name is restricted.
|
||||
* @private
|
||||
*/
|
||||
function isRestricted(name) {
|
||||
return denyList.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given node represents a reference to a global variable that is not declared in the source code.
|
||||
* These identifiers will be allowed, as it is assumed that user has no control over the names of external global variables.
|
||||
* @param {ASTNode} node `Identifier` node to check.
|
||||
* @returns {boolean} `true` if the node is a reference to a global variable.
|
||||
*/
|
||||
function isReferenceToGlobalVariable(node) {
|
||||
const variable = globalScope.set.get(node.name);
|
||||
|
||||
return variable && variable.defs.length === 0 &&
|
||||
variable.references.some(ref => ref.identifier === node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given node should be checked.
|
||||
* @param {ASTNode} node `Identifier` node.
|
||||
* @returns {boolean} `true` if the node should be checked.
|
||||
*/
|
||||
function shouldCheck(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
/*
|
||||
* Member access has special rules for checking property names.
|
||||
* Read access to a property with a restricted name is allowed, because it can be on an object that user has no control over.
|
||||
* Write access isn't allowed, because it potentially creates a new property with a restricted name.
|
||||
*/
|
||||
if (
|
||||
parent.type === "MemberExpression" &&
|
||||
parent.property === node &&
|
||||
!parent.computed
|
||||
) {
|
||||
return isAssignmentTarget(parent);
|
||||
}
|
||||
|
||||
return (
|
||||
parent.type !== "CallExpression" &&
|
||||
parent.type !== "NewExpression" &&
|
||||
!isRenamedImport(node) &&
|
||||
!isRenamedInDestructuring(node) &&
|
||||
!(
|
||||
isReferenceToGlobalVariable(node) &&
|
||||
!isShorthandPropertyDefinition(node)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports an AST node as a rule violation.
|
||||
* @param {ASTNode} node The node to report.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function report(node) {
|
||||
|
||||
/*
|
||||
* We used the range instead of the node because it's possible
|
||||
* for the same identifier to be represented by two different
|
||||
* nodes, with the most clear example being shorthand properties:
|
||||
* { foo }
|
||||
* In this case, "foo" is represented by one node for the name
|
||||
* and one for the value. The only way to know they are the same
|
||||
* is to look at the range.
|
||||
*/
|
||||
if (!reportedNodes.has(node.range.toString())) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "restricted",
|
||||
data: {
|
||||
name: node.name
|
||||
}
|
||||
});
|
||||
reportedNodes.add(node.range.toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Program(node) {
|
||||
globalScope = sourceCode.getScope(node);
|
||||
},
|
||||
|
||||
Identifier(node) {
|
||||
if (isRestricted(node.name) && shouldCheck(node)) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* @fileoverview Rule that warns when identifier names that are
|
||||
* specified in the configuration are used.
|
||||
* @author Keith Cirkel (http://keithcirkel.co.uk)
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether the given node represents assignment target in a normal assignment or destructuring.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} `true` if the node is assignment target.
|
||||
*/
|
||||
function isAssignmentTarget(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
return (
|
||||
|
||||
// normal assignment
|
||||
(
|
||||
parent.type === "AssignmentExpression" &&
|
||||
parent.left === node
|
||||
) ||
|
||||
|
||||
// destructuring
|
||||
parent.type === "ArrayPattern" ||
|
||||
parent.type === "RestElement" ||
|
||||
(
|
||||
parent.type === "Property" &&
|
||||
parent.value === node &&
|
||||
parent.parent.type === "ObjectPattern"
|
||||
) ||
|
||||
(
|
||||
parent.type === "AssignmentPattern" &&
|
||||
parent.left === node
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given node represents an imported name that is renamed in the same import/export specifier.
|
||||
*
|
||||
* Examples:
|
||||
* import { a as b } from 'mod'; // node `a` is renamed import
|
||||
* export { a as b } from 'mod'; // node `a` is renamed import
|
||||
* @param {ASTNode} node `Identifier` node to check.
|
||||
* @returns {boolean} `true` if the node is a renamed import.
|
||||
*/
|
||||
function isRenamedImport(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
return (
|
||||
(
|
||||
parent.type === "ImportSpecifier" &&
|
||||
parent.imported !== parent.local &&
|
||||
parent.imported === node
|
||||
) ||
|
||||
(
|
||||
parent.type === "ExportSpecifier" &&
|
||||
parent.parent.source && // re-export
|
||||
parent.local !== parent.exported &&
|
||||
parent.local === node
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given node is an ObjectPattern destructuring.
|
||||
*
|
||||
* Examples:
|
||||
* const { a : b } = foo;
|
||||
* @param {ASTNode} node `Identifier` node to check.
|
||||
* @returns {boolean} `true` if the node is in an ObjectPattern destructuring.
|
||||
*/
|
||||
function isPropertyNameInDestructuring(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
return (
|
||||
(
|
||||
!parent.computed &&
|
||||
parent.type === "Property" &&
|
||||
parent.parent.type === "ObjectPattern" &&
|
||||
parent.key === node
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow specified identifiers",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/id-denylist"
|
||||
},
|
||||
|
||||
schema: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
},
|
||||
uniqueItems: true
|
||||
},
|
||||
messages: {
|
||||
restricted: "Identifier '{{name}}' is restricted.",
|
||||
restrictedPrivate: "Identifier '#{{name}}' is restricted."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const denyList = new Set(context.options);
|
||||
const reportedNodes = new Set();
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
let globalScope;
|
||||
|
||||
/**
|
||||
* Checks whether the given name is restricted.
|
||||
* @param {string} name The name to check.
|
||||
* @returns {boolean} `true` if the name is restricted.
|
||||
* @private
|
||||
*/
|
||||
function isRestricted(name) {
|
||||
return denyList.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given node represents a reference to a global variable that is not declared in the source code.
|
||||
* These identifiers will be allowed, as it is assumed that user has no control over the names of external global variables.
|
||||
* @param {ASTNode} node `Identifier` node to check.
|
||||
* @returns {boolean} `true` if the node is a reference to a global variable.
|
||||
*/
|
||||
function isReferenceToGlobalVariable(node) {
|
||||
const variable = globalScope.set.get(node.name);
|
||||
|
||||
return variable && variable.defs.length === 0 &&
|
||||
variable.references.some(ref => ref.identifier === node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given node should be checked.
|
||||
* @param {ASTNode} node `Identifier` node.
|
||||
* @returns {boolean} `true` if the node should be checked.
|
||||
*/
|
||||
function shouldCheck(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
/*
|
||||
* Member access has special rules for checking property names.
|
||||
* Read access to a property with a restricted name is allowed, because it can be on an object that user has no control over.
|
||||
* Write access isn't allowed, because it potentially creates a new property with a restricted name.
|
||||
*/
|
||||
if (
|
||||
parent.type === "MemberExpression" &&
|
||||
parent.property === node &&
|
||||
!parent.computed
|
||||
) {
|
||||
return isAssignmentTarget(parent);
|
||||
}
|
||||
|
||||
return (
|
||||
parent.type !== "CallExpression" &&
|
||||
parent.type !== "NewExpression" &&
|
||||
!isRenamedImport(node) &&
|
||||
!isPropertyNameInDestructuring(node) &&
|
||||
!isReferenceToGlobalVariable(node)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports an AST node as a rule violation.
|
||||
* @param {ASTNode} node The node to report.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function report(node) {
|
||||
|
||||
/*
|
||||
* We used the range instead of the node because it's possible
|
||||
* for the same identifier to be represented by two different
|
||||
* nodes, with the most clear example being shorthand properties:
|
||||
* { foo }
|
||||
* In this case, "foo" is represented by one node for the name
|
||||
* and one for the value. The only way to know they are the same
|
||||
* is to look at the range.
|
||||
*/
|
||||
if (!reportedNodes.has(node.range.toString())) {
|
||||
const isPrivate = node.type === "PrivateIdentifier";
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: isPrivate ? "restrictedPrivate" : "restricted",
|
||||
data: {
|
||||
name: node.name
|
||||
}
|
||||
});
|
||||
reportedNodes.add(node.range.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Program(node) {
|
||||
globalScope = sourceCode.getScope(node);
|
||||
},
|
||||
|
||||
[[
|
||||
"Identifier",
|
||||
"PrivateIdentifier"
|
||||
]](node) {
|
||||
if (isRestricted(node.name) && shouldCheck(node)) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* @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";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const { getGraphemeCount } = require("../shared/string-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce minimum and maximum identifier lengths",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/id-length"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
min: {
|
||||
type: "integer",
|
||||
default: 2
|
||||
},
|
||||
max: {
|
||||
type: "integer"
|
||||
},
|
||||
exceptions: {
|
||||
type: "array",
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
exceptionPatterns: {
|
||||
type: "array",
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
properties: {
|
||||
enum: ["always", "never"]
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
tooShort: "Identifier name '{{name}}' is too short (< {{min}}).",
|
||||
tooShortPrivate: "Identifier name '#{{name}}' is too short (< {{min}}).",
|
||||
tooLong: "Identifier name '{{name}}' is too long (> {{max}}).",
|
||||
tooLongPrivate: "Identifier name #'{{name}}' is too long (> {{max}})."
|
||||
}
|
||||
},
|
||||
|
||||
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 = new Set(options.exceptions);
|
||||
const exceptionPatterns = (options.exceptionPatterns || []).map(pattern => new RegExp(pattern, "u"));
|
||||
const reportedNodes = new Set();
|
||||
|
||||
/**
|
||||
* Checks if a string matches the provided exception patterns
|
||||
* @param {string} name The string to check.
|
||||
* @returns {boolean} if the string is a match
|
||||
* @private
|
||||
*/
|
||||
function matchesExceptionPattern(name) {
|
||||
return exceptionPatterns.some(pattern => pattern.test(name));
|
||||
}
|
||||
|
||||
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(parent, node) {
|
||||
|
||||
if (parent.parent.type === "ObjectPattern") {
|
||||
const isKeyAndValueSame = parent.value.name === parent.key.name;
|
||||
|
||||
return (
|
||||
!isKeyAndValueSame && parent.value === node ||
|
||||
isKeyAndValueSame && parent.key === node && properties
|
||||
);
|
||||
}
|
||||
return properties && !parent.computed && parent.key.name === node.name;
|
||||
},
|
||||
ImportDefaultSpecifier: true,
|
||||
RestElement: true,
|
||||
FunctionExpression: true,
|
||||
ArrowFunctionExpression: true,
|
||||
ClassDeclaration: true,
|
||||
FunctionDeclaration: true,
|
||||
MethodDefinition: true,
|
||||
PropertyDefinition: true,
|
||||
CatchClause: true,
|
||||
ArrayPattern: true
|
||||
};
|
||||
|
||||
return {
|
||||
[[
|
||||
"Identifier",
|
||||
"PrivateIdentifier"
|
||||
]](node) {
|
||||
const name = node.name;
|
||||
const parent = node.parent;
|
||||
|
||||
const nameLength = getGraphemeCount(name);
|
||||
|
||||
const isShort = nameLength < minLength;
|
||||
const isLong = nameLength > maxLength;
|
||||
|
||||
if (!(isShort || isLong) || exceptions.has(name) || matchesExceptionPattern(name)) {
|
||||
return; // Nothing to report
|
||||
}
|
||||
|
||||
const isValidExpression = SUPPORTED_EXPRESSIONS[parent.type];
|
||||
|
||||
/*
|
||||
* We used the range instead of the node because it's possible
|
||||
* for the same identifier to be represented by two different
|
||||
* nodes, with the most clear example being shorthand properties:
|
||||
* { foo }
|
||||
* In this case, "foo" is represented by one node for the name
|
||||
* and one for the value. The only way to know they are the same
|
||||
* is to look at the range.
|
||||
*/
|
||||
if (isValidExpression && !reportedNodes.has(node.range.toString()) && (isValidExpression === true || isValidExpression(parent, node))) {
|
||||
reportedNodes.add(node.range.toString());
|
||||
|
||||
let messageId = isShort ? "tooShort" : "tooLong";
|
||||
|
||||
if (node.type === "PrivateIdentifier") {
|
||||
messageId += "Private";
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId,
|
||||
data: { name, min: minLength, max: maxLength }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag non-matching identifiers
|
||||
* @author Matthieu Larcher
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require identifiers to match a specified regular expression",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/id-match"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
properties: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
classFields: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
onlyDeclarations: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
ignoreDestructuring: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
notMatch: "Identifier '{{name}}' does not match the pattern '{{pattern}}'.",
|
||||
notMatchPrivate: "Identifier '#{{name}}' does not match the pattern '{{pattern}}'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
//--------------------------------------------------------------------------
|
||||
const pattern = context.options[0] || "^.+$",
|
||||
regexp = new RegExp(pattern, "u");
|
||||
|
||||
const options = context.options[1] || {},
|
||||
checkProperties = !!options.properties,
|
||||
checkClassFields = !!options.classFields,
|
||||
onlyDeclarations = !!options.onlyDeclarations,
|
||||
ignoreDestructuring = !!options.ignoreDestructuring;
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
let globalScope;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// contains reported nodes to avoid reporting twice on destructuring with shorthand notation
|
||||
const reportedNodes = new Set();
|
||||
const ALLOWED_PARENT_TYPES = new Set(["CallExpression", "NewExpression"]);
|
||||
const DECLARATION_TYPES = new Set(["FunctionDeclaration", "VariableDeclarator"]);
|
||||
const IMPORT_TYPES = new Set(["ImportSpecifier", "ImportNamespaceSpecifier", "ImportDefaultSpecifier"]);
|
||||
|
||||
/**
|
||||
* Checks whether the given node represents a reference to a global variable that is not declared in the source code.
|
||||
* These identifiers will be allowed, as it is assumed that user has no control over the names of external global variables.
|
||||
* @param {ASTNode} node `Identifier` node to check.
|
||||
* @returns {boolean} `true` if the node is a reference to a global variable.
|
||||
*/
|
||||
function isReferenceToGlobalVariable(node) {
|
||||
const variable = globalScope.set.get(node.name);
|
||||
|
||||
return variable && variable.defs.length === 0 &&
|
||||
variable.references.some(ref => ref.identifier === node);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a parent of a node is an ObjectPattern.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} if the node is inside an ObjectPattern
|
||||
* @private
|
||||
*/
|
||||
function isInsideObjectPattern(node) {
|
||||
let { parent } = node;
|
||||
|
||||
while (parent) {
|
||||
if (parent.type === "ObjectPattern") {
|
||||
return true;
|
||||
}
|
||||
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (!onlyDeclarations || DECLARATION_TYPES.has(effectiveParent.type)) &&
|
||||
!ALLOWED_PARENT_TYPES.has(effectiveParent.type) && isInvalid(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports an AST node as a rule violation.
|
||||
* @param {ASTNode} node The node to report.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function report(node) {
|
||||
|
||||
/*
|
||||
* We used the range instead of the node because it's possible
|
||||
* for the same identifier to be represented by two different
|
||||
* nodes, with the most clear example being shorthand properties:
|
||||
* { foo }
|
||||
* In this case, "foo" is represented by one node for the name
|
||||
* and one for the value. The only way to know they are the same
|
||||
* is to look at the range.
|
||||
*/
|
||||
if (!reportedNodes.has(node.range.toString())) {
|
||||
|
||||
const messageId = (node.type === "PrivateIdentifier")
|
||||
? "notMatchPrivate" : "notMatch";
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId,
|
||||
data: {
|
||||
name: node.name,
|
||||
pattern
|
||||
}
|
||||
});
|
||||
reportedNodes.add(node.range.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Program(node) {
|
||||
globalScope = sourceCode.getScope(node);
|
||||
},
|
||||
|
||||
Identifier(node) {
|
||||
const name = node.name,
|
||||
parent = node.parent,
|
||||
effectiveParent = (parent.type === "MemberExpression") ? parent.parent : parent;
|
||||
|
||||
if (isReferenceToGlobalVariable(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parent.type === "MemberExpression") {
|
||||
|
||||
if (!checkProperties) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always check object names
|
||||
if (parent.object.type === "Identifier" &&
|
||||
parent.object.name === name) {
|
||||
if (isInvalid(name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
// Report AssignmentExpressions left side's assigned variable id
|
||||
} else if (effectiveParent.type === "AssignmentExpression" &&
|
||||
effectiveParent.left.type === "MemberExpression" &&
|
||||
effectiveParent.left.property.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") {
|
||||
if (isInvalid(name)) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
|
||||
// For https://github.com/eslint/eslint/issues/15123
|
||||
} else if (
|
||||
parent.type === "Property" &&
|
||||
parent.parent.type === "ObjectExpression" &&
|
||||
parent.key === node &&
|
||||
!parent.computed
|
||||
) {
|
||||
if (checkProperties && isInvalid(name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
/*
|
||||
* Properties have their own rules, and
|
||||
* AssignmentPattern nodes can be treated like Properties:
|
||||
* e.g.: const { no_camelcased = false } = bar;
|
||||
*/
|
||||
} else if (parent.type === "Property" || parent.type === "AssignmentPattern") {
|
||||
|
||||
if (parent.parent && parent.parent.type === "ObjectPattern") {
|
||||
if (!ignoreDestructuring && parent.shorthand && parent.value.left && isInvalid(name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
const assignmentKeyEqualsValue = parent.key.name === parent.value.name;
|
||||
|
||||
// prevent checking righthand side of destructured object
|
||||
if (!assignmentKeyEqualsValue && parent.key === node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valueIsInvalid = parent.value.name && isInvalid(name);
|
||||
|
||||
// ignore destructuring if the option is set, unless a new identifier is created
|
||||
if (valueIsInvalid && !(assignmentKeyEqualsValue && ignoreDestructuring)) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
|
||||
// never check properties or always ignore destructuring
|
||||
if ((!checkProperties && !parent.computed) || (ignoreDestructuring && isInsideObjectPattern(node))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// don't check right hand side of AssignmentExpression to prevent duplicate warnings
|
||||
if (parent.right !== node && shouldReport(effectiveParent, name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
// Check if it's an import specifier
|
||||
} else if (IMPORT_TYPES.has(parent.type)) {
|
||||
|
||||
// Report only if the local imported identifier is invalid
|
||||
if (parent.local && parent.local.name === node.name && isInvalid(name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
} else if (parent.type === "PropertyDefinition") {
|
||||
|
||||
if (checkClassFields && isInvalid(name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
// Report anything that is invalid that isn't a CallExpression
|
||||
} else if (shouldReport(effectiveParent, name)) {
|
||||
report(node);
|
||||
}
|
||||
},
|
||||
|
||||
"PrivateIdentifier"(node) {
|
||||
|
||||
const isClassField = node.parent.type === "PropertyDefinition";
|
||||
|
||||
if (isClassField && !checkClassFields) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInvalid(node.name)) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @fileoverview enforce the location of arrow function bodies
|
||||
* @author Sharmila Jesupaul
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const { isCommentToken, isNotOpeningParenToken } = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce the location of arrow function bodies",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/implicit-arrow-linebreak"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["beside", "below"]
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
expected: "Expected a linebreak before this expression.",
|
||||
unexpected: "Expected no linebreak before this expression."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const option = context.options[0] || "beside";
|
||||
|
||||
/**
|
||||
* Validates the location of an arrow function body
|
||||
* @param {ASTNode} node The arrow function body
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateExpression(node) {
|
||||
if (node.body.type === "BlockStatement") {
|
||||
return;
|
||||
}
|
||||
|
||||
const arrowToken = sourceCode.getTokenBefore(node.body, isNotOpeningParenToken);
|
||||
const firstTokenOfBody = sourceCode.getTokenAfter(arrowToken);
|
||||
|
||||
if (arrowToken.loc.end.line === firstTokenOfBody.loc.start.line && option === "below") {
|
||||
context.report({
|
||||
node: firstTokenOfBody,
|
||||
messageId: "expected",
|
||||
fix: fixer => fixer.insertTextBefore(firstTokenOfBody, "\n")
|
||||
});
|
||||
} else if (arrowToken.loc.end.line !== firstTokenOfBody.loc.start.line && option === "beside") {
|
||||
context.report({
|
||||
node: firstTokenOfBody,
|
||||
messageId: "unexpected",
|
||||
fix(fixer) {
|
||||
if (sourceCode.getFirstTokenBetween(arrowToken, firstTokenOfBody, { includeComments: true, filter: isCommentToken })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fixer.replaceTextRange([arrowToken.range[1], firstTokenOfBody.range[0]], " ");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
return {
|
||||
ArrowFunctionExpression: node => validateExpression(node)
|
||||
};
|
||||
}
|
||||
};
|
||||
+1126
@@ -0,0 +1,1126 @@
|
||||
/**
|
||||
* @fileoverview This option sets a specific tab width for your code
|
||||
*
|
||||
* This rule has been ported and modified from nodeca.
|
||||
* @author Vitaly Puzrin
|
||||
* @author Gyandeep Singh
|
||||
* @deprecated in ESLint v4.0.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
// this rule has known coverage issues, but it's deprecated and shouldn't be updated in the future anyway.
|
||||
/* c8 ignore next */
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent indentation",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/indent-legacy"
|
||||
},
|
||||
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: ["indent"],
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["tab"]
|
||||
},
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
SwitchCase: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
VariableDeclarator: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
var: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
let: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
const: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
outerIIFEBody: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
MemberExpression: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
FunctionDeclaration: {
|
||||
type: "object",
|
||||
properties: {
|
||||
parameters: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
enum: ["first"]
|
||||
}
|
||||
]
|
||||
},
|
||||
body: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
FunctionExpression: {
|
||||
type: "object",
|
||||
properties: {
|
||||
parameters: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
enum: ["first"]
|
||||
}
|
||||
]
|
||||
},
|
||||
body: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
CallExpression: {
|
||||
type: "object",
|
||||
properties: {
|
||||
parameters: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
enum: ["first"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
ArrayExpression: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
enum: ["first"]
|
||||
}
|
||||
]
|
||||
},
|
||||
ObjectExpression: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
enum: ["first"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
expected: "Expected indentation of {{expected}} but found {{actual}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const DEFAULT_VARIABLE_INDENT = 1;
|
||||
const DEFAULT_PARAMETER_INDENT = null; // For backwards compatibility, don't check parameter indentation unless specified in the config
|
||||
const DEFAULT_FUNCTION_BODY_INDENT = 1;
|
||||
|
||||
let indentType = "space";
|
||||
let indentSize = 4;
|
||||
const options = {
|
||||
SwitchCase: 0,
|
||||
VariableDeclarator: {
|
||||
var: DEFAULT_VARIABLE_INDENT,
|
||||
let: DEFAULT_VARIABLE_INDENT,
|
||||
const: DEFAULT_VARIABLE_INDENT
|
||||
},
|
||||
outerIIFEBody: null,
|
||||
FunctionDeclaration: {
|
||||
parameters: DEFAULT_PARAMETER_INDENT,
|
||||
body: DEFAULT_FUNCTION_BODY_INDENT
|
||||
},
|
||||
FunctionExpression: {
|
||||
parameters: DEFAULT_PARAMETER_INDENT,
|
||||
body: DEFAULT_FUNCTION_BODY_INDENT
|
||||
},
|
||||
CallExpression: {
|
||||
arguments: DEFAULT_PARAMETER_INDENT
|
||||
},
|
||||
ArrayExpression: 1,
|
||||
ObjectExpression: 1
|
||||
};
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
if (context.options.length) {
|
||||
if (context.options[0] === "tab") {
|
||||
indentSize = 1;
|
||||
indentType = "tab";
|
||||
} else /* c8 ignore start */ if (typeof context.options[0] === "number") {
|
||||
indentSize = context.options[0];
|
||||
indentType = "space";
|
||||
}/* c8 ignore stop */
|
||||
|
||||
if (context.options[1]) {
|
||||
const opts = context.options[1];
|
||||
|
||||
options.SwitchCase = opts.SwitchCase || 0;
|
||||
const variableDeclaratorRules = opts.VariableDeclarator;
|
||||
|
||||
if (typeof variableDeclaratorRules === "number") {
|
||||
options.VariableDeclarator = {
|
||||
var: variableDeclaratorRules,
|
||||
let: variableDeclaratorRules,
|
||||
const: variableDeclaratorRules
|
||||
};
|
||||
} else if (typeof variableDeclaratorRules === "object") {
|
||||
Object.assign(options.VariableDeclarator, variableDeclaratorRules);
|
||||
}
|
||||
|
||||
if (typeof opts.outerIIFEBody === "number") {
|
||||
options.outerIIFEBody = opts.outerIIFEBody;
|
||||
}
|
||||
|
||||
if (typeof opts.MemberExpression === "number") {
|
||||
options.MemberExpression = opts.MemberExpression;
|
||||
}
|
||||
|
||||
if (typeof opts.FunctionDeclaration === "object") {
|
||||
Object.assign(options.FunctionDeclaration, opts.FunctionDeclaration);
|
||||
}
|
||||
|
||||
if (typeof opts.FunctionExpression === "object") {
|
||||
Object.assign(options.FunctionExpression, opts.FunctionExpression);
|
||||
}
|
||||
|
||||
if (typeof opts.CallExpression === "object") {
|
||||
Object.assign(options.CallExpression, opts.CallExpression);
|
||||
}
|
||||
|
||||
if (typeof opts.ArrayExpression === "number" || typeof opts.ArrayExpression === "string") {
|
||||
options.ArrayExpression = opts.ArrayExpression;
|
||||
}
|
||||
|
||||
if (typeof opts.ObjectExpression === "number" || typeof opts.ObjectExpression === "string") {
|
||||
options.ObjectExpression = opts.ObjectExpression;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const caseIndentStore = {};
|
||||
|
||||
/**
|
||||
* Creates an error message for a line, given the expected/actual indentation.
|
||||
* @param {int} expectedAmount The expected amount of indentation characters for this line
|
||||
* @param {int} actualSpaces The actual number of indentation spaces that were found on this line
|
||||
* @param {int} actualTabs The actual number of indentation tabs that were found on this line
|
||||
* @returns {string} An error message for this line
|
||||
*/
|
||||
function createErrorMessageData(expectedAmount, actualSpaces, actualTabs) {
|
||||
const expectedStatement = `${expectedAmount} ${indentType}${expectedAmount === 1 ? "" : "s"}`; // e.g. "2 tabs"
|
||||
const foundSpacesWord = `space${actualSpaces === 1 ? "" : "s"}`; // e.g. "space"
|
||||
const foundTabsWord = `tab${actualTabs === 1 ? "" : "s"}`; // e.g. "tabs"
|
||||
let foundStatement;
|
||||
|
||||
if (actualSpaces > 0 && actualTabs > 0) {
|
||||
foundStatement = `${actualSpaces} ${foundSpacesWord} and ${actualTabs} ${foundTabsWord}`; // e.g. "1 space and 2 tabs"
|
||||
} else if (actualSpaces > 0) {
|
||||
|
||||
/*
|
||||
* Abbreviate the message if the expected indentation is also spaces.
|
||||
* e.g. 'Expected 4 spaces but found 2' rather than 'Expected 4 spaces but found 2 spaces'
|
||||
*/
|
||||
foundStatement = indentType === "space" ? actualSpaces : `${actualSpaces} ${foundSpacesWord}`;
|
||||
} else if (actualTabs > 0) {
|
||||
foundStatement = indentType === "tab" ? actualTabs : `${actualTabs} ${foundTabsWord}`;
|
||||
} else {
|
||||
foundStatement = "0";
|
||||
}
|
||||
return {
|
||||
expected: expectedStatement,
|
||||
actual: foundStatement
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given indent violation
|
||||
* @param {ASTNode} node Node violating the indent rule
|
||||
* @param {int} needed Expected indentation character count
|
||||
* @param {int} gottenSpaces Indentation space count in the actual node/code
|
||||
* @param {int} gottenTabs Indentation tab count in the actual node/code
|
||||
* @param {Object} [loc] Error line and column location
|
||||
* @param {boolean} isLastNodeCheck Is the error for last node check
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(node, needed, gottenSpaces, gottenTabs, loc, isLastNodeCheck) {
|
||||
if (gottenSpaces && gottenTabs) {
|
||||
|
||||
// To avoid conflicts with `no-mixed-spaces-and-tabs`, don't report lines that have both spaces and tabs.
|
||||
return;
|
||||
}
|
||||
|
||||
const desiredIndent = (indentType === "space" ? " " : "\t").repeat(needed);
|
||||
|
||||
const textRange = isLastNodeCheck
|
||||
? [node.range[1] - node.loc.end.column, node.range[1] - node.loc.end.column + gottenSpaces + gottenTabs]
|
||||
: [node.range[0] - node.loc.start.column, node.range[0] - node.loc.start.column + gottenSpaces + gottenTabs];
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc,
|
||||
messageId: "expected",
|
||||
data: createErrorMessageData(needed, gottenSpaces, gottenTabs),
|
||||
fix: fixer => fixer.replaceTextRange(textRange, desiredIndent)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual indent of node
|
||||
* @param {ASTNode|Token} node Node to examine
|
||||
* @param {boolean} [byLastLine=false] get indent of node's last line
|
||||
* @returns {Object} The node's indent. Contains keys `space` and `tab`, representing the indent of each character. Also
|
||||
* contains keys `goodChar` and `badChar`, where `goodChar` is the amount of the user's desired indentation character, and
|
||||
* `badChar` is the amount of the other indentation character.
|
||||
*/
|
||||
function getNodeIndent(node, byLastLine) {
|
||||
const token = byLastLine ? sourceCode.getLastToken(node) : sourceCode.getFirstToken(node);
|
||||
const srcCharsBeforeNode = sourceCode.getText(token, token.loc.start.column).split("");
|
||||
const indentChars = srcCharsBeforeNode.slice(0, srcCharsBeforeNode.findIndex(char => char !== " " && char !== "\t"));
|
||||
const spaces = indentChars.filter(char => char === " ").length;
|
||||
const tabs = indentChars.filter(char => char === "\t").length;
|
||||
|
||||
return {
|
||||
space: spaces,
|
||||
tab: tabs,
|
||||
goodChar: indentType === "space" ? spaces : tabs,
|
||||
badChar: indentType === "space" ? tabs : spaces
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks node is the first in its own start line. By default it looks by start line.
|
||||
* @param {ASTNode} node The node to check
|
||||
* @param {boolean} [byEndLocation=false] Lookup based on start position or end
|
||||
* @returns {boolean} true if its the first in the its start line
|
||||
*/
|
||||
function isNodeFirstInLine(node, byEndLocation) {
|
||||
const firstToken = byEndLocation === true ? sourceCode.getLastToken(node, 1) : sourceCode.getTokenBefore(node),
|
||||
startLine = byEndLocation === true ? node.loc.end.line : node.loc.start.line,
|
||||
endLine = firstToken ? firstToken.loc.end.line : -1;
|
||||
|
||||
return startLine !== endLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check indent for node
|
||||
* @param {ASTNode} node Node to check
|
||||
* @param {int} neededIndent needed indent
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkNodeIndent(node, neededIndent) {
|
||||
const actualIndent = getNodeIndent(node, false);
|
||||
|
||||
if (
|
||||
node.type !== "ArrayExpression" &&
|
||||
node.type !== "ObjectExpression" &&
|
||||
(actualIndent.goodChar !== neededIndent || actualIndent.badChar !== 0) &&
|
||||
isNodeFirstInLine(node)
|
||||
) {
|
||||
report(node, neededIndent, actualIndent.space, actualIndent.tab);
|
||||
}
|
||||
|
||||
if (node.type === "IfStatement" && node.alternate) {
|
||||
const elseToken = sourceCode.getTokenBefore(node.alternate);
|
||||
|
||||
checkNodeIndent(elseToken, neededIndent);
|
||||
|
||||
if (!isNodeFirstInLine(node.alternate)) {
|
||||
checkNodeIndent(node.alternate, neededIndent);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === "TryStatement" && node.handler) {
|
||||
const catchToken = sourceCode.getFirstToken(node.handler);
|
||||
|
||||
checkNodeIndent(catchToken, neededIndent);
|
||||
}
|
||||
|
||||
if (node.type === "TryStatement" && node.finalizer) {
|
||||
const finallyToken = sourceCode.getTokenBefore(node.finalizer);
|
||||
|
||||
checkNodeIndent(finallyToken, neededIndent);
|
||||
}
|
||||
|
||||
if (node.type === "DoWhileStatement") {
|
||||
const whileToken = sourceCode.getTokenAfter(node.body);
|
||||
|
||||
checkNodeIndent(whileToken, neededIndent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check indent for nodes list
|
||||
* @param {ASTNode[]} nodes list of node objects
|
||||
* @param {int} indent needed indent
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkNodesIndent(nodes, indent) {
|
||||
nodes.forEach(node => checkNodeIndent(node, indent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check last node line indent this detects, that block closed correctly
|
||||
* @param {ASTNode} node Node to examine
|
||||
* @param {int} lastLineIndent needed indent
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkLastNodeLineIndent(node, lastLineIndent) {
|
||||
const lastToken = sourceCode.getLastToken(node);
|
||||
const endIndent = getNodeIndent(lastToken, true);
|
||||
|
||||
if ((endIndent.goodChar !== lastLineIndent || endIndent.badChar !== 0) && isNodeFirstInLine(node, true)) {
|
||||
report(
|
||||
node,
|
||||
lastLineIndent,
|
||||
endIndent.space,
|
||||
endIndent.tab,
|
||||
{ line: lastToken.loc.start.line, column: lastToken.loc.start.column },
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check last node line indent this detects, that block closed correctly
|
||||
* This function for more complicated return statement case, where closing parenthesis may be followed by ';'
|
||||
* @param {ASTNode} node Node to examine
|
||||
* @param {int} firstLineIndent first line needed indent
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkLastReturnStatementLineIndent(node, firstLineIndent) {
|
||||
|
||||
/*
|
||||
* in case if return statement ends with ');' we have traverse back to ')'
|
||||
* otherwise we'll measure indent for ';' and replace ')'
|
||||
*/
|
||||
const lastToken = sourceCode.getLastToken(node, astUtils.isClosingParenToken);
|
||||
const textBeforeClosingParenthesis = sourceCode.getText(lastToken, lastToken.loc.start.column).slice(0, -1);
|
||||
|
||||
if (textBeforeClosingParenthesis.trim()) {
|
||||
|
||||
// There are tokens before the closing paren, don't report this case
|
||||
return;
|
||||
}
|
||||
|
||||
const endIndent = getNodeIndent(lastToken, true);
|
||||
|
||||
if (endIndent.goodChar !== firstLineIndent) {
|
||||
report(
|
||||
node,
|
||||
firstLineIndent,
|
||||
endIndent.space,
|
||||
endIndent.tab,
|
||||
{ line: lastToken.loc.start.line, column: lastToken.loc.start.column },
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check first node line indent is correct
|
||||
* @param {ASTNode} node Node to examine
|
||||
* @param {int} firstLineIndent needed indent
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkFirstNodeLineIndent(node, firstLineIndent) {
|
||||
const startIndent = getNodeIndent(node, false);
|
||||
|
||||
if ((startIndent.goodChar !== firstLineIndent || startIndent.badChar !== 0) && isNodeFirstInLine(node)) {
|
||||
report(
|
||||
node,
|
||||
firstLineIndent,
|
||||
startIndent.space,
|
||||
startIndent.tab,
|
||||
{ line: node.loc.start.line, column: node.loc.start.column }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a parent node of given node based on a specified type
|
||||
* if not present then return null
|
||||
* @param {ASTNode} node node to examine
|
||||
* @param {string} type type that is being looked for
|
||||
* @param {string} stopAtList end points for the evaluating code
|
||||
* @returns {ASTNode|void} if found then node otherwise null
|
||||
*/
|
||||
function getParentNodeByType(node, type, stopAtList) {
|
||||
let parent = node.parent;
|
||||
const stopAtSet = new Set(stopAtList || ["Program"]);
|
||||
|
||||
while (parent.type !== type && !stopAtSet.has(parent.type) && parent.type !== "Program") {
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return parent.type === type ? parent : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the VariableDeclarator based on the current node
|
||||
* if not present then return null
|
||||
* @param {ASTNode} node node to examine
|
||||
* @returns {ASTNode|void} if found then node otherwise null
|
||||
*/
|
||||
function getVariableDeclaratorNode(node) {
|
||||
return getParentNodeByType(node, "VariableDeclarator");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the node is part of the multi-line variable declaration.
|
||||
* Also if its on the same line as the varNode
|
||||
* @param {ASTNode} node node to check
|
||||
* @param {ASTNode} varNode variable declaration node to check against
|
||||
* @returns {boolean} True if all the above condition satisfy
|
||||
*/
|
||||
function isNodeInVarOnTop(node, varNode) {
|
||||
return varNode &&
|
||||
varNode.parent.loc.start.line === node.loc.start.line &&
|
||||
varNode.parent.declarations.length > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the argument before the callee node is multi-line and
|
||||
* there should only be 1 argument before the callee node
|
||||
* @param {ASTNode} node node to check
|
||||
* @returns {boolean} True if arguments are multi-line
|
||||
*/
|
||||
function isArgBeforeCalleeNodeMultiline(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
if (parent.arguments.length >= 2 && parent.arguments[1] === node) {
|
||||
return parent.arguments[0].loc.end.line > parent.arguments[0].loc.start.line;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the node is a file level IIFE
|
||||
* @param {ASTNode} node The function node to check.
|
||||
* @returns {boolean} True if the node is the outer IIFE
|
||||
*/
|
||||
function isOuterIIFE(node) {
|
||||
const parent = node.parent;
|
||||
let stmt = parent.parent;
|
||||
|
||||
/*
|
||||
* Verify that the node is an IIEF
|
||||
*/
|
||||
if (
|
||||
parent.type !== "CallExpression" ||
|
||||
parent.callee !== node) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Navigate legal ancestors to determine whether this IIEF is outer
|
||||
*/
|
||||
while (
|
||||
stmt.type === "UnaryExpression" && (
|
||||
stmt.operator === "!" ||
|
||||
stmt.operator === "~" ||
|
||||
stmt.operator === "+" ||
|
||||
stmt.operator === "-") ||
|
||||
stmt.type === "AssignmentExpression" ||
|
||||
stmt.type === "LogicalExpression" ||
|
||||
stmt.type === "SequenceExpression" ||
|
||||
stmt.type === "VariableDeclarator") {
|
||||
|
||||
stmt = stmt.parent;
|
||||
}
|
||||
|
||||
return ((
|
||||
stmt.type === "ExpressionStatement" ||
|
||||
stmt.type === "VariableDeclaration") &&
|
||||
stmt.parent && stmt.parent.type === "Program"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check indent for function block content
|
||||
* @param {ASTNode} node A BlockStatement node that is inside of a function.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkIndentInFunctionBlock(node) {
|
||||
|
||||
/*
|
||||
* Search first caller in chain.
|
||||
* Ex.:
|
||||
*
|
||||
* Models <- Identifier
|
||||
* .User
|
||||
* .find()
|
||||
* .exec(function() {
|
||||
* // function body
|
||||
* });
|
||||
*
|
||||
* Looks for 'Models'
|
||||
*/
|
||||
const calleeNode = node.parent; // FunctionExpression
|
||||
let indent;
|
||||
|
||||
if (calleeNode.parent &&
|
||||
(calleeNode.parent.type === "Property" ||
|
||||
calleeNode.parent.type === "ArrayExpression")) {
|
||||
|
||||
// If function is part of array or object, comma can be put at left
|
||||
indent = getNodeIndent(calleeNode, false).goodChar;
|
||||
} else {
|
||||
|
||||
// If function is standalone, simple calculate indent
|
||||
indent = getNodeIndent(calleeNode).goodChar;
|
||||
}
|
||||
|
||||
if (calleeNode.parent.type === "CallExpression") {
|
||||
const calleeParent = calleeNode.parent;
|
||||
|
||||
if (calleeNode.type !== "FunctionExpression" && calleeNode.type !== "ArrowFunctionExpression") {
|
||||
if (calleeParent && calleeParent.loc.start.line < node.loc.start.line) {
|
||||
indent = getNodeIndent(calleeParent).goodChar;
|
||||
}
|
||||
} else {
|
||||
if (isArgBeforeCalleeNodeMultiline(calleeNode) &&
|
||||
calleeParent.callee.loc.start.line === calleeParent.callee.loc.end.line &&
|
||||
!isNodeFirstInLine(calleeNode)) {
|
||||
indent = getNodeIndent(calleeParent).goodChar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* function body indent should be indent + indent size, unless this
|
||||
* is a FunctionDeclaration, FunctionExpression, or outer IIFE and the corresponding options are enabled.
|
||||
*/
|
||||
let functionOffset = indentSize;
|
||||
|
||||
if (options.outerIIFEBody !== null && isOuterIIFE(calleeNode)) {
|
||||
functionOffset = options.outerIIFEBody * indentSize;
|
||||
} else if (calleeNode.type === "FunctionExpression") {
|
||||
functionOffset = options.FunctionExpression.body * indentSize;
|
||||
} else if (calleeNode.type === "FunctionDeclaration") {
|
||||
functionOffset = options.FunctionDeclaration.body * indentSize;
|
||||
}
|
||||
indent += functionOffset;
|
||||
|
||||
// check if the node is inside a variable
|
||||
const parentVarNode = getVariableDeclaratorNode(node);
|
||||
|
||||
if (parentVarNode && isNodeInVarOnTop(node, parentVarNode)) {
|
||||
indent += indentSize * options.VariableDeclarator[parentVarNode.parent.kind];
|
||||
}
|
||||
|
||||
if (node.body.length > 0) {
|
||||
checkNodesIndent(node.body, indent);
|
||||
}
|
||||
|
||||
checkLastNodeLineIndent(node, indent - functionOffset);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the given node starts and ends on the same line
|
||||
* @param {ASTNode} node The node to check
|
||||
* @returns {boolean} Whether or not the block starts and ends on the same line.
|
||||
*/
|
||||
function isSingleLineNode(node) {
|
||||
const lastToken = sourceCode.getLastToken(node),
|
||||
startLine = node.loc.start.line,
|
||||
endLine = lastToken.loc.end.line;
|
||||
|
||||
return startLine === endLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check indent for array block content or object block content
|
||||
* @param {ASTNode} node node to examine
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkIndentInArrayOrObjectBlock(node) {
|
||||
|
||||
// Skip inline
|
||||
if (isSingleLineNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let elements = (node.type === "ArrayExpression") ? node.elements : node.properties;
|
||||
|
||||
// filter out empty elements example would be [ , 2] so remove first element as espree considers it as null
|
||||
elements = elements.filter(elem => elem !== null);
|
||||
|
||||
let nodeIndent;
|
||||
let elementsIndent;
|
||||
const parentVarNode = getVariableDeclaratorNode(node);
|
||||
|
||||
// TODO - come up with a better strategy in future
|
||||
if (isNodeFirstInLine(node)) {
|
||||
const parent = node.parent;
|
||||
|
||||
nodeIndent = getNodeIndent(parent).goodChar;
|
||||
if (!parentVarNode || parentVarNode.loc.start.line !== node.loc.start.line) {
|
||||
if (parent.type !== "VariableDeclarator" || parentVarNode === parentVarNode.parent.declarations[0]) {
|
||||
if (parent.type === "VariableDeclarator" && parentVarNode.loc.start.line === parent.loc.start.line) {
|
||||
nodeIndent += (indentSize * options.VariableDeclarator[parentVarNode.parent.kind]);
|
||||
} else if (parent.type === "ObjectExpression" || parent.type === "ArrayExpression") {
|
||||
const parentElements = node.parent.type === "ObjectExpression" ? node.parent.properties : node.parent.elements;
|
||||
|
||||
if (parentElements[0] &&
|
||||
parentElements[0].loc.start.line === parent.loc.start.line &&
|
||||
parentElements[0].loc.end.line !== parent.loc.start.line) {
|
||||
|
||||
/*
|
||||
* If the first element of the array spans multiple lines, don't increase the expected indentation of the rest.
|
||||
* e.g. [{
|
||||
* foo: 1
|
||||
* },
|
||||
* {
|
||||
* bar: 1
|
||||
* }]
|
||||
* the second object is not indented.
|
||||
*/
|
||||
} else if (typeof options[parent.type] === "number") {
|
||||
nodeIndent += options[parent.type] * indentSize;
|
||||
} else {
|
||||
nodeIndent = parentElements[0].loc.start.column;
|
||||
}
|
||||
} else if (parent.type === "CallExpression" || parent.type === "NewExpression") {
|
||||
if (typeof options.CallExpression.arguments === "number") {
|
||||
nodeIndent += options.CallExpression.arguments * indentSize;
|
||||
} else if (options.CallExpression.arguments === "first") {
|
||||
if (parent.arguments.includes(node)) {
|
||||
nodeIndent = parent.arguments[0].loc.start.column;
|
||||
}
|
||||
} else {
|
||||
nodeIndent += indentSize;
|
||||
}
|
||||
} else if (parent.type === "LogicalExpression" || parent.type === "ArrowFunctionExpression") {
|
||||
nodeIndent += indentSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkFirstNodeLineIndent(node, nodeIndent);
|
||||
} else {
|
||||
nodeIndent = getNodeIndent(node).goodChar;
|
||||
}
|
||||
|
||||
if (options[node.type] === "first") {
|
||||
elementsIndent = elements.length ? elements[0].loc.start.column : 0; // If there are no elements, elementsIndent doesn't matter.
|
||||
} else {
|
||||
elementsIndent = nodeIndent + indentSize * options[node.type];
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if the node is a multiple variable declaration; if so, then
|
||||
* make sure indentation takes that into account.
|
||||
*/
|
||||
if (isNodeInVarOnTop(node, parentVarNode)) {
|
||||
elementsIndent += indentSize * options.VariableDeclarator[parentVarNode.parent.kind];
|
||||
}
|
||||
|
||||
checkNodesIndent(elements, elementsIndent);
|
||||
|
||||
if (elements.length > 0) {
|
||||
|
||||
// Skip last block line check if last item in same line
|
||||
if (elements[elements.length - 1].loc.end.line === node.loc.end.line) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
checkLastNodeLineIndent(node, nodeIndent +
|
||||
(isNodeInVarOnTop(node, parentVarNode) ? options.VariableDeclarator[parentVarNode.parent.kind] * indentSize : 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the node or node body is a BlockStatement or not
|
||||
* @param {ASTNode} node node to test
|
||||
* @returns {boolean} True if it or its body is a block statement
|
||||
*/
|
||||
function isNodeBodyBlock(node) {
|
||||
return node.type === "BlockStatement" || node.type === "ClassBody" || (node.body && node.body.type === "BlockStatement") ||
|
||||
(node.consequent && node.consequent.type === "BlockStatement");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check indentation for blocks
|
||||
* @param {ASTNode} node node to check
|
||||
* @returns {void}
|
||||
*/
|
||||
function blockIndentationCheck(node) {
|
||||
|
||||
// Skip inline blocks
|
||||
if (isSingleLineNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.parent && (
|
||||
node.parent.type === "FunctionExpression" ||
|
||||
node.parent.type === "FunctionDeclaration" ||
|
||||
node.parent.type === "ArrowFunctionExpression")
|
||||
) {
|
||||
checkIndentInFunctionBlock(node);
|
||||
return;
|
||||
}
|
||||
|
||||
let indent;
|
||||
let nodesToCheck = [];
|
||||
|
||||
/*
|
||||
* For this statements we should check indent from statement beginning,
|
||||
* not from the beginning of the block.
|
||||
*/
|
||||
const statementsWithProperties = [
|
||||
"IfStatement", "WhileStatement", "ForStatement", "ForInStatement", "ForOfStatement", "DoWhileStatement", "ClassDeclaration", "TryStatement"
|
||||
];
|
||||
|
||||
if (node.parent && statementsWithProperties.includes(node.parent.type) && isNodeBodyBlock(node)) {
|
||||
indent = getNodeIndent(node.parent).goodChar;
|
||||
} else if (node.parent && node.parent.type === "CatchClause") {
|
||||
indent = getNodeIndent(node.parent.parent).goodChar;
|
||||
} else {
|
||||
indent = getNodeIndent(node).goodChar;
|
||||
}
|
||||
|
||||
if (node.type === "IfStatement" && node.consequent.type !== "BlockStatement") {
|
||||
nodesToCheck = [node.consequent];
|
||||
} else if (Array.isArray(node.body)) {
|
||||
nodesToCheck = node.body;
|
||||
} else {
|
||||
nodesToCheck = [node.body];
|
||||
}
|
||||
|
||||
if (nodesToCheck.length > 0) {
|
||||
checkNodesIndent(nodesToCheck, indent + indentSize);
|
||||
}
|
||||
|
||||
if (node.type === "BlockStatement") {
|
||||
checkLastNodeLineIndent(node, indent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out the elements which are on the same line of each other or the node.
|
||||
* basically have only 1 elements from each line except the variable declaration line.
|
||||
* @param {ASTNode} node Variable declaration node
|
||||
* @returns {ASTNode[]} Filtered elements
|
||||
*/
|
||||
function filterOutSameLineVars(node) {
|
||||
return node.declarations.reduce((finalCollection, elem) => {
|
||||
const lastElem = finalCollection[finalCollection.length - 1];
|
||||
|
||||
if ((elem.loc.start.line !== node.loc.start.line && !lastElem) ||
|
||||
(lastElem && lastElem.loc.start.line !== elem.loc.start.line)) {
|
||||
finalCollection.push(elem);
|
||||
}
|
||||
|
||||
return finalCollection;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check indentation for variable declarations
|
||||
* @param {ASTNode} node node to examine
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkIndentInVariableDeclarations(node) {
|
||||
const elements = filterOutSameLineVars(node);
|
||||
const nodeIndent = getNodeIndent(node).goodChar;
|
||||
const lastElement = elements[elements.length - 1];
|
||||
|
||||
const elementsIndent = nodeIndent + indentSize * options.VariableDeclarator[node.kind];
|
||||
|
||||
checkNodesIndent(elements, elementsIndent);
|
||||
|
||||
// Only check the last line if there is any token after the last item
|
||||
if (sourceCode.getLastToken(node).loc.end.line <= lastElement.loc.end.line) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenBeforeLastElement = sourceCode.getTokenBefore(lastElement);
|
||||
|
||||
if (tokenBeforeLastElement.value === ",") {
|
||||
|
||||
// Special case for comma-first syntax where the semicolon is indented
|
||||
checkLastNodeLineIndent(node, getNodeIndent(tokenBeforeLastElement).goodChar);
|
||||
} else {
|
||||
checkLastNodeLineIndent(node, elementsIndent - indentSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and decide whether to check for indentation for blockless nodes
|
||||
* Scenarios are for or while statements without braces around them
|
||||
* @param {ASTNode} node node to examine
|
||||
* @returns {void}
|
||||
*/
|
||||
function blockLessNodes(node) {
|
||||
if (node.body.type !== "BlockStatement") {
|
||||
blockIndentationCheck(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expected indentation for the case statement
|
||||
* @param {ASTNode} node node to examine
|
||||
* @param {int} [providedSwitchIndent] indent for switch statement
|
||||
* @returns {int} indent size
|
||||
*/
|
||||
function expectedCaseIndent(node, providedSwitchIndent) {
|
||||
const switchNode = (node.type === "SwitchStatement") ? node : node.parent;
|
||||
const switchIndent = typeof providedSwitchIndent === "undefined"
|
||||
? getNodeIndent(switchNode).goodChar
|
||||
: providedSwitchIndent;
|
||||
let caseIndent;
|
||||
|
||||
if (caseIndentStore[switchNode.loc.start.line]) {
|
||||
return caseIndentStore[switchNode.loc.start.line];
|
||||
}
|
||||
|
||||
if (switchNode.cases.length > 0 && options.SwitchCase === 0) {
|
||||
caseIndent = switchIndent;
|
||||
} else {
|
||||
caseIndent = switchIndent + (indentSize * options.SwitchCase);
|
||||
}
|
||||
|
||||
caseIndentStore[switchNode.loc.start.line] = caseIndent;
|
||||
return caseIndent;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a return statement is wrapped in ()
|
||||
* @param {ASTNode} node node to examine
|
||||
* @returns {boolean} the result
|
||||
*/
|
||||
function isWrappedInParenthesis(node) {
|
||||
const regex = /^return\s*?\(\s*?\);*?/u;
|
||||
|
||||
const statementWithoutArgument = sourceCode.getText(node).replace(
|
||||
sourceCode.getText(node.argument), ""
|
||||
);
|
||||
|
||||
return regex.test(statementWithoutArgument);
|
||||
}
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
if (node.body.length > 0) {
|
||||
|
||||
// Root nodes should have no indent
|
||||
checkNodesIndent(node.body, getNodeIndent(node).goodChar);
|
||||
}
|
||||
},
|
||||
|
||||
ClassBody: blockIndentationCheck,
|
||||
|
||||
BlockStatement: blockIndentationCheck,
|
||||
|
||||
WhileStatement: blockLessNodes,
|
||||
|
||||
ForStatement: blockLessNodes,
|
||||
|
||||
ForInStatement: blockLessNodes,
|
||||
|
||||
ForOfStatement: blockLessNodes,
|
||||
|
||||
DoWhileStatement: blockLessNodes,
|
||||
|
||||
IfStatement(node) {
|
||||
if (node.consequent.type !== "BlockStatement" && node.consequent.loc.start.line > node.loc.start.line) {
|
||||
blockIndentationCheck(node);
|
||||
}
|
||||
},
|
||||
|
||||
VariableDeclaration(node) {
|
||||
if (node.declarations[node.declarations.length - 1].loc.start.line > node.declarations[0].loc.start.line) {
|
||||
checkIndentInVariableDeclarations(node);
|
||||
}
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
checkIndentInArrayOrObjectBlock(node);
|
||||
},
|
||||
|
||||
ArrayExpression(node) {
|
||||
checkIndentInArrayOrObjectBlock(node);
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
|
||||
if (typeof options.MemberExpression === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSingleLineNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* The typical layout of variable declarations and assignments
|
||||
* alter the expectation of correct indentation. Skip them.
|
||||
* TODO: Add appropriate configuration options for variable
|
||||
* declarations and assignments.
|
||||
*/
|
||||
if (getParentNodeByType(node, "VariableDeclarator", ["FunctionExpression", "ArrowFunctionExpression"])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getParentNodeByType(node, "AssignmentExpression", ["FunctionExpression"])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propertyIndent = getNodeIndent(node).goodChar + indentSize * options.MemberExpression;
|
||||
|
||||
const checkNodes = [node.property];
|
||||
|
||||
const dot = sourceCode.getTokenBefore(node.property);
|
||||
|
||||
if (dot.type === "Punctuator" && dot.value === ".") {
|
||||
checkNodes.push(dot);
|
||||
}
|
||||
|
||||
checkNodesIndent(checkNodes, propertyIndent);
|
||||
},
|
||||
|
||||
SwitchStatement(node) {
|
||||
|
||||
// Switch is not a 'BlockStatement'
|
||||
const switchIndent = getNodeIndent(node).goodChar;
|
||||
const caseIndent = expectedCaseIndent(node, switchIndent);
|
||||
|
||||
checkNodesIndent(node.cases, caseIndent);
|
||||
|
||||
|
||||
checkLastNodeLineIndent(node, switchIndent);
|
||||
},
|
||||
|
||||
SwitchCase(node) {
|
||||
|
||||
// Skip inline cases
|
||||
if (isSingleLineNode(node)) {
|
||||
return;
|
||||
}
|
||||
const caseIndent = expectedCaseIndent(node);
|
||||
|
||||
checkNodesIndent(node.consequent, caseIndent + indentSize);
|
||||
},
|
||||
|
||||
FunctionDeclaration(node) {
|
||||
if (isSingleLineNode(node)) {
|
||||
return;
|
||||
}
|
||||
if (options.FunctionDeclaration.parameters === "first" && node.params.length) {
|
||||
checkNodesIndent(node.params.slice(1), node.params[0].loc.start.column);
|
||||
} else if (options.FunctionDeclaration.parameters !== null) {
|
||||
checkNodesIndent(node.params, getNodeIndent(node).goodChar + indentSize * options.FunctionDeclaration.parameters);
|
||||
}
|
||||
},
|
||||
|
||||
FunctionExpression(node) {
|
||||
if (isSingleLineNode(node)) {
|
||||
return;
|
||||
}
|
||||
if (options.FunctionExpression.parameters === "first" && node.params.length) {
|
||||
checkNodesIndent(node.params.slice(1), node.params[0].loc.start.column);
|
||||
} else if (options.FunctionExpression.parameters !== null) {
|
||||
checkNodesIndent(node.params, getNodeIndent(node).goodChar + indentSize * options.FunctionExpression.parameters);
|
||||
}
|
||||
},
|
||||
|
||||
ReturnStatement(node) {
|
||||
if (isSingleLineNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstLineIndent = getNodeIndent(node).goodChar;
|
||||
|
||||
// in case if return statement is wrapped in parenthesis
|
||||
if (isWrappedInParenthesis(node)) {
|
||||
checkLastReturnStatementLineIndent(node, firstLineIndent);
|
||||
} else {
|
||||
checkNodeIndent(node, firstLineIndent);
|
||||
}
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (isSingleLineNode(node)) {
|
||||
return;
|
||||
}
|
||||
if (options.CallExpression.arguments === "first" && node.arguments.length) {
|
||||
checkNodesIndent(node.arguments.slice(1), node.arguments[0].loc.start.column);
|
||||
} else if (options.CallExpression.arguments !== null) {
|
||||
checkNodesIndent(node.arguments, getNodeIndent(node).goodChar + indentSize * options.CallExpression.arguments);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+1803
@@ -0,0 +1,1803 @@
|
||||
/**
|
||||
* @fileoverview This rule sets a specific indentation style and width for your code
|
||||
*
|
||||
* @author Teddy Katz
|
||||
* @author Vitaly Puzrin
|
||||
* @author Gyandeep Singh
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const KNOWN_NODES = new Set([
|
||||
"AssignmentExpression",
|
||||
"AssignmentPattern",
|
||||
"ArrayExpression",
|
||||
"ArrayPattern",
|
||||
"ArrowFunctionExpression",
|
||||
"AwaitExpression",
|
||||
"BlockStatement",
|
||||
"BinaryExpression",
|
||||
"BreakStatement",
|
||||
"CallExpression",
|
||||
"CatchClause",
|
||||
"ChainExpression",
|
||||
"ClassBody",
|
||||
"ClassDeclaration",
|
||||
"ClassExpression",
|
||||
"ConditionalExpression",
|
||||
"ContinueStatement",
|
||||
"DoWhileStatement",
|
||||
"DebuggerStatement",
|
||||
"EmptyStatement",
|
||||
"ExperimentalRestProperty",
|
||||
"ExperimentalSpreadProperty",
|
||||
"ExpressionStatement",
|
||||
"ForStatement",
|
||||
"ForInStatement",
|
||||
"ForOfStatement",
|
||||
"FunctionDeclaration",
|
||||
"FunctionExpression",
|
||||
"Identifier",
|
||||
"IfStatement",
|
||||
"Literal",
|
||||
"LabeledStatement",
|
||||
"LogicalExpression",
|
||||
"MemberExpression",
|
||||
"MetaProperty",
|
||||
"MethodDefinition",
|
||||
"NewExpression",
|
||||
"ObjectExpression",
|
||||
"ObjectPattern",
|
||||
"PrivateIdentifier",
|
||||
"Program",
|
||||
"Property",
|
||||
"PropertyDefinition",
|
||||
"RestElement",
|
||||
"ReturnStatement",
|
||||
"SequenceExpression",
|
||||
"SpreadElement",
|
||||
"StaticBlock",
|
||||
"Super",
|
||||
"SwitchCase",
|
||||
"SwitchStatement",
|
||||
"TaggedTemplateExpression",
|
||||
"TemplateElement",
|
||||
"TemplateLiteral",
|
||||
"ThisExpression",
|
||||
"ThrowStatement",
|
||||
"TryStatement",
|
||||
"UnaryExpression",
|
||||
"UpdateExpression",
|
||||
"VariableDeclaration",
|
||||
"VariableDeclarator",
|
||||
"WhileStatement",
|
||||
"WithStatement",
|
||||
"YieldExpression",
|
||||
"JSXFragment",
|
||||
"JSXOpeningFragment",
|
||||
"JSXClosingFragment",
|
||||
"JSXIdentifier",
|
||||
"JSXNamespacedName",
|
||||
"JSXMemberExpression",
|
||||
"JSXEmptyExpression",
|
||||
"JSXExpressionContainer",
|
||||
"JSXElement",
|
||||
"JSXClosingElement",
|
||||
"JSXOpeningElement",
|
||||
"JSXAttribute",
|
||||
"JSXSpreadAttribute",
|
||||
"JSXText",
|
||||
"ExportDefaultDeclaration",
|
||||
"ExportNamedDeclaration",
|
||||
"ExportAllDeclaration",
|
||||
"ExportSpecifier",
|
||||
"ImportDeclaration",
|
||||
"ImportSpecifier",
|
||||
"ImportDefaultSpecifier",
|
||||
"ImportNamespaceSpecifier",
|
||||
"ImportExpression"
|
||||
]);
|
||||
|
||||
/*
|
||||
* General rule strategy:
|
||||
* 1. An OffsetStorage instance stores a map of desired offsets, where each token has a specified offset from another
|
||||
* specified token or to the first column.
|
||||
* 2. As the AST is traversed, modify the desired offsets of tokens accordingly. For example, when entering a
|
||||
* BlockStatement, offset all of the tokens in the BlockStatement by 1 indent level from the opening curly
|
||||
* brace of the BlockStatement.
|
||||
* 3. After traversing the AST, calculate the expected indentation levels of every token according to the
|
||||
* OffsetStorage container.
|
||||
* 4. For each line, compare the expected indentation of the first token to the actual indentation in the file,
|
||||
* and report the token if the two values are not equal.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* A mutable map that stores (key, value) pairs. The keys are numeric indices, and must be unique.
|
||||
* This is intended to be a generic wrapper around a map with non-negative integer keys, so that the underlying implementation
|
||||
* can easily be swapped out.
|
||||
*/
|
||||
class IndexMap {
|
||||
|
||||
/**
|
||||
* Creates an empty map
|
||||
* @param {number} maxKey The maximum key
|
||||
*/
|
||||
constructor(maxKey) {
|
||||
|
||||
// Initializing the array with the maximum expected size avoids dynamic reallocations that could degrade performance.
|
||||
this._values = Array(maxKey + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an entry into the map.
|
||||
* @param {number} key The entry's key
|
||||
* @param {any} value The entry's value
|
||||
* @returns {void}
|
||||
*/
|
||||
insert(key, value) {
|
||||
this._values[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the value of the entry with the largest key less than or equal to the provided key
|
||||
* @param {number} key The provided key
|
||||
* @returns {*|undefined} The value of the found entry, or undefined if no such entry exists.
|
||||
*/
|
||||
findLastNotAfter(key) {
|
||||
const values = this._values;
|
||||
|
||||
for (let index = key; index >= 0; index--) {
|
||||
const value = values[index];
|
||||
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all of the keys in the interval [start, end)
|
||||
* @param {number} start The start of the range
|
||||
* @param {number} end The end of the range
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteRange(start, end) {
|
||||
this._values.fill(void 0, start, end);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper class to get token-based info related to indentation
|
||||
*/
|
||||
class TokenInfo {
|
||||
|
||||
/**
|
||||
* @param {SourceCode} sourceCode A SourceCode object
|
||||
*/
|
||||
constructor(sourceCode) {
|
||||
this.sourceCode = sourceCode;
|
||||
this.firstTokensByLineNumber = new Map();
|
||||
const tokens = sourceCode.tokensAndComments;
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
|
||||
if (!this.firstTokensByLineNumber.has(token.loc.start.line)) {
|
||||
this.firstTokensByLineNumber.set(token.loc.start.line, token);
|
||||
}
|
||||
if (!this.firstTokensByLineNumber.has(token.loc.end.line) && sourceCode.text.slice(token.range[1] - token.loc.end.column, token.range[1]).trim()) {
|
||||
this.firstTokensByLineNumber.set(token.loc.end.line, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first token on a given token's line
|
||||
* @param {Token|ASTNode} token a node or token
|
||||
* @returns {Token} The first token on the given line
|
||||
*/
|
||||
getFirstTokenOfLine(token) {
|
||||
return this.firstTokensByLineNumber.get(token.loc.start.line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a token is the first token in its line
|
||||
* @param {Token} token The token
|
||||
* @returns {boolean} `true` if the token is the first on its line
|
||||
*/
|
||||
isFirstTokenOfLine(token) {
|
||||
return this.getFirstTokenOfLine(token) === token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual indent of a token
|
||||
* @param {Token} token Token to examine. This should be the first token on its line.
|
||||
* @returns {string} The indentation characters that precede the token
|
||||
*/
|
||||
getTokenIndent(token) {
|
||||
return this.sourceCode.text.slice(token.range[0] - token.loc.start.column, token.range[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class to store information on desired offsets of tokens from each other
|
||||
*/
|
||||
class OffsetStorage {
|
||||
|
||||
/**
|
||||
* @param {TokenInfo} tokenInfo a TokenInfo instance
|
||||
* @param {number} indentSize The desired size of each indentation level
|
||||
* @param {string} indentType The indentation character
|
||||
* @param {number} maxIndex The maximum end index of any token
|
||||
*/
|
||||
constructor(tokenInfo, indentSize, indentType, maxIndex) {
|
||||
this._tokenInfo = tokenInfo;
|
||||
this._indentSize = indentSize;
|
||||
this._indentType = indentType;
|
||||
|
||||
this._indexMap = new IndexMap(maxIndex);
|
||||
this._indexMap.insert(0, { offset: 0, from: null, force: false });
|
||||
|
||||
this._lockedFirstTokens = new WeakMap();
|
||||
this._desiredIndentCache = new WeakMap();
|
||||
this._ignoredTokens = new WeakSet();
|
||||
}
|
||||
|
||||
_getOffsetDescriptor(token) {
|
||||
return this._indexMap.findLastNotAfter(token.range[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset column of token B to match the offset column of token A.
|
||||
* - **WARNING**: This matches a *column*, even if baseToken is not the first token on its line. In
|
||||
* most cases, `setDesiredOffset` should be used instead.
|
||||
* @param {Token} baseToken The first token
|
||||
* @param {Token} offsetToken The second token, whose offset should be matched to the first token
|
||||
* @returns {void}
|
||||
*/
|
||||
matchOffsetOf(baseToken, offsetToken) {
|
||||
|
||||
/*
|
||||
* lockedFirstTokens is a map from a token whose indentation is controlled by the "first" option to
|
||||
* the token that it depends on. For example, with the `ArrayExpression: first` option, the first
|
||||
* token of each element in the array after the first will be mapped to the first token of the first
|
||||
* element. The desired indentation of each of these tokens is computed based on the desired indentation
|
||||
* of the "first" element, rather than through the normal offset mechanism.
|
||||
*/
|
||||
this._lockedFirstTokens.set(offsetToken, baseToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the desired offset of a token.
|
||||
*
|
||||
* This uses a line-based offset collapsing behavior to handle tokens on the same line.
|
||||
* For example, consider the following two cases:
|
||||
*
|
||||
* (
|
||||
* [
|
||||
* bar
|
||||
* ]
|
||||
* )
|
||||
*
|
||||
* ([
|
||||
* bar
|
||||
* ])
|
||||
*
|
||||
* Based on the first case, it's clear that the `bar` token needs to have an offset of 1 indent level (4 spaces) from
|
||||
* the `[` token, and the `[` token has to have an offset of 1 indent level from the `(` token. Since the `(` token is
|
||||
* the first on its line (with an indent of 0 spaces), the `bar` token needs to be offset by 2 indent levels (8 spaces)
|
||||
* from the start of its line.
|
||||
*
|
||||
* However, in the second case `bar` should only be indented by 4 spaces. This is because the offset of 1 indent level
|
||||
* between the `(` and the `[` tokens gets "collapsed" because the two tokens are on the same line. As a result, the
|
||||
* `(` token is mapped to the `[` token with an offset of 0, and the rule correctly decides that `bar` should be indented
|
||||
* by 1 indent level from the start of the line.
|
||||
*
|
||||
* This is useful because rule listeners can usually just call `setDesiredOffset` for all the tokens in the node,
|
||||
* without needing to check which lines those tokens are on.
|
||||
*
|
||||
* Note that since collapsing only occurs when two tokens are on the same line, there are a few cases where non-intuitive
|
||||
* behavior can occur. For example, consider the following cases:
|
||||
*
|
||||
* foo(
|
||||
* ).
|
||||
* bar(
|
||||
* baz
|
||||
* )
|
||||
*
|
||||
* foo(
|
||||
* ).bar(
|
||||
* baz
|
||||
* )
|
||||
*
|
||||
* Based on the first example, it would seem that `bar` should be offset by 1 indent level from `foo`, and `baz`
|
||||
* should be offset by 1 indent level from `bar`. However, this is not correct, because it would result in `baz`
|
||||
* being indented by 2 indent levels in the second case (since `foo`, `bar`, and `baz` are all on separate lines, no
|
||||
* collapsing would occur).
|
||||
*
|
||||
* Instead, the correct way would be to offset `baz` by 1 level from `bar`, offset `bar` by 1 level from the `)`, and
|
||||
* offset the `)` by 0 levels from `foo`. This ensures that the offset between `bar` and the `)` are correctly collapsed
|
||||
* in the second case.
|
||||
* @param {Token} token The token
|
||||
* @param {Token} fromToken The token that `token` should be offset from
|
||||
* @param {number} offset The desired indent level
|
||||
* @returns {void}
|
||||
*/
|
||||
setDesiredOffset(token, fromToken, offset) {
|
||||
return this.setDesiredOffsets(token.range, fromToken, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the desired offset of all tokens in a range
|
||||
* It's common for node listeners in this file to need to apply the same offset to a large, contiguous range of tokens.
|
||||
* Moreover, the offset of any given token is usually updated multiple times (roughly once for each node that contains
|
||||
* it). This means that the offset of each token is updated O(AST depth) times.
|
||||
* It would not be performant to store and update the offsets for each token independently, because the rule would end
|
||||
* up having a time complexity of O(number of tokens * AST depth), which is quite slow for large files.
|
||||
*
|
||||
* Instead, the offset tree is represented as a collection of contiguous offset ranges in a file. For example, the following
|
||||
* list could represent the state of the offset tree at a given point:
|
||||
*
|
||||
* - Tokens starting in the interval [0, 15) are aligned with the beginning of the file
|
||||
* - Tokens starting in the interval [15, 30) are offset by 1 indent level from the `bar` token
|
||||
* - Tokens starting in the interval [30, 43) are offset by 1 indent level from the `foo` token
|
||||
* - Tokens starting in the interval [43, 820) are offset by 2 indent levels from the `bar` token
|
||||
* - Tokens starting in the interval [820, ∞) are offset by 1 indent level from the `baz` token
|
||||
*
|
||||
* The `setDesiredOffsets` methods inserts ranges like the ones above. The third line above would be inserted by using:
|
||||
* `setDesiredOffsets([30, 43], fooToken, 1);`
|
||||
* @param {[number, number]} range A [start, end] pair. All tokens with range[0] <= token.start < range[1] will have the offset applied.
|
||||
* @param {Token} fromToken The token that this is offset from
|
||||
* @param {number} offset The desired indent level
|
||||
* @param {boolean} force `true` if this offset should not use the normal collapsing behavior. This should almost always be false.
|
||||
* @returns {void}
|
||||
*/
|
||||
setDesiredOffsets(range, fromToken, offset, force) {
|
||||
|
||||
/*
|
||||
* Offset ranges are stored as a collection of nodes, where each node maps a numeric key to an offset
|
||||
* descriptor. The tree for the example above would have the following nodes:
|
||||
*
|
||||
* * key: 0, value: { offset: 0, from: null }
|
||||
* * key: 15, value: { offset: 1, from: barToken }
|
||||
* * key: 30, value: { offset: 1, from: fooToken }
|
||||
* * key: 43, value: { offset: 2, from: barToken }
|
||||
* * key: 820, value: { offset: 1, from: bazToken }
|
||||
*
|
||||
* To find the offset descriptor for any given token, one needs to find the node with the largest key
|
||||
* which is <= token.start. To make this operation fast, the nodes are stored in a map indexed by key.
|
||||
*/
|
||||
|
||||
const descriptorToInsert = { offset, from: fromToken, force };
|
||||
|
||||
const descriptorAfterRange = this._indexMap.findLastNotAfter(range[1]);
|
||||
|
||||
const fromTokenIsInRange = fromToken && fromToken.range[0] >= range[0] && fromToken.range[1] <= range[1];
|
||||
const fromTokenDescriptor = fromTokenIsInRange && this._getOffsetDescriptor(fromToken);
|
||||
|
||||
// First, remove any existing nodes in the range from the map.
|
||||
this._indexMap.deleteRange(range[0] + 1, range[1]);
|
||||
|
||||
// Insert a new node into the map for this range
|
||||
this._indexMap.insert(range[0], descriptorToInsert);
|
||||
|
||||
/*
|
||||
* To avoid circular offset dependencies, keep the `fromToken` token mapped to whatever it was mapped to previously,
|
||||
* even if it's in the current range.
|
||||
*/
|
||||
if (fromTokenIsInRange) {
|
||||
this._indexMap.insert(fromToken.range[0], fromTokenDescriptor);
|
||||
this._indexMap.insert(fromToken.range[1], descriptorToInsert);
|
||||
}
|
||||
|
||||
/*
|
||||
* To avoid modifying the offset of tokens after the range, insert another node to keep the offset of the following
|
||||
* tokens the same as it was before.
|
||||
*/
|
||||
this._indexMap.insert(range[1], descriptorAfterRange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the desired indent of a token
|
||||
* @param {Token} token The token
|
||||
* @returns {string} The desired indent of the token
|
||||
*/
|
||||
getDesiredIndent(token) {
|
||||
if (!this._desiredIndentCache.has(token)) {
|
||||
|
||||
if (this._ignoredTokens.has(token)) {
|
||||
|
||||
/*
|
||||
* If the token is ignored, use the actual indent of the token as the desired indent.
|
||||
* This ensures that no errors are reported for this token.
|
||||
*/
|
||||
this._desiredIndentCache.set(
|
||||
token,
|
||||
this._tokenInfo.getTokenIndent(token)
|
||||
);
|
||||
} else if (this._lockedFirstTokens.has(token)) {
|
||||
const firstToken = this._lockedFirstTokens.get(token);
|
||||
|
||||
this._desiredIndentCache.set(
|
||||
token,
|
||||
|
||||
// (indentation for the first element's line)
|
||||
this.getDesiredIndent(this._tokenInfo.getFirstTokenOfLine(firstToken)) +
|
||||
|
||||
// (space between the start of the first element's line and the first element)
|
||||
this._indentType.repeat(firstToken.loc.start.column - this._tokenInfo.getFirstTokenOfLine(firstToken).loc.start.column)
|
||||
);
|
||||
} else {
|
||||
const offsetInfo = this._getOffsetDescriptor(token);
|
||||
const offset = (
|
||||
offsetInfo.from &&
|
||||
offsetInfo.from.loc.start.line === token.loc.start.line &&
|
||||
!/^\s*?\n/u.test(token.value) &&
|
||||
!offsetInfo.force
|
||||
) ? 0 : offsetInfo.offset * this._indentSize;
|
||||
|
||||
this._desiredIndentCache.set(
|
||||
token,
|
||||
(offsetInfo.from ? this.getDesiredIndent(offsetInfo.from) : "") + this._indentType.repeat(offset)
|
||||
);
|
||||
}
|
||||
}
|
||||
return this._desiredIndentCache.get(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignores a token, preventing it from being reported.
|
||||
* @param {Token} token The token
|
||||
* @returns {void}
|
||||
*/
|
||||
ignoreToken(token) {
|
||||
if (this._tokenInfo.isFirstTokenOfLine(token)) {
|
||||
this._ignoredTokens.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first token that the given token's indentation is dependent on
|
||||
* @param {Token} token The token
|
||||
* @returns {Token} The token that the given token depends on, or `null` if the given token is at the top level
|
||||
*/
|
||||
getFirstDependency(token) {
|
||||
return this._getOffsetDescriptor(token).from;
|
||||
}
|
||||
}
|
||||
|
||||
const ELEMENT_LIST_SCHEMA = {
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
enum: ["first", "off"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent indentation",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/indent"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["tab"]
|
||||
},
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
SwitchCase: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
default: 0
|
||||
},
|
||||
VariableDeclarator: {
|
||||
oneOf: [
|
||||
ELEMENT_LIST_SCHEMA,
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
var: ELEMENT_LIST_SCHEMA,
|
||||
let: ELEMENT_LIST_SCHEMA,
|
||||
const: ELEMENT_LIST_SCHEMA
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
outerIIFEBody: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
enum: ["off"]
|
||||
}
|
||||
]
|
||||
},
|
||||
MemberExpression: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
enum: ["off"]
|
||||
}
|
||||
]
|
||||
},
|
||||
FunctionDeclaration: {
|
||||
type: "object",
|
||||
properties: {
|
||||
parameters: ELEMENT_LIST_SCHEMA,
|
||||
body: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
FunctionExpression: {
|
||||
type: "object",
|
||||
properties: {
|
||||
parameters: ELEMENT_LIST_SCHEMA,
|
||||
body: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
StaticBlock: {
|
||||
type: "object",
|
||||
properties: {
|
||||
body: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
CallExpression: {
|
||||
type: "object",
|
||||
properties: {
|
||||
arguments: ELEMENT_LIST_SCHEMA
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
ArrayExpression: ELEMENT_LIST_SCHEMA,
|
||||
ObjectExpression: ELEMENT_LIST_SCHEMA,
|
||||
ImportDeclaration: ELEMENT_LIST_SCHEMA,
|
||||
flatTernaryExpressions: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
offsetTernaryExpressions: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
ignoredNodes: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
not: {
|
||||
pattern: ":exit$"
|
||||
}
|
||||
}
|
||||
},
|
||||
ignoreComments: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
wrongIndentation: "Expected indentation of {{expected}} but found {{actual}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const DEFAULT_VARIABLE_INDENT = 1;
|
||||
const DEFAULT_PARAMETER_INDENT = 1;
|
||||
const DEFAULT_FUNCTION_BODY_INDENT = 1;
|
||||
|
||||
let indentType = "space";
|
||||
let indentSize = 4;
|
||||
const options = {
|
||||
SwitchCase: 0,
|
||||
VariableDeclarator: {
|
||||
var: DEFAULT_VARIABLE_INDENT,
|
||||
let: DEFAULT_VARIABLE_INDENT,
|
||||
const: DEFAULT_VARIABLE_INDENT
|
||||
},
|
||||
outerIIFEBody: 1,
|
||||
FunctionDeclaration: {
|
||||
parameters: DEFAULT_PARAMETER_INDENT,
|
||||
body: DEFAULT_FUNCTION_BODY_INDENT
|
||||
},
|
||||
FunctionExpression: {
|
||||
parameters: DEFAULT_PARAMETER_INDENT,
|
||||
body: DEFAULT_FUNCTION_BODY_INDENT
|
||||
},
|
||||
StaticBlock: {
|
||||
body: DEFAULT_FUNCTION_BODY_INDENT
|
||||
},
|
||||
CallExpression: {
|
||||
arguments: DEFAULT_PARAMETER_INDENT
|
||||
},
|
||||
MemberExpression: 1,
|
||||
ArrayExpression: 1,
|
||||
ObjectExpression: 1,
|
||||
ImportDeclaration: 1,
|
||||
flatTernaryExpressions: false,
|
||||
ignoredNodes: [],
|
||||
ignoreComments: false
|
||||
};
|
||||
|
||||
if (context.options.length) {
|
||||
if (context.options[0] === "tab") {
|
||||
indentSize = 1;
|
||||
indentType = "tab";
|
||||
} else {
|
||||
indentSize = context.options[0];
|
||||
indentType = "space";
|
||||
}
|
||||
|
||||
if (context.options[1]) {
|
||||
Object.assign(options, context.options[1]);
|
||||
|
||||
if (typeof options.VariableDeclarator === "number" || options.VariableDeclarator === "first") {
|
||||
options.VariableDeclarator = {
|
||||
var: options.VariableDeclarator,
|
||||
let: options.VariableDeclarator,
|
||||
const: options.VariableDeclarator
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
const tokenInfo = new TokenInfo(sourceCode);
|
||||
const offsets = new OffsetStorage(tokenInfo, indentSize, indentType === "space" ? " " : "\t", sourceCode.text.length);
|
||||
const parameterParens = new WeakSet();
|
||||
|
||||
/**
|
||||
* Creates an error message for a line, given the expected/actual indentation.
|
||||
* @param {int} expectedAmount The expected amount of indentation characters for this line
|
||||
* @param {int} actualSpaces The actual number of indentation spaces that were found on this line
|
||||
* @param {int} actualTabs The actual number of indentation tabs that were found on this line
|
||||
* @returns {string} An error message for this line
|
||||
*/
|
||||
function createErrorMessageData(expectedAmount, actualSpaces, actualTabs) {
|
||||
const expectedStatement = `${expectedAmount} ${indentType}${expectedAmount === 1 ? "" : "s"}`; // e.g. "2 tabs"
|
||||
const foundSpacesWord = `space${actualSpaces === 1 ? "" : "s"}`; // e.g. "space"
|
||||
const foundTabsWord = `tab${actualTabs === 1 ? "" : "s"}`; // e.g. "tabs"
|
||||
let foundStatement;
|
||||
|
||||
if (actualSpaces > 0) {
|
||||
|
||||
/*
|
||||
* Abbreviate the message if the expected indentation is also spaces.
|
||||
* e.g. 'Expected 4 spaces but found 2' rather than 'Expected 4 spaces but found 2 spaces'
|
||||
*/
|
||||
foundStatement = indentType === "space" ? actualSpaces : `${actualSpaces} ${foundSpacesWord}`;
|
||||
} else if (actualTabs > 0) {
|
||||
foundStatement = indentType === "tab" ? actualTabs : `${actualTabs} ${foundTabsWord}`;
|
||||
} else {
|
||||
foundStatement = "0";
|
||||
}
|
||||
return {
|
||||
expected: expectedStatement,
|
||||
actual: foundStatement
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given indent violation
|
||||
* @param {Token} token Token violating the indent rule
|
||||
* @param {string} neededIndent Expected indentation string
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(token, neededIndent) {
|
||||
const actualIndent = Array.from(tokenInfo.getTokenIndent(token));
|
||||
const numSpaces = actualIndent.filter(char => char === " ").length;
|
||||
const numTabs = actualIndent.filter(char => char === "\t").length;
|
||||
|
||||
context.report({
|
||||
node: token,
|
||||
messageId: "wrongIndentation",
|
||||
data: createErrorMessageData(neededIndent.length, numSpaces, numTabs),
|
||||
loc: {
|
||||
start: { line: token.loc.start.line, column: 0 },
|
||||
end: { line: token.loc.start.line, column: token.loc.start.column }
|
||||
},
|
||||
fix(fixer) {
|
||||
const range = [token.range[0] - token.loc.start.column, token.range[0]];
|
||||
const newText = neededIndent;
|
||||
|
||||
return fixer.replaceTextRange(range, newText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a token's indentation is correct
|
||||
* @param {Token} token Token to examine
|
||||
* @param {string} desiredIndent Desired indentation of the string
|
||||
* @returns {boolean} `true` if the token's indentation is correct
|
||||
*/
|
||||
function validateTokenIndent(token, desiredIndent) {
|
||||
const indentation = tokenInfo.getTokenIndent(token);
|
||||
|
||||
return indentation === desiredIndent ||
|
||||
|
||||
// To avoid conflicts with no-mixed-spaces-and-tabs, don't report mixed spaces and tabs.
|
||||
indentation.includes(" ") && indentation.includes("\t");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the node is a file level IIFE
|
||||
* @param {ASTNode} node The function node to check.
|
||||
* @returns {boolean} True if the node is the outer IIFE
|
||||
*/
|
||||
function isOuterIIFE(node) {
|
||||
|
||||
/*
|
||||
* Verify that the node is an IIFE
|
||||
*/
|
||||
if (!node.parent || node.parent.type !== "CallExpression" || node.parent.callee !== node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Navigate legal ancestors to determine whether this IIFE is outer.
|
||||
* A "legal ancestor" is an expression or statement that causes the function to get executed immediately.
|
||||
* For example, `!(function(){})()` is an outer IIFE even though it is preceded by a ! operator.
|
||||
*/
|
||||
let statement = node.parent && node.parent.parent;
|
||||
|
||||
while (
|
||||
statement.type === "UnaryExpression" && ["!", "~", "+", "-"].includes(statement.operator) ||
|
||||
statement.type === "AssignmentExpression" ||
|
||||
statement.type === "LogicalExpression" ||
|
||||
statement.type === "SequenceExpression" ||
|
||||
statement.type === "VariableDeclarator"
|
||||
) {
|
||||
statement = statement.parent;
|
||||
}
|
||||
|
||||
return (statement.type === "ExpressionStatement" || statement.type === "VariableDeclaration") && statement.parent.type === "Program";
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of linebreaks that follow the last non-whitespace character in a string
|
||||
* @param {string} string The string to check
|
||||
* @returns {number} The number of JavaScript linebreaks that follow the last non-whitespace character,
|
||||
* or the total number of linebreaks if the string is all whitespace.
|
||||
*/
|
||||
function countTrailingLinebreaks(string) {
|
||||
const trailingWhitespace = string.match(/\s*$/u)[0];
|
||||
const linebreakMatches = trailingWhitespace.match(astUtils.createGlobalLinebreakMatcher());
|
||||
|
||||
return linebreakMatches === null ? 0 : linebreakMatches.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check indentation for lists of elements (arrays, objects, function params)
|
||||
* @param {ASTNode[]} elements List of elements that should be offset
|
||||
* @param {Token} startToken The start token of the list that element should be aligned against, e.g. '['
|
||||
* @param {Token} endToken The end token of the list, e.g. ']'
|
||||
* @param {number|string} offset The amount that the elements should be offset
|
||||
* @returns {void}
|
||||
*/
|
||||
function addElementListIndent(elements, startToken, endToken, offset) {
|
||||
|
||||
/**
|
||||
* Gets the first token of a given element, including surrounding parentheses.
|
||||
* @param {ASTNode} element A node in the `elements` list
|
||||
* @returns {Token} The first token of this element
|
||||
*/
|
||||
function getFirstToken(element) {
|
||||
let token = sourceCode.getTokenBefore(element);
|
||||
|
||||
while (astUtils.isOpeningParenToken(token) && token !== startToken) {
|
||||
token = sourceCode.getTokenBefore(token);
|
||||
}
|
||||
return sourceCode.getTokenAfter(token);
|
||||
}
|
||||
|
||||
// Run through all the tokens in the list, and offset them by one indent level (mainly for comments, other things will end up overridden)
|
||||
offsets.setDesiredOffsets(
|
||||
[startToken.range[1], endToken.range[0]],
|
||||
startToken,
|
||||
typeof offset === "number" ? offset : 1
|
||||
);
|
||||
offsets.setDesiredOffset(endToken, startToken, 0);
|
||||
|
||||
// If the preference is "first" but there is no first element (e.g. sparse arrays w/ empty first slot), fall back to 1 level.
|
||||
if (offset === "first" && elements.length && !elements[0]) {
|
||||
return;
|
||||
}
|
||||
elements.forEach((element, index) => {
|
||||
if (!element) {
|
||||
|
||||
// Skip holes in arrays
|
||||
return;
|
||||
}
|
||||
if (offset === "off") {
|
||||
|
||||
// Ignore the first token of every element if the "off" option is used
|
||||
offsets.ignoreToken(getFirstToken(element));
|
||||
}
|
||||
|
||||
// Offset the following elements correctly relative to the first element
|
||||
if (index === 0) {
|
||||
return;
|
||||
}
|
||||
if (offset === "first" && tokenInfo.isFirstTokenOfLine(getFirstToken(element))) {
|
||||
offsets.matchOffsetOf(getFirstToken(elements[0]), getFirstToken(element));
|
||||
} else {
|
||||
const previousElement = elements[index - 1];
|
||||
const firstTokenOfPreviousElement = previousElement && getFirstToken(previousElement);
|
||||
const previousElementLastToken = previousElement && sourceCode.getLastToken(previousElement);
|
||||
|
||||
if (
|
||||
previousElement &&
|
||||
previousElementLastToken.loc.end.line - countTrailingLinebreaks(previousElementLastToken.value) > startToken.loc.end.line
|
||||
) {
|
||||
offsets.setDesiredOffsets(
|
||||
[previousElement.range[1], element.range[1]],
|
||||
firstTokenOfPreviousElement,
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and decide whether to check for indentation for blockless nodes
|
||||
* Scenarios are for or while statements without braces around them
|
||||
* @param {ASTNode} node node to examine
|
||||
* @returns {void}
|
||||
*/
|
||||
function addBlocklessNodeIndent(node) {
|
||||
if (node.type !== "BlockStatement") {
|
||||
const lastParentToken = sourceCode.getTokenBefore(node, astUtils.isNotOpeningParenToken);
|
||||
|
||||
let firstBodyToken = sourceCode.getFirstToken(node);
|
||||
let lastBodyToken = sourceCode.getLastToken(node);
|
||||
|
||||
while (
|
||||
astUtils.isOpeningParenToken(sourceCode.getTokenBefore(firstBodyToken)) &&
|
||||
astUtils.isClosingParenToken(sourceCode.getTokenAfter(lastBodyToken))
|
||||
) {
|
||||
firstBodyToken = sourceCode.getTokenBefore(firstBodyToken);
|
||||
lastBodyToken = sourceCode.getTokenAfter(lastBodyToken);
|
||||
}
|
||||
|
||||
offsets.setDesiredOffsets([firstBodyToken.range[0], lastBodyToken.range[1]], lastParentToken, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the indentation for nodes that are like function calls (`CallExpression` and `NewExpression`)
|
||||
* @param {ASTNode} node A CallExpression or NewExpression node
|
||||
* @returns {void}
|
||||
*/
|
||||
function addFunctionCallIndent(node) {
|
||||
let openingParen;
|
||||
|
||||
if (node.arguments.length) {
|
||||
openingParen = sourceCode.getFirstTokenBetween(node.callee, node.arguments[0], astUtils.isOpeningParenToken);
|
||||
} else {
|
||||
openingParen = sourceCode.getLastToken(node, 1);
|
||||
}
|
||||
const closingParen = sourceCode.getLastToken(node);
|
||||
|
||||
parameterParens.add(openingParen);
|
||||
parameterParens.add(closingParen);
|
||||
|
||||
/*
|
||||
* If `?.` token exists, set desired offset for that.
|
||||
* This logic is copied from `MemberExpression`'s.
|
||||
*/
|
||||
if (node.optional) {
|
||||
const dotToken = sourceCode.getTokenAfter(node.callee, astUtils.isQuestionDotToken);
|
||||
const calleeParenCount = sourceCode.getTokensBetween(node.callee, dotToken, { filter: astUtils.isClosingParenToken }).length;
|
||||
const firstTokenOfCallee = calleeParenCount
|
||||
? sourceCode.getTokenBefore(node.callee, { skip: calleeParenCount - 1 })
|
||||
: sourceCode.getFirstToken(node.callee);
|
||||
const lastTokenOfCallee = sourceCode.getTokenBefore(dotToken);
|
||||
const offsetBase = lastTokenOfCallee.loc.end.line === openingParen.loc.start.line
|
||||
? lastTokenOfCallee
|
||||
: firstTokenOfCallee;
|
||||
|
||||
offsets.setDesiredOffset(dotToken, offsetBase, 1);
|
||||
}
|
||||
|
||||
const offsetAfterToken = node.callee.type === "TaggedTemplateExpression" ? sourceCode.getFirstToken(node.callee.quasi) : openingParen;
|
||||
const offsetToken = sourceCode.getTokenBefore(offsetAfterToken);
|
||||
|
||||
offsets.setDesiredOffset(openingParen, offsetToken, 0);
|
||||
|
||||
addElementListIndent(node.arguments, openingParen, closingParen, options.CallExpression.arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the indentation of parenthesized values, given a list of tokens in a program
|
||||
* @param {Token[]} tokens A list of tokens
|
||||
* @returns {void}
|
||||
*/
|
||||
function addParensIndent(tokens) {
|
||||
const parenStack = [];
|
||||
const parenPairs = [];
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const nextToken = tokens[i];
|
||||
|
||||
if (astUtils.isOpeningParenToken(nextToken)) {
|
||||
parenStack.push(nextToken);
|
||||
} else if (astUtils.isClosingParenToken(nextToken)) {
|
||||
parenPairs.push({ left: parenStack.pop(), right: nextToken });
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = parenPairs.length - 1; i >= 0; i--) {
|
||||
const leftParen = parenPairs[i].left;
|
||||
const rightParen = parenPairs[i].right;
|
||||
|
||||
// We only want to handle parens around expressions, so exclude parentheses that are in function parameters and function call arguments.
|
||||
if (!parameterParens.has(leftParen) && !parameterParens.has(rightParen)) {
|
||||
const parenthesizedTokens = new Set(sourceCode.getTokensBetween(leftParen, rightParen));
|
||||
|
||||
parenthesizedTokens.forEach(token => {
|
||||
if (!parenthesizedTokens.has(offsets.getFirstDependency(token))) {
|
||||
offsets.setDesiredOffset(token, leftParen, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
offsets.setDesiredOffset(rightParen, leftParen, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore all tokens within an unknown node whose offset do not depend
|
||||
* on another token's offset within the unknown node
|
||||
* @param {ASTNode} node Unknown Node
|
||||
* @returns {void}
|
||||
*/
|
||||
function ignoreNode(node) {
|
||||
const unknownNodeTokens = new Set(sourceCode.getTokens(node, { includeComments: true }));
|
||||
|
||||
unknownNodeTokens.forEach(token => {
|
||||
if (!unknownNodeTokens.has(offsets.getFirstDependency(token))) {
|
||||
const firstTokenOfLine = tokenInfo.getFirstTokenOfLine(token);
|
||||
|
||||
if (token === firstTokenOfLine) {
|
||||
offsets.ignoreToken(token);
|
||||
} else {
|
||||
offsets.setDesiredOffset(token, firstTokenOfLine, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given token is on the first line of a statement.
|
||||
* @param {Token} token The token to check.
|
||||
* @param {ASTNode} leafNode The expression node that the token belongs directly.
|
||||
* @returns {boolean} `true` if the token is on the first line of a statement.
|
||||
*/
|
||||
function isOnFirstLineOfStatement(token, leafNode) {
|
||||
let node = leafNode;
|
||||
|
||||
while (node.parent && !node.parent.type.endsWith("Statement") && !node.parent.type.endsWith("Declaration")) {
|
||||
node = node.parent;
|
||||
}
|
||||
node = node.parent;
|
||||
|
||||
return !node || node.loc.start.line === token.loc.start.line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether there are any blank (whitespace-only) lines between
|
||||
* two tokens on separate lines.
|
||||
* @param {Token} firstToken The first token.
|
||||
* @param {Token} secondToken The second token.
|
||||
* @returns {boolean} `true` if the tokens are on separate lines and
|
||||
* there exists a blank line between them, `false` otherwise.
|
||||
*/
|
||||
function hasBlankLinesBetween(firstToken, secondToken) {
|
||||
const firstTokenLine = firstToken.loc.end.line;
|
||||
const secondTokenLine = secondToken.loc.start.line;
|
||||
|
||||
if (firstTokenLine === secondTokenLine || firstTokenLine === secondTokenLine - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let line = firstTokenLine + 1; line < secondTokenLine; ++line) {
|
||||
if (!tokenInfo.firstTokensByLineNumber.has(line)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const ignoredNodeFirstTokens = new Set();
|
||||
|
||||
const baseOffsetListeners = {
|
||||
"ArrayExpression, ArrayPattern"(node) {
|
||||
const openingBracket = sourceCode.getFirstToken(node);
|
||||
const closingBracket = sourceCode.getTokenAfter([...node.elements].reverse().find(_ => _) || openingBracket, astUtils.isClosingBracketToken);
|
||||
|
||||
addElementListIndent(node.elements, openingBracket, closingBracket, options.ArrayExpression);
|
||||
},
|
||||
|
||||
"ObjectExpression, ObjectPattern"(node) {
|
||||
const openingCurly = sourceCode.getFirstToken(node);
|
||||
const closingCurly = sourceCode.getTokenAfter(
|
||||
node.properties.length ? node.properties[node.properties.length - 1] : openingCurly,
|
||||
astUtils.isClosingBraceToken
|
||||
);
|
||||
|
||||
addElementListIndent(node.properties, openingCurly, closingCurly, options.ObjectExpression);
|
||||
},
|
||||
|
||||
ArrowFunctionExpression(node) {
|
||||
const maybeOpeningParen = sourceCode.getFirstToken(node, { skip: node.async ? 1 : 0 });
|
||||
|
||||
if (astUtils.isOpeningParenToken(maybeOpeningParen)) {
|
||||
const openingParen = maybeOpeningParen;
|
||||
const closingParen = sourceCode.getTokenBefore(node.body, astUtils.isClosingParenToken);
|
||||
|
||||
parameterParens.add(openingParen);
|
||||
parameterParens.add(closingParen);
|
||||
addElementListIndent(node.params, openingParen, closingParen, options.FunctionExpression.parameters);
|
||||
}
|
||||
|
||||
addBlocklessNodeIndent(node.body);
|
||||
},
|
||||
|
||||
AssignmentExpression(node) {
|
||||
const operator = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
|
||||
|
||||
offsets.setDesiredOffsets([operator.range[0], node.range[1]], sourceCode.getLastToken(node.left), 1);
|
||||
offsets.ignoreToken(operator);
|
||||
offsets.ignoreToken(sourceCode.getTokenAfter(operator));
|
||||
},
|
||||
|
||||
"BinaryExpression, LogicalExpression"(node) {
|
||||
const operator = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
|
||||
|
||||
/*
|
||||
* For backwards compatibility, don't check BinaryExpression indents, e.g.
|
||||
* var foo = bar &&
|
||||
* baz;
|
||||
*/
|
||||
|
||||
const tokenAfterOperator = sourceCode.getTokenAfter(operator);
|
||||
|
||||
offsets.ignoreToken(operator);
|
||||
offsets.ignoreToken(tokenAfterOperator);
|
||||
offsets.setDesiredOffset(tokenAfterOperator, operator, 0);
|
||||
},
|
||||
|
||||
"BlockStatement, ClassBody"(node) {
|
||||
let blockIndentLevel;
|
||||
|
||||
if (node.parent && isOuterIIFE(node.parent)) {
|
||||
blockIndentLevel = options.outerIIFEBody;
|
||||
} else if (node.parent && (node.parent.type === "FunctionExpression" || node.parent.type === "ArrowFunctionExpression")) {
|
||||
blockIndentLevel = options.FunctionExpression.body;
|
||||
} else if (node.parent && node.parent.type === "FunctionDeclaration") {
|
||||
blockIndentLevel = options.FunctionDeclaration.body;
|
||||
} else {
|
||||
blockIndentLevel = 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* For blocks that aren't lone statements, ensure that the opening curly brace
|
||||
* is aligned with the parent.
|
||||
*/
|
||||
if (!astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) {
|
||||
offsets.setDesiredOffset(sourceCode.getFirstToken(node), sourceCode.getFirstToken(node.parent), 0);
|
||||
}
|
||||
|
||||
addElementListIndent(node.body, sourceCode.getFirstToken(node), sourceCode.getLastToken(node), blockIndentLevel);
|
||||
},
|
||||
|
||||
CallExpression: addFunctionCallIndent,
|
||||
|
||||
"ClassDeclaration[superClass], ClassExpression[superClass]"(node) {
|
||||
const classToken = sourceCode.getFirstToken(node);
|
||||
const extendsToken = sourceCode.getTokenBefore(node.superClass, astUtils.isNotOpeningParenToken);
|
||||
|
||||
offsets.setDesiredOffsets([extendsToken.range[0], node.body.range[0]], classToken, 1);
|
||||
},
|
||||
|
||||
ConditionalExpression(node) {
|
||||
const firstToken = sourceCode.getFirstToken(node);
|
||||
|
||||
// `flatTernaryExpressions` option is for the following style:
|
||||
// var a =
|
||||
// foo > 0 ? bar :
|
||||
// foo < 0 ? baz :
|
||||
// /*else*/ qiz ;
|
||||
if (!options.flatTernaryExpressions ||
|
||||
!astUtils.isTokenOnSameLine(node.test, node.consequent) ||
|
||||
isOnFirstLineOfStatement(firstToken, node)
|
||||
) {
|
||||
const questionMarkToken = sourceCode.getFirstTokenBetween(node.test, node.consequent, token => token.type === "Punctuator" && token.value === "?");
|
||||
const colonToken = sourceCode.getFirstTokenBetween(node.consequent, node.alternate, token => token.type === "Punctuator" && token.value === ":");
|
||||
|
||||
const firstConsequentToken = sourceCode.getTokenAfter(questionMarkToken);
|
||||
const lastConsequentToken = sourceCode.getTokenBefore(colonToken);
|
||||
const firstAlternateToken = sourceCode.getTokenAfter(colonToken);
|
||||
|
||||
offsets.setDesiredOffset(questionMarkToken, firstToken, 1);
|
||||
offsets.setDesiredOffset(colonToken, firstToken, 1);
|
||||
|
||||
offsets.setDesiredOffset(firstConsequentToken, firstToken, firstConsequentToken.type === "Punctuator" &&
|
||||
options.offsetTernaryExpressions ? 2 : 1);
|
||||
|
||||
/*
|
||||
* The alternate and the consequent should usually have the same indentation.
|
||||
* If they share part of a line, align the alternate against the first token of the consequent.
|
||||
* This allows the alternate to be indented correctly in cases like this:
|
||||
* foo ? (
|
||||
* bar
|
||||
* ) : ( // this '(' is aligned with the '(' above, so it's considered to be aligned with `foo`
|
||||
* baz // as a result, `baz` is offset by 1 rather than 2
|
||||
* )
|
||||
*/
|
||||
if (lastConsequentToken.loc.end.line === firstAlternateToken.loc.start.line) {
|
||||
offsets.setDesiredOffset(firstAlternateToken, firstConsequentToken, 0);
|
||||
} else {
|
||||
|
||||
/**
|
||||
* If the alternate and consequent do not share part of a line, offset the alternate from the first
|
||||
* token of the conditional expression. For example:
|
||||
* foo ? bar
|
||||
* : baz
|
||||
*
|
||||
* If `baz` were aligned with `bar` rather than being offset by 1 from `foo`, `baz` would end up
|
||||
* having no expected indentation.
|
||||
*/
|
||||
offsets.setDesiredOffset(firstAlternateToken, firstToken, firstAlternateToken.type === "Punctuator" &&
|
||||
options.offsetTernaryExpressions ? 2 : 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"DoWhileStatement, WhileStatement, ForInStatement, ForOfStatement, WithStatement": node => addBlocklessNodeIndent(node.body),
|
||||
|
||||
ExportNamedDeclaration(node) {
|
||||
if (node.declaration === null) {
|
||||
const closingCurly = sourceCode.getLastToken(node, astUtils.isClosingBraceToken);
|
||||
|
||||
// Indent the specifiers in `export {foo, bar, baz}`
|
||||
addElementListIndent(node.specifiers, sourceCode.getFirstToken(node, { skip: 1 }), closingCurly, 1);
|
||||
|
||||
if (node.source) {
|
||||
|
||||
// Indent everything after and including the `from` token in `export {foo, bar, baz} from 'qux'`
|
||||
offsets.setDesiredOffsets([closingCurly.range[1], node.range[1]], sourceCode.getFirstToken(node), 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ForStatement(node) {
|
||||
const forOpeningParen = sourceCode.getFirstToken(node, 1);
|
||||
|
||||
if (node.init) {
|
||||
offsets.setDesiredOffsets(node.init.range, forOpeningParen, 1);
|
||||
}
|
||||
if (node.test) {
|
||||
offsets.setDesiredOffsets(node.test.range, forOpeningParen, 1);
|
||||
}
|
||||
if (node.update) {
|
||||
offsets.setDesiredOffsets(node.update.range, forOpeningParen, 1);
|
||||
}
|
||||
addBlocklessNodeIndent(node.body);
|
||||
},
|
||||
|
||||
"FunctionDeclaration, FunctionExpression"(node) {
|
||||
const closingParen = sourceCode.getTokenBefore(node.body);
|
||||
const openingParen = sourceCode.getTokenBefore(node.params.length ? node.params[0] : closingParen);
|
||||
|
||||
parameterParens.add(openingParen);
|
||||
parameterParens.add(closingParen);
|
||||
addElementListIndent(node.params, openingParen, closingParen, options[node.type].parameters);
|
||||
},
|
||||
|
||||
IfStatement(node) {
|
||||
addBlocklessNodeIndent(node.consequent);
|
||||
if (node.alternate) {
|
||||
addBlocklessNodeIndent(node.alternate);
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* For blockless nodes with semicolon-first style, don't indent the semicolon.
|
||||
* e.g.
|
||||
* if (foo)
|
||||
* bar()
|
||||
* ; [1, 2, 3].map(foo)
|
||||
*
|
||||
* Traversal into the node sets indentation of the semicolon, so we need to override it on exit.
|
||||
*/
|
||||
":matches(DoWhileStatement, ForStatement, ForInStatement, ForOfStatement, IfStatement, WhileStatement, WithStatement):exit"(node) {
|
||||
let nodesToCheck;
|
||||
|
||||
if (node.type === "IfStatement") {
|
||||
nodesToCheck = [node.consequent];
|
||||
if (node.alternate) {
|
||||
nodesToCheck.push(node.alternate);
|
||||
}
|
||||
} else {
|
||||
nodesToCheck = [node.body];
|
||||
}
|
||||
|
||||
for (const nodeToCheck of nodesToCheck) {
|
||||
const lastToken = sourceCode.getLastToken(nodeToCheck);
|
||||
|
||||
if (astUtils.isSemicolonToken(lastToken)) {
|
||||
const tokenBeforeLast = sourceCode.getTokenBefore(lastToken);
|
||||
const tokenAfterLast = sourceCode.getTokenAfter(lastToken);
|
||||
|
||||
// override indentation of `;` only if its line looks like a semicolon-first style line
|
||||
if (
|
||||
!astUtils.isTokenOnSameLine(tokenBeforeLast, lastToken) &&
|
||||
tokenAfterLast &&
|
||||
astUtils.isTokenOnSameLine(lastToken, tokenAfterLast)
|
||||
) {
|
||||
offsets.setDesiredOffset(
|
||||
lastToken,
|
||||
sourceCode.getFirstToken(node),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ImportDeclaration(node) {
|
||||
if (node.specifiers.some(specifier => specifier.type === "ImportSpecifier")) {
|
||||
const openingCurly = sourceCode.getFirstToken(node, astUtils.isOpeningBraceToken);
|
||||
const closingCurly = sourceCode.getLastToken(node, astUtils.isClosingBraceToken);
|
||||
|
||||
addElementListIndent(node.specifiers.filter(specifier => specifier.type === "ImportSpecifier"), openingCurly, closingCurly, options.ImportDeclaration);
|
||||
}
|
||||
|
||||
const fromToken = sourceCode.getLastToken(node, token => token.type === "Identifier" && token.value === "from");
|
||||
const sourceToken = sourceCode.getLastToken(node, token => token.type === "String");
|
||||
const semiToken = sourceCode.getLastToken(node, token => token.type === "Punctuator" && token.value === ";");
|
||||
|
||||
if (fromToken) {
|
||||
const end = semiToken && semiToken.range[1] === sourceToken.range[1] ? node.range[1] : sourceToken.range[1];
|
||||
|
||||
offsets.setDesiredOffsets([fromToken.range[0], end], sourceCode.getFirstToken(node), 1);
|
||||
}
|
||||
},
|
||||
|
||||
ImportExpression(node) {
|
||||
const openingParen = sourceCode.getFirstToken(node, 1);
|
||||
const closingParen = sourceCode.getLastToken(node);
|
||||
|
||||
parameterParens.add(openingParen);
|
||||
parameterParens.add(closingParen);
|
||||
offsets.setDesiredOffset(openingParen, sourceCode.getTokenBefore(openingParen), 0);
|
||||
|
||||
addElementListIndent([node.source], openingParen, closingParen, options.CallExpression.arguments);
|
||||
},
|
||||
|
||||
"MemberExpression, JSXMemberExpression, MetaProperty"(node) {
|
||||
const object = node.type === "MetaProperty" ? node.meta : node.object;
|
||||
const firstNonObjectToken = sourceCode.getFirstTokenBetween(object, node.property, astUtils.isNotClosingParenToken);
|
||||
const secondNonObjectToken = sourceCode.getTokenAfter(firstNonObjectToken);
|
||||
|
||||
const objectParenCount = sourceCode.getTokensBetween(object, node.property, { filter: astUtils.isClosingParenToken }).length;
|
||||
const firstObjectToken = objectParenCount
|
||||
? sourceCode.getTokenBefore(object, { skip: objectParenCount - 1 })
|
||||
: sourceCode.getFirstToken(object);
|
||||
const lastObjectToken = sourceCode.getTokenBefore(firstNonObjectToken);
|
||||
const firstPropertyToken = node.computed ? firstNonObjectToken : secondNonObjectToken;
|
||||
|
||||
if (node.computed) {
|
||||
|
||||
// For computed MemberExpressions, match the closing bracket with the opening bracket.
|
||||
offsets.setDesiredOffset(sourceCode.getLastToken(node), firstNonObjectToken, 0);
|
||||
offsets.setDesiredOffsets(node.property.range, firstNonObjectToken, 1);
|
||||
}
|
||||
|
||||
/*
|
||||
* If the object ends on the same line that the property starts, match against the last token
|
||||
* of the object, to ensure that the MemberExpression is not indented.
|
||||
*
|
||||
* Otherwise, match against the first token of the object, e.g.
|
||||
* foo
|
||||
* .bar
|
||||
* .baz // <-- offset by 1 from `foo`
|
||||
*/
|
||||
const offsetBase = lastObjectToken.loc.end.line === firstPropertyToken.loc.start.line
|
||||
? lastObjectToken
|
||||
: firstObjectToken;
|
||||
|
||||
if (typeof options.MemberExpression === "number") {
|
||||
|
||||
// Match the dot (for non-computed properties) or the opening bracket (for computed properties) against the object.
|
||||
offsets.setDesiredOffset(firstNonObjectToken, offsetBase, options.MemberExpression);
|
||||
|
||||
/*
|
||||
* For computed MemberExpressions, match the first token of the property against the opening bracket.
|
||||
* Otherwise, match the first token of the property against the object.
|
||||
*/
|
||||
offsets.setDesiredOffset(secondNonObjectToken, node.computed ? firstNonObjectToken : offsetBase, options.MemberExpression);
|
||||
} else {
|
||||
|
||||
// If the MemberExpression option is off, ignore the dot and the first token of the property.
|
||||
offsets.ignoreToken(firstNonObjectToken);
|
||||
offsets.ignoreToken(secondNonObjectToken);
|
||||
|
||||
// To ignore the property indentation, ensure that the property tokens depend on the ignored tokens.
|
||||
offsets.setDesiredOffset(firstNonObjectToken, offsetBase, 0);
|
||||
offsets.setDesiredOffset(secondNonObjectToken, firstNonObjectToken, 0);
|
||||
}
|
||||
},
|
||||
|
||||
NewExpression(node) {
|
||||
|
||||
// Only indent the arguments if the NewExpression has parens (e.g. `new Foo(bar)` or `new Foo()`, but not `new Foo`
|
||||
if (node.arguments.length > 0 ||
|
||||
astUtils.isClosingParenToken(sourceCode.getLastToken(node)) &&
|
||||
astUtils.isOpeningParenToken(sourceCode.getLastToken(node, 1))) {
|
||||
addFunctionCallIndent(node);
|
||||
}
|
||||
},
|
||||
|
||||
Property(node) {
|
||||
if (!node.shorthand && !node.method && node.kind === "init") {
|
||||
const colon = sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isColonToken);
|
||||
|
||||
offsets.ignoreToken(sourceCode.getTokenAfter(colon));
|
||||
}
|
||||
},
|
||||
|
||||
PropertyDefinition(node) {
|
||||
const firstToken = sourceCode.getFirstToken(node);
|
||||
const maybeSemicolonToken = sourceCode.getLastToken(node);
|
||||
let keyLastToken = null;
|
||||
|
||||
// Indent key.
|
||||
if (node.computed) {
|
||||
const bracketTokenL = sourceCode.getTokenBefore(node.key, astUtils.isOpeningBracketToken);
|
||||
const bracketTokenR = keyLastToken = sourceCode.getTokenAfter(node.key, astUtils.isClosingBracketToken);
|
||||
const keyRange = [bracketTokenL.range[1], bracketTokenR.range[0]];
|
||||
|
||||
if (bracketTokenL !== firstToken) {
|
||||
offsets.setDesiredOffset(bracketTokenL, firstToken, 0);
|
||||
}
|
||||
offsets.setDesiredOffsets(keyRange, bracketTokenL, 1);
|
||||
offsets.setDesiredOffset(bracketTokenR, bracketTokenL, 0);
|
||||
} else {
|
||||
const idToken = keyLastToken = sourceCode.getFirstToken(node.key);
|
||||
|
||||
if (idToken !== firstToken) {
|
||||
offsets.setDesiredOffset(idToken, firstToken, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Indent initializer.
|
||||
if (node.value) {
|
||||
const eqToken = sourceCode.getTokenBefore(node.value, astUtils.isEqToken);
|
||||
const valueToken = sourceCode.getTokenAfter(eqToken);
|
||||
|
||||
offsets.setDesiredOffset(eqToken, keyLastToken, 1);
|
||||
offsets.setDesiredOffset(valueToken, eqToken, 1);
|
||||
if (astUtils.isSemicolonToken(maybeSemicolonToken)) {
|
||||
offsets.setDesiredOffset(maybeSemicolonToken, eqToken, 1);
|
||||
}
|
||||
} else if (astUtils.isSemicolonToken(maybeSemicolonToken)) {
|
||||
offsets.setDesiredOffset(maybeSemicolonToken, keyLastToken, 1);
|
||||
}
|
||||
},
|
||||
|
||||
StaticBlock(node) {
|
||||
const openingCurly = sourceCode.getFirstToken(node, { skip: 1 }); // skip the `static` token
|
||||
const closingCurly = sourceCode.getLastToken(node);
|
||||
|
||||
addElementListIndent(node.body, openingCurly, closingCurly, options.StaticBlock.body);
|
||||
},
|
||||
|
||||
SwitchStatement(node) {
|
||||
const openingCurly = sourceCode.getTokenAfter(node.discriminant, astUtils.isOpeningBraceToken);
|
||||
const closingCurly = sourceCode.getLastToken(node);
|
||||
|
||||
offsets.setDesiredOffsets([openingCurly.range[1], closingCurly.range[0]], openingCurly, options.SwitchCase);
|
||||
|
||||
if (node.cases.length) {
|
||||
sourceCode.getTokensBetween(
|
||||
node.cases[node.cases.length - 1],
|
||||
closingCurly,
|
||||
{ includeComments: true, filter: astUtils.isCommentToken }
|
||||
).forEach(token => offsets.ignoreToken(token));
|
||||
}
|
||||
},
|
||||
|
||||
SwitchCase(node) {
|
||||
if (!(node.consequent.length === 1 && node.consequent[0].type === "BlockStatement")) {
|
||||
const caseKeyword = sourceCode.getFirstToken(node);
|
||||
const tokenAfterCurrentCase = sourceCode.getTokenAfter(node);
|
||||
|
||||
offsets.setDesiredOffsets([caseKeyword.range[1], tokenAfterCurrentCase.range[0]], caseKeyword, 1);
|
||||
}
|
||||
},
|
||||
|
||||
TemplateLiteral(node) {
|
||||
node.expressions.forEach((expression, index) => {
|
||||
const previousQuasi = node.quasis[index];
|
||||
const nextQuasi = node.quasis[index + 1];
|
||||
const tokenToAlignFrom = previousQuasi.loc.start.line === previousQuasi.loc.end.line
|
||||
? sourceCode.getFirstToken(previousQuasi)
|
||||
: null;
|
||||
|
||||
offsets.setDesiredOffsets([previousQuasi.range[1], nextQuasi.range[0]], tokenToAlignFrom, 1);
|
||||
offsets.setDesiredOffset(sourceCode.getFirstToken(nextQuasi), tokenToAlignFrom, 0);
|
||||
});
|
||||
},
|
||||
|
||||
VariableDeclaration(node) {
|
||||
let variableIndent = Object.prototype.hasOwnProperty.call(options.VariableDeclarator, node.kind)
|
||||
? options.VariableDeclarator[node.kind]
|
||||
: DEFAULT_VARIABLE_INDENT;
|
||||
|
||||
const firstToken = sourceCode.getFirstToken(node),
|
||||
lastToken = sourceCode.getLastToken(node);
|
||||
|
||||
if (options.VariableDeclarator[node.kind] === "first") {
|
||||
if (node.declarations.length > 1) {
|
||||
addElementListIndent(
|
||||
node.declarations,
|
||||
firstToken,
|
||||
lastToken,
|
||||
"first"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
variableIndent = DEFAULT_VARIABLE_INDENT;
|
||||
}
|
||||
|
||||
if (node.declarations[node.declarations.length - 1].loc.start.line > node.loc.start.line) {
|
||||
|
||||
/*
|
||||
* VariableDeclarator indentation is a bit different from other forms of indentation, in that the
|
||||
* indentation of an opening bracket sometimes won't match that of a closing bracket. For example,
|
||||
* the following indentations are correct:
|
||||
*
|
||||
* var foo = {
|
||||
* ok: true
|
||||
* };
|
||||
*
|
||||
* var foo = {
|
||||
* ok: true,
|
||||
* },
|
||||
* bar = 1;
|
||||
*
|
||||
* Account for when exiting the AST (after indentations have already been set for the nodes in
|
||||
* the declaration) by manually increasing the indentation level of the tokens in this declarator
|
||||
* on the same line as the start of the declaration, provided that there are declarators that
|
||||
* follow this one.
|
||||
*/
|
||||
offsets.setDesiredOffsets(node.range, firstToken, variableIndent, true);
|
||||
} else {
|
||||
offsets.setDesiredOffsets(node.range, firstToken, variableIndent);
|
||||
}
|
||||
|
||||
if (astUtils.isSemicolonToken(lastToken)) {
|
||||
offsets.ignoreToken(lastToken);
|
||||
}
|
||||
},
|
||||
|
||||
VariableDeclarator(node) {
|
||||
if (node.init) {
|
||||
const equalOperator = sourceCode.getTokenBefore(node.init, astUtils.isNotOpeningParenToken);
|
||||
const tokenAfterOperator = sourceCode.getTokenAfter(equalOperator);
|
||||
|
||||
offsets.ignoreToken(equalOperator);
|
||||
offsets.ignoreToken(tokenAfterOperator);
|
||||
offsets.setDesiredOffsets([tokenAfterOperator.range[0], node.range[1]], equalOperator, 1);
|
||||
offsets.setDesiredOffset(equalOperator, sourceCode.getLastToken(node.id), 0);
|
||||
}
|
||||
},
|
||||
|
||||
"JSXAttribute[value]"(node) {
|
||||
const equalsToken = sourceCode.getFirstTokenBetween(node.name, node.value, token => token.type === "Punctuator" && token.value === "=");
|
||||
|
||||
offsets.setDesiredOffsets([equalsToken.range[0], node.value.range[1]], sourceCode.getFirstToken(node.name), 1);
|
||||
},
|
||||
|
||||
JSXElement(node) {
|
||||
if (node.closingElement) {
|
||||
addElementListIndent(node.children, sourceCode.getFirstToken(node.openingElement), sourceCode.getFirstToken(node.closingElement), 1);
|
||||
}
|
||||
},
|
||||
|
||||
JSXOpeningElement(node) {
|
||||
const firstToken = sourceCode.getFirstToken(node);
|
||||
let closingToken;
|
||||
|
||||
if (node.selfClosing) {
|
||||
closingToken = sourceCode.getLastToken(node, { skip: 1 });
|
||||
offsets.setDesiredOffset(sourceCode.getLastToken(node), closingToken, 0);
|
||||
} else {
|
||||
closingToken = sourceCode.getLastToken(node);
|
||||
}
|
||||
offsets.setDesiredOffsets(node.name.range, sourceCode.getFirstToken(node));
|
||||
addElementListIndent(node.attributes, firstToken, closingToken, 1);
|
||||
},
|
||||
|
||||
JSXClosingElement(node) {
|
||||
const firstToken = sourceCode.getFirstToken(node);
|
||||
|
||||
offsets.setDesiredOffsets(node.name.range, firstToken, 1);
|
||||
},
|
||||
|
||||
JSXFragment(node) {
|
||||
const firstOpeningToken = sourceCode.getFirstToken(node.openingFragment);
|
||||
const firstClosingToken = sourceCode.getFirstToken(node.closingFragment);
|
||||
|
||||
addElementListIndent(node.children, firstOpeningToken, firstClosingToken, 1);
|
||||
},
|
||||
|
||||
JSXOpeningFragment(node) {
|
||||
const firstToken = sourceCode.getFirstToken(node);
|
||||
const closingToken = sourceCode.getLastToken(node);
|
||||
|
||||
offsets.setDesiredOffsets(node.range, firstToken, 1);
|
||||
offsets.matchOffsetOf(firstToken, closingToken);
|
||||
},
|
||||
|
||||
JSXClosingFragment(node) {
|
||||
const firstToken = sourceCode.getFirstToken(node);
|
||||
const slashToken = sourceCode.getLastToken(node, { skip: 1 });
|
||||
const closingToken = sourceCode.getLastToken(node);
|
||||
const tokenToMatch = astUtils.isTokenOnSameLine(slashToken, closingToken) ? slashToken : closingToken;
|
||||
|
||||
offsets.setDesiredOffsets(node.range, firstToken, 1);
|
||||
offsets.matchOffsetOf(firstToken, tokenToMatch);
|
||||
},
|
||||
|
||||
JSXExpressionContainer(node) {
|
||||
const openingCurly = sourceCode.getFirstToken(node);
|
||||
const closingCurly = sourceCode.getLastToken(node);
|
||||
|
||||
offsets.setDesiredOffsets(
|
||||
[openingCurly.range[1], closingCurly.range[0]],
|
||||
openingCurly,
|
||||
1
|
||||
);
|
||||
},
|
||||
|
||||
JSXSpreadAttribute(node) {
|
||||
const openingCurly = sourceCode.getFirstToken(node);
|
||||
const closingCurly = sourceCode.getLastToken(node);
|
||||
|
||||
offsets.setDesiredOffsets(
|
||||
[openingCurly.range[1], closingCurly.range[0]],
|
||||
openingCurly,
|
||||
1
|
||||
);
|
||||
},
|
||||
|
||||
"*"(node) {
|
||||
const firstToken = sourceCode.getFirstToken(node);
|
||||
|
||||
// Ensure that the children of every node are indented at least as much as the first token.
|
||||
if (firstToken && !ignoredNodeFirstTokens.has(firstToken)) {
|
||||
offsets.setDesiredOffsets(node.range, firstToken, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const listenerCallQueue = [];
|
||||
|
||||
/*
|
||||
* To ignore the indentation of a node:
|
||||
* 1. Don't call the node's listener when entering it (if it has a listener)
|
||||
* 2. Don't set any offsets against the first token of the node.
|
||||
* 3. Call `ignoreNode` on the node sometime after exiting it and before validating offsets.
|
||||
*/
|
||||
const offsetListeners = {};
|
||||
|
||||
for (const [selector, listener] of Object.entries(baseOffsetListeners)) {
|
||||
|
||||
/*
|
||||
* Offset listener calls are deferred until traversal is finished, and are called as
|
||||
* part of the final `Program:exit` listener. This is necessary because a node might
|
||||
* be matched by multiple selectors.
|
||||
*
|
||||
* Example: Suppose there is an offset listener for `Identifier`, and the user has
|
||||
* specified in configuration that `MemberExpression > Identifier` should be ignored.
|
||||
* Due to selector specificity rules, the `Identifier` listener will get called first. However,
|
||||
* if a given Identifier node is supposed to be ignored, then the `Identifier` offset listener
|
||||
* should not have been called at all. Without doing extra selector matching, we don't know
|
||||
* whether the Identifier matches the `MemberExpression > Identifier` selector until the
|
||||
* `MemberExpression > Identifier` listener is called.
|
||||
*
|
||||
* To avoid this, the `Identifier` listener isn't called until traversal finishes and all
|
||||
* ignored nodes are known.
|
||||
*/
|
||||
offsetListeners[selector] = node => listenerCallQueue.push({ listener, node });
|
||||
}
|
||||
|
||||
// For each ignored node selector, set up a listener to collect it into the `ignoredNodes` set.
|
||||
const ignoredNodes = new Set();
|
||||
|
||||
/**
|
||||
* Ignores a node
|
||||
* @param {ASTNode} node The node to ignore
|
||||
* @returns {void}
|
||||
*/
|
||||
function addToIgnoredNodes(node) {
|
||||
ignoredNodes.add(node);
|
||||
ignoredNodeFirstTokens.add(sourceCode.getFirstToken(node));
|
||||
}
|
||||
|
||||
const ignoredNodeListeners = options.ignoredNodes.reduce(
|
||||
(listeners, ignoredSelector) => Object.assign(listeners, { [ignoredSelector]: addToIgnoredNodes }),
|
||||
{}
|
||||
);
|
||||
|
||||
/*
|
||||
* Join the listeners, and add a listener to verify that all tokens actually have the correct indentation
|
||||
* at the end.
|
||||
*
|
||||
* Using Object.assign will cause some offset listeners to be overwritten if the same selector also appears
|
||||
* in `ignoredNodeListeners`. This isn't a problem because all of the matching nodes will be ignored,
|
||||
* so those listeners wouldn't be called anyway.
|
||||
*/
|
||||
return Object.assign(
|
||||
offsetListeners,
|
||||
ignoredNodeListeners,
|
||||
{
|
||||
"*:exit"(node) {
|
||||
|
||||
// If a node's type is nonstandard, we can't tell how its children should be offset, so ignore it.
|
||||
if (!KNOWN_NODES.has(node.type)) {
|
||||
addToIgnoredNodes(node);
|
||||
}
|
||||
},
|
||||
"Program:exit"() {
|
||||
|
||||
// If ignoreComments option is enabled, ignore all comment tokens.
|
||||
if (options.ignoreComments) {
|
||||
sourceCode.getAllComments()
|
||||
.forEach(comment => offsets.ignoreToken(comment));
|
||||
}
|
||||
|
||||
// Invoke the queued offset listeners for the nodes that aren't ignored.
|
||||
for (let i = 0; i < listenerCallQueue.length; i++) {
|
||||
const nodeInfo = listenerCallQueue[i];
|
||||
|
||||
if (!ignoredNodes.has(nodeInfo.node)) {
|
||||
nodeInfo.listener(nodeInfo.node);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the offsets for ignored nodes to prevent their child tokens from being reported.
|
||||
ignoredNodes.forEach(ignoreNode);
|
||||
|
||||
addParensIndent(sourceCode.ast.tokens);
|
||||
|
||||
/*
|
||||
* Create a Map from (tokenOrComment) => (precedingToken).
|
||||
* This is necessary because sourceCode.getTokenBefore does not handle a comment as an argument correctly.
|
||||
*/
|
||||
const precedingTokens = new WeakMap();
|
||||
|
||||
for (let i = 0; i < sourceCode.ast.comments.length; i++) {
|
||||
const comment = sourceCode.ast.comments[i];
|
||||
|
||||
const tokenOrCommentBefore = sourceCode.getTokenBefore(comment, { includeComments: true });
|
||||
const hasToken = precedingTokens.has(tokenOrCommentBefore) ? precedingTokens.get(tokenOrCommentBefore) : tokenOrCommentBefore;
|
||||
|
||||
precedingTokens.set(comment, hasToken);
|
||||
}
|
||||
|
||||
for (let i = 1; i < sourceCode.lines.length + 1; i++) {
|
||||
|
||||
if (!tokenInfo.firstTokensByLineNumber.has(i)) {
|
||||
|
||||
// Don't check indentation on blank lines
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstTokenOfLine = tokenInfo.firstTokensByLineNumber.get(i);
|
||||
|
||||
if (firstTokenOfLine.loc.start.line !== i) {
|
||||
|
||||
// Don't check the indentation of multi-line tokens (e.g. template literals or block comments) twice.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (astUtils.isCommentToken(firstTokenOfLine)) {
|
||||
const tokenBefore = precedingTokens.get(firstTokenOfLine);
|
||||
const tokenAfter = tokenBefore ? sourceCode.getTokenAfter(tokenBefore) : sourceCode.ast.tokens[0];
|
||||
const mayAlignWithBefore = tokenBefore && !hasBlankLinesBetween(tokenBefore, firstTokenOfLine);
|
||||
const mayAlignWithAfter = tokenAfter && !hasBlankLinesBetween(firstTokenOfLine, tokenAfter);
|
||||
|
||||
/*
|
||||
* If a comment precedes a line that begins with a semicolon token, align to that token, i.e.
|
||||
*
|
||||
* let foo
|
||||
* // comment
|
||||
* ;(async () => {})()
|
||||
*/
|
||||
if (tokenAfter && astUtils.isSemicolonToken(tokenAfter) && !astUtils.isTokenOnSameLine(firstTokenOfLine, tokenAfter)) {
|
||||
offsets.setDesiredOffset(firstTokenOfLine, tokenAfter, 0);
|
||||
}
|
||||
|
||||
// If a comment matches the expected indentation of the token immediately before or after, don't report it.
|
||||
if (
|
||||
mayAlignWithBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore)) ||
|
||||
mayAlignWithAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If the token matches the expected indentation, don't report it.
|
||||
if (validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, report the token/comment.
|
||||
report(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* @fileoverview Collects the built-in rules into a map structure so that they can be imported all at once and without
|
||||
* using the file-system directly.
|
||||
* @author Peter (Somogyvari) Metz
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/* eslint sort-keys: ["error", "asc"] -- More readable for long list */
|
||||
|
||||
const { LazyLoadingRuleMap } = require("./utils/lazy-loading-rule-map");
|
||||
|
||||
/** @type {Map<string, import("../shared/types").Rule>} */
|
||||
module.exports = new LazyLoadingRuleMap(Object.entries({
|
||||
"accessor-pairs": () => require("./accessor-pairs"),
|
||||
"array-bracket-newline": () => require("./array-bracket-newline"),
|
||||
"array-bracket-spacing": () => require("./array-bracket-spacing"),
|
||||
"array-callback-return": () => require("./array-callback-return"),
|
||||
"array-element-newline": () => require("./array-element-newline"),
|
||||
"arrow-body-style": () => require("./arrow-body-style"),
|
||||
"arrow-parens": () => require("./arrow-parens"),
|
||||
"arrow-spacing": () => require("./arrow-spacing"),
|
||||
"block-scoped-var": () => require("./block-scoped-var"),
|
||||
"block-spacing": () => require("./block-spacing"),
|
||||
"brace-style": () => require("./brace-style"),
|
||||
"callback-return": () => require("./callback-return"),
|
||||
camelcase: () => require("./camelcase"),
|
||||
"capitalized-comments": () => require("./capitalized-comments"),
|
||||
"class-methods-use-this": () => require("./class-methods-use-this"),
|
||||
"comma-dangle": () => require("./comma-dangle"),
|
||||
"comma-spacing": () => require("./comma-spacing"),
|
||||
"comma-style": () => require("./comma-style"),
|
||||
complexity: () => require("./complexity"),
|
||||
"computed-property-spacing": () => require("./computed-property-spacing"),
|
||||
"consistent-return": () => require("./consistent-return"),
|
||||
"consistent-this": () => require("./consistent-this"),
|
||||
"constructor-super": () => require("./constructor-super"),
|
||||
curly: () => require("./curly"),
|
||||
"default-case": () => require("./default-case"),
|
||||
"default-case-last": () => require("./default-case-last"),
|
||||
"default-param-last": () => require("./default-param-last"),
|
||||
"dot-location": () => require("./dot-location"),
|
||||
"dot-notation": () => require("./dot-notation"),
|
||||
"eol-last": () => require("./eol-last"),
|
||||
eqeqeq: () => require("./eqeqeq"),
|
||||
"for-direction": () => require("./for-direction"),
|
||||
"func-call-spacing": () => require("./func-call-spacing"),
|
||||
"func-name-matching": () => require("./func-name-matching"),
|
||||
"func-names": () => require("./func-names"),
|
||||
"func-style": () => require("./func-style"),
|
||||
"function-call-argument-newline": () => require("./function-call-argument-newline"),
|
||||
"function-paren-newline": () => require("./function-paren-newline"),
|
||||
"generator-star-spacing": () => require("./generator-star-spacing"),
|
||||
"getter-return": () => require("./getter-return"),
|
||||
"global-require": () => require("./global-require"),
|
||||
"grouped-accessor-pairs": () => require("./grouped-accessor-pairs"),
|
||||
"guard-for-in": () => require("./guard-for-in"),
|
||||
"handle-callback-err": () => require("./handle-callback-err"),
|
||||
"id-blacklist": () => require("./id-blacklist"),
|
||||
"id-denylist": () => require("./id-denylist"),
|
||||
"id-length": () => require("./id-length"),
|
||||
"id-match": () => require("./id-match"),
|
||||
"implicit-arrow-linebreak": () => require("./implicit-arrow-linebreak"),
|
||||
indent: () => require("./indent"),
|
||||
"indent-legacy": () => require("./indent-legacy"),
|
||||
"init-declarations": () => require("./init-declarations"),
|
||||
"jsx-quotes": () => require("./jsx-quotes"),
|
||||
"key-spacing": () => require("./key-spacing"),
|
||||
"keyword-spacing": () => require("./keyword-spacing"),
|
||||
"line-comment-position": () => require("./line-comment-position"),
|
||||
"linebreak-style": () => require("./linebreak-style"),
|
||||
"lines-around-comment": () => require("./lines-around-comment"),
|
||||
"lines-around-directive": () => require("./lines-around-directive"),
|
||||
"lines-between-class-members": () => require("./lines-between-class-members"),
|
||||
"logical-assignment-operators": () => require("./logical-assignment-operators"),
|
||||
"max-classes-per-file": () => require("./max-classes-per-file"),
|
||||
"max-depth": () => require("./max-depth"),
|
||||
"max-len": () => require("./max-len"),
|
||||
"max-lines": () => require("./max-lines"),
|
||||
"max-lines-per-function": () => require("./max-lines-per-function"),
|
||||
"max-nested-callbacks": () => require("./max-nested-callbacks"),
|
||||
"max-params": () => require("./max-params"),
|
||||
"max-statements": () => require("./max-statements"),
|
||||
"max-statements-per-line": () => require("./max-statements-per-line"),
|
||||
"multiline-comment-style": () => require("./multiline-comment-style"),
|
||||
"multiline-ternary": () => require("./multiline-ternary"),
|
||||
"new-cap": () => require("./new-cap"),
|
||||
"new-parens": () => require("./new-parens"),
|
||||
"newline-after-var": () => require("./newline-after-var"),
|
||||
"newline-before-return": () => require("./newline-before-return"),
|
||||
"newline-per-chained-call": () => require("./newline-per-chained-call"),
|
||||
"no-alert": () => require("./no-alert"),
|
||||
"no-array-constructor": () => require("./no-array-constructor"),
|
||||
"no-async-promise-executor": () => require("./no-async-promise-executor"),
|
||||
"no-await-in-loop": () => require("./no-await-in-loop"),
|
||||
"no-bitwise": () => require("./no-bitwise"),
|
||||
"no-buffer-constructor": () => require("./no-buffer-constructor"),
|
||||
"no-caller": () => require("./no-caller"),
|
||||
"no-case-declarations": () => require("./no-case-declarations"),
|
||||
"no-catch-shadow": () => require("./no-catch-shadow"),
|
||||
"no-class-assign": () => require("./no-class-assign"),
|
||||
"no-compare-neg-zero": () => require("./no-compare-neg-zero"),
|
||||
"no-cond-assign": () => require("./no-cond-assign"),
|
||||
"no-confusing-arrow": () => require("./no-confusing-arrow"),
|
||||
"no-console": () => require("./no-console"),
|
||||
"no-const-assign": () => require("./no-const-assign"),
|
||||
"no-constant-binary-expression": () => require("./no-constant-binary-expression"),
|
||||
"no-constant-condition": () => require("./no-constant-condition"),
|
||||
"no-constructor-return": () => require("./no-constructor-return"),
|
||||
"no-continue": () => require("./no-continue"),
|
||||
"no-control-regex": () => require("./no-control-regex"),
|
||||
"no-debugger": () => require("./no-debugger"),
|
||||
"no-delete-var": () => require("./no-delete-var"),
|
||||
"no-div-regex": () => require("./no-div-regex"),
|
||||
"no-dupe-args": () => require("./no-dupe-args"),
|
||||
"no-dupe-class-members": () => require("./no-dupe-class-members"),
|
||||
"no-dupe-else-if": () => require("./no-dupe-else-if"),
|
||||
"no-dupe-keys": () => require("./no-dupe-keys"),
|
||||
"no-duplicate-case": () => require("./no-duplicate-case"),
|
||||
"no-duplicate-imports": () => require("./no-duplicate-imports"),
|
||||
"no-else-return": () => require("./no-else-return"),
|
||||
"no-empty": () => require("./no-empty"),
|
||||
"no-empty-character-class": () => require("./no-empty-character-class"),
|
||||
"no-empty-function": () => require("./no-empty-function"),
|
||||
"no-empty-pattern": () => require("./no-empty-pattern"),
|
||||
"no-empty-static-block": () => require("./no-empty-static-block"),
|
||||
"no-eq-null": () => require("./no-eq-null"),
|
||||
"no-eval": () => require("./no-eval"),
|
||||
"no-ex-assign": () => require("./no-ex-assign"),
|
||||
"no-extend-native": () => require("./no-extend-native"),
|
||||
"no-extra-bind": () => require("./no-extra-bind"),
|
||||
"no-extra-boolean-cast": () => require("./no-extra-boolean-cast"),
|
||||
"no-extra-label": () => require("./no-extra-label"),
|
||||
"no-extra-parens": () => require("./no-extra-parens"),
|
||||
"no-extra-semi": () => require("./no-extra-semi"),
|
||||
"no-fallthrough": () => require("./no-fallthrough"),
|
||||
"no-floating-decimal": () => require("./no-floating-decimal"),
|
||||
"no-func-assign": () => require("./no-func-assign"),
|
||||
"no-global-assign": () => require("./no-global-assign"),
|
||||
"no-implicit-coercion": () => require("./no-implicit-coercion"),
|
||||
"no-implicit-globals": () => require("./no-implicit-globals"),
|
||||
"no-implied-eval": () => require("./no-implied-eval"),
|
||||
"no-import-assign": () => require("./no-import-assign"),
|
||||
"no-inline-comments": () => require("./no-inline-comments"),
|
||||
"no-inner-declarations": () => require("./no-inner-declarations"),
|
||||
"no-invalid-regexp": () => require("./no-invalid-regexp"),
|
||||
"no-invalid-this": () => require("./no-invalid-this"),
|
||||
"no-irregular-whitespace": () => require("./no-irregular-whitespace"),
|
||||
"no-iterator": () => require("./no-iterator"),
|
||||
"no-label-var": () => require("./no-label-var"),
|
||||
"no-labels": () => require("./no-labels"),
|
||||
"no-lone-blocks": () => require("./no-lone-blocks"),
|
||||
"no-lonely-if": () => require("./no-lonely-if"),
|
||||
"no-loop-func": () => require("./no-loop-func"),
|
||||
"no-loss-of-precision": () => require("./no-loss-of-precision"),
|
||||
"no-magic-numbers": () => require("./no-magic-numbers"),
|
||||
"no-misleading-character-class": () => require("./no-misleading-character-class"),
|
||||
"no-mixed-operators": () => require("./no-mixed-operators"),
|
||||
"no-mixed-requires": () => require("./no-mixed-requires"),
|
||||
"no-mixed-spaces-and-tabs": () => require("./no-mixed-spaces-and-tabs"),
|
||||
"no-multi-assign": () => require("./no-multi-assign"),
|
||||
"no-multi-spaces": () => require("./no-multi-spaces"),
|
||||
"no-multi-str": () => require("./no-multi-str"),
|
||||
"no-multiple-empty-lines": () => require("./no-multiple-empty-lines"),
|
||||
"no-native-reassign": () => require("./no-native-reassign"),
|
||||
"no-negated-condition": () => require("./no-negated-condition"),
|
||||
"no-negated-in-lhs": () => require("./no-negated-in-lhs"),
|
||||
"no-nested-ternary": () => require("./no-nested-ternary"),
|
||||
"no-new": () => require("./no-new"),
|
||||
"no-new-func": () => require("./no-new-func"),
|
||||
"no-new-native-nonconstructor": () => require("./no-new-native-nonconstructor"),
|
||||
"no-new-object": () => require("./no-new-object"),
|
||||
"no-new-require": () => require("./no-new-require"),
|
||||
"no-new-symbol": () => require("./no-new-symbol"),
|
||||
"no-new-wrappers": () => require("./no-new-wrappers"),
|
||||
"no-nonoctal-decimal-escape": () => require("./no-nonoctal-decimal-escape"),
|
||||
"no-obj-calls": () => require("./no-obj-calls"),
|
||||
"no-object-constructor": () => require("./no-object-constructor"),
|
||||
"no-octal": () => require("./no-octal"),
|
||||
"no-octal-escape": () => require("./no-octal-escape"),
|
||||
"no-param-reassign": () => require("./no-param-reassign"),
|
||||
"no-path-concat": () => require("./no-path-concat"),
|
||||
"no-plusplus": () => require("./no-plusplus"),
|
||||
"no-process-env": () => require("./no-process-env"),
|
||||
"no-process-exit": () => require("./no-process-exit"),
|
||||
"no-promise-executor-return": () => require("./no-promise-executor-return"),
|
||||
"no-proto": () => require("./no-proto"),
|
||||
"no-prototype-builtins": () => require("./no-prototype-builtins"),
|
||||
"no-redeclare": () => require("./no-redeclare"),
|
||||
"no-regex-spaces": () => require("./no-regex-spaces"),
|
||||
"no-restricted-exports": () => require("./no-restricted-exports"),
|
||||
"no-restricted-globals": () => require("./no-restricted-globals"),
|
||||
"no-restricted-imports": () => require("./no-restricted-imports"),
|
||||
"no-restricted-modules": () => require("./no-restricted-modules"),
|
||||
"no-restricted-properties": () => require("./no-restricted-properties"),
|
||||
"no-restricted-syntax": () => require("./no-restricted-syntax"),
|
||||
"no-return-assign": () => require("./no-return-assign"),
|
||||
"no-return-await": () => require("./no-return-await"),
|
||||
"no-script-url": () => require("./no-script-url"),
|
||||
"no-self-assign": () => require("./no-self-assign"),
|
||||
"no-self-compare": () => require("./no-self-compare"),
|
||||
"no-sequences": () => require("./no-sequences"),
|
||||
"no-setter-return": () => require("./no-setter-return"),
|
||||
"no-shadow": () => require("./no-shadow"),
|
||||
"no-shadow-restricted-names": () => require("./no-shadow-restricted-names"),
|
||||
"no-spaced-func": () => require("./no-spaced-func"),
|
||||
"no-sparse-arrays": () => require("./no-sparse-arrays"),
|
||||
"no-sync": () => require("./no-sync"),
|
||||
"no-tabs": () => require("./no-tabs"),
|
||||
"no-template-curly-in-string": () => require("./no-template-curly-in-string"),
|
||||
"no-ternary": () => require("./no-ternary"),
|
||||
"no-this-before-super": () => require("./no-this-before-super"),
|
||||
"no-throw-literal": () => require("./no-throw-literal"),
|
||||
"no-trailing-spaces": () => require("./no-trailing-spaces"),
|
||||
"no-undef": () => require("./no-undef"),
|
||||
"no-undef-init": () => require("./no-undef-init"),
|
||||
"no-undefined": () => require("./no-undefined"),
|
||||
"no-underscore-dangle": () => require("./no-underscore-dangle"),
|
||||
"no-unexpected-multiline": () => require("./no-unexpected-multiline"),
|
||||
"no-unmodified-loop-condition": () => require("./no-unmodified-loop-condition"),
|
||||
"no-unneeded-ternary": () => require("./no-unneeded-ternary"),
|
||||
"no-unreachable": () => require("./no-unreachable"),
|
||||
"no-unreachable-loop": () => require("./no-unreachable-loop"),
|
||||
"no-unsafe-finally": () => require("./no-unsafe-finally"),
|
||||
"no-unsafe-negation": () => require("./no-unsafe-negation"),
|
||||
"no-unsafe-optional-chaining": () => require("./no-unsafe-optional-chaining"),
|
||||
"no-unused-expressions": () => require("./no-unused-expressions"),
|
||||
"no-unused-labels": () => require("./no-unused-labels"),
|
||||
"no-unused-private-class-members": () => require("./no-unused-private-class-members"),
|
||||
"no-unused-vars": () => require("./no-unused-vars"),
|
||||
"no-use-before-define": () => require("./no-use-before-define"),
|
||||
"no-useless-backreference": () => require("./no-useless-backreference"),
|
||||
"no-useless-call": () => require("./no-useless-call"),
|
||||
"no-useless-catch": () => require("./no-useless-catch"),
|
||||
"no-useless-computed-key": () => require("./no-useless-computed-key"),
|
||||
"no-useless-concat": () => require("./no-useless-concat"),
|
||||
"no-useless-constructor": () => require("./no-useless-constructor"),
|
||||
"no-useless-escape": () => require("./no-useless-escape"),
|
||||
"no-useless-rename": () => require("./no-useless-rename"),
|
||||
"no-useless-return": () => require("./no-useless-return"),
|
||||
"no-var": () => require("./no-var"),
|
||||
"no-void": () => require("./no-void"),
|
||||
"no-warning-comments": () => require("./no-warning-comments"),
|
||||
"no-whitespace-before-property": () => require("./no-whitespace-before-property"),
|
||||
"no-with": () => require("./no-with"),
|
||||
"nonblock-statement-body-position": () => require("./nonblock-statement-body-position"),
|
||||
"object-curly-newline": () => require("./object-curly-newline"),
|
||||
"object-curly-spacing": () => require("./object-curly-spacing"),
|
||||
"object-property-newline": () => require("./object-property-newline"),
|
||||
"object-shorthand": () => require("./object-shorthand"),
|
||||
"one-var": () => require("./one-var"),
|
||||
"one-var-declaration-per-line": () => require("./one-var-declaration-per-line"),
|
||||
"operator-assignment": () => require("./operator-assignment"),
|
||||
"operator-linebreak": () => require("./operator-linebreak"),
|
||||
"padded-blocks": () => require("./padded-blocks"),
|
||||
"padding-line-between-statements": () => require("./padding-line-between-statements"),
|
||||
"prefer-arrow-callback": () => require("./prefer-arrow-callback"),
|
||||
"prefer-const": () => require("./prefer-const"),
|
||||
"prefer-destructuring": () => require("./prefer-destructuring"),
|
||||
"prefer-exponentiation-operator": () => require("./prefer-exponentiation-operator"),
|
||||
"prefer-named-capture-group": () => require("./prefer-named-capture-group"),
|
||||
"prefer-numeric-literals": () => require("./prefer-numeric-literals"),
|
||||
"prefer-object-has-own": () => require("./prefer-object-has-own"),
|
||||
"prefer-object-spread": () => require("./prefer-object-spread"),
|
||||
"prefer-promise-reject-errors": () => require("./prefer-promise-reject-errors"),
|
||||
"prefer-reflect": () => require("./prefer-reflect"),
|
||||
"prefer-regex-literals": () => require("./prefer-regex-literals"),
|
||||
"prefer-rest-params": () => require("./prefer-rest-params"),
|
||||
"prefer-spread": () => require("./prefer-spread"),
|
||||
"prefer-template": () => require("./prefer-template"),
|
||||
"quote-props": () => require("./quote-props"),
|
||||
quotes: () => require("./quotes"),
|
||||
radix: () => require("./radix"),
|
||||
"require-atomic-updates": () => require("./require-atomic-updates"),
|
||||
"require-await": () => require("./require-await"),
|
||||
"require-jsdoc": () => require("./require-jsdoc"),
|
||||
"require-unicode-regexp": () => require("./require-unicode-regexp"),
|
||||
"require-yield": () => require("./require-yield"),
|
||||
"rest-spread-spacing": () => require("./rest-spread-spacing"),
|
||||
semi: () => require("./semi"),
|
||||
"semi-spacing": () => require("./semi-spacing"),
|
||||
"semi-style": () => require("./semi-style"),
|
||||
"sort-imports": () => require("./sort-imports"),
|
||||
"sort-keys": () => require("./sort-keys"),
|
||||
"sort-vars": () => require("./sort-vars"),
|
||||
"space-before-blocks": () => require("./space-before-blocks"),
|
||||
"space-before-function-paren": () => require("./space-before-function-paren"),
|
||||
"space-in-parens": () => require("./space-in-parens"),
|
||||
"space-infix-ops": () => require("./space-infix-ops"),
|
||||
"space-unary-ops": () => require("./space-unary-ops"),
|
||||
"spaced-comment": () => require("./spaced-comment"),
|
||||
strict: () => require("./strict"),
|
||||
"switch-colon-spacing": () => require("./switch-colon-spacing"),
|
||||
"symbol-description": () => require("./symbol-description"),
|
||||
"template-curly-spacing": () => require("./template-curly-spacing"),
|
||||
"template-tag-spacing": () => require("./template-tag-spacing"),
|
||||
"unicode-bom": () => require("./unicode-bom"),
|
||||
"use-isnan": () => require("./use-isnan"),
|
||||
"valid-jsdoc": () => require("./valid-jsdoc"),
|
||||
"valid-typeof": () => require("./valid-typeof"),
|
||||
"vars-on-top": () => require("./vars-on-top"),
|
||||
"wrap-iife": () => require("./wrap-iife"),
|
||||
"wrap-regex": () => require("./wrap-regex"),
|
||||
"yield-star-spacing": () => require("./yield-star-spacing"),
|
||||
yoda: () => require("./yoda")
|
||||
}));
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require or disallow initialization in variable declarations",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/init-declarations"
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
},
|
||||
messages: {
|
||||
initialized: "Variable '{{idName}}' should be initialized on declaration.",
|
||||
notInitialized: "Variable '{{idName}}' should not be initialized on declaration."
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
let messageId = "";
|
||||
|
||||
if (mode === MODE_ALWAYS && !initialized) {
|
||||
messageId = "initialized";
|
||||
} else if (mode === MODE_NEVER && kind !== "const" && initialized && !isIgnoredForLoop) {
|
||||
messageId = "notInitialized";
|
||||
}
|
||||
|
||||
if (id.type === "Identifier" && messageId) {
|
||||
context.report({
|
||||
node: declaration,
|
||||
messageId,
|
||||
data: {
|
||||
idName: id.name
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @fileoverview A rule to ensure consistent quotes used in jsx syntax.
|
||||
* @author Mathias Schreck <https://github.com/lo1tuma>
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const QUOTE_SETTINGS = {
|
||||
"prefer-double": {
|
||||
quote: "\"",
|
||||
description: "singlequote",
|
||||
convert(str) {
|
||||
return str.replace(/'/gu, "\"");
|
||||
}
|
||||
},
|
||||
"prefer-single": {
|
||||
quote: "'",
|
||||
description: "doublequote",
|
||||
convert(str) {
|
||||
return str.replace(/"/gu, "'");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce the consistent use of either double or single quotes in JSX attributes",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/jsx-quotes"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["prefer-single", "prefer-double"]
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
unexpected: "Unexpected usage of {{description}}."
|
||||
}
|
||||
},
|
||||
|
||||
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.includes(setting.quote) || astUtils.isSurroundedBy(node.raw, setting.quote);
|
||||
}
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
const attributeValue = node.value;
|
||||
|
||||
if (attributeValue && astUtils.isStringLiteral(attributeValue) && !usesExpectedQuotes(attributeValue)) {
|
||||
context.report({
|
||||
node: attributeValue,
|
||||
messageId: "unexpected",
|
||||
data: {
|
||||
description: setting.description
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(attributeValue, setting.convert(attributeValue.raw));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+687
@@ -0,0 +1,687 @@
|
||||
/**
|
||||
* @fileoverview Rule to specify spacing of object literal keys and values
|
||||
* @author Brandon Mills
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
const { getGraphemeCount } = require("../shared/string-utils");
|
||||
|
||||
/**
|
||||
* 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 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the properties on a single line.
|
||||
* @param {ASTNode[]} properties List of Property AST nodes.
|
||||
* @returns {boolean} True if all properties is on a single line.
|
||||
*/
|
||||
function isSingleLineProperties(properties) {
|
||||
const [firstProp] = properties,
|
||||
lastProp = last(properties);
|
||||
|
||||
return firstProp.loc.start.line === lastProp.loc.end.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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent spacing between keys and values in object literal properties",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/key-spacing"
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
}],
|
||||
messages: {
|
||||
extraKey: "Extra space after {{computed}}key '{{key}}'.",
|
||||
extraValue: "Extra space before value for {{computed}}key '{{key}}'.",
|
||||
missingKey: "Missing space after {{computed}}key '{{key}}'.",
|
||||
missingValue: "Missing space before value for {{computed}}key '{{key}}'."
|
||||
}
|
||||
},
|
||||
|
||||
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.sourceCode;
|
||||
|
||||
/**
|
||||
* 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 "SpreadElement"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting from the given 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting from the given 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 = getNextColon(node);
|
||||
|
||||
return sourceCode.getTokenBefore(colonToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting from the given node (a property.key node here) looks forward
|
||||
* until it finds the first token after a colon punctuator and returns it.
|
||||
* @param {ASTNode} node The node to start looking from.
|
||||
* @returns {ASTNode} The first token after a colon punctuator.
|
||||
*/
|
||||
function getFirstTokenAfterColon(node) {
|
||||
const colonToken = getNextColon(node);
|
||||
|
||||
return sourceCode.getTokenAfter(colonToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
candidateValueStartLine = (isKeyValueProperty(candidate) ? getFirstTokenAfterColon(candidate.key) : candidate).loc.start.line;
|
||||
|
||||
if (candidateValueStartLine - 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 leadingComments = sourceCode.getCommentsBefore(candidate);
|
||||
|
||||
if (
|
||||
leadingComments.length &&
|
||||
leadingComments[0].loc.start.line - groupEndLine <= 1 &&
|
||||
candidateValueStartLine - last(leadingComments).loc.end.line <= 1
|
||||
) {
|
||||
for (let i = 1; i < leadingComments.length; i++) {
|
||||
if (leadingComments[i].loc.start.line - leadingComments[i - 1].loc.end.line > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 astUtils.getStaticPropertyName(property);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
if ((
|
||||
diff && mode === "strict" ||
|
||||
diff < 0 && mode === "minimum" ||
|
||||
diff > 0 && !expected && mode === "minimum") &&
|
||||
!(expected && containsLineTerminator(whitespace))
|
||||
) {
|
||||
const nextColon = getNextColon(property.key),
|
||||
tokenBeforeColon = sourceCode.getTokenBefore(nextColon, { includeComments: true }),
|
||||
tokenAfterColon = sourceCode.getTokenAfter(nextColon, { includeComments: true }),
|
||||
isKeySide = side === "key",
|
||||
isExtra = diff > 0,
|
||||
diffAbs = Math.abs(diff),
|
||||
spaces = Array(diffAbs + 1).join(" ");
|
||||
|
||||
const locStart = isKeySide ? tokenBeforeColon.loc.end : nextColon.loc.start;
|
||||
const locEnd = isKeySide ? nextColon.loc.start : tokenAfterColon.loc.start;
|
||||
const missingLoc = isKeySide ? tokenBeforeColon.loc : tokenAfterColon.loc;
|
||||
const loc = isExtra ? { start: locStart, end: locEnd } : missingLoc;
|
||||
|
||||
let fix;
|
||||
|
||||
if (isExtra) {
|
||||
let range;
|
||||
|
||||
// Remove whitespace
|
||||
if (isKeySide) {
|
||||
range = [tokenBeforeColon.range[1], tokenBeforeColon.range[1] + diffAbs];
|
||||
} else {
|
||||
range = [tokenAfterColon.range[0] - diffAbs, tokenAfterColon.range[0]];
|
||||
}
|
||||
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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let messageId = "";
|
||||
|
||||
if (isExtra) {
|
||||
messageId = side === "key" ? "extraKey" : "extraValue";
|
||||
} else {
|
||||
messageId = side === "key" ? "missingKey" : "missingValue";
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: property[side],
|
||||
loc,
|
||||
messageId,
|
||||
data: {
|
||||
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 getGraphemeCount(sourceCode.getText().slice(startToken.range[0], endToken.range[1]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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*)/u.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(...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 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.
|
||||
* @param {Object} lineOptions Configured singleLine or multiLine options
|
||||
* @returns {void}
|
||||
*/
|
||||
function verifyListSpacing(properties, lineOptions) {
|
||||
const length = properties.length;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
verifySpacing(properties[i], lineOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 => {
|
||||
const properties = group.filter(isKeyValueProperty);
|
||||
|
||||
if (properties.length > 0 && isSingleLineProperties(properties)) {
|
||||
verifyListSpacing(properties, multiLineOptions);
|
||||
} else {
|
||||
verifyGroupAlignment(properties);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
if (alignmentOptions) { // Verify vertical alignment
|
||||
|
||||
return {
|
||||
ObjectExpression(node) {
|
||||
if (isSingleLine(node)) {
|
||||
verifyListSpacing(node.properties.filter(isKeyValueProperty), singleLineOptions);
|
||||
} else {
|
||||
verifyAlignment(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Obey beforeColon and afterColon in each property as configured
|
||||
return {
|
||||
Property(node) {
|
||||
verifySpacing(node, isSingleLine(node.parent) ? singleLineOptions : multiLineOptions);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
+640
@@ -0,0 +1,640 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce spacing before and after keywords.
|
||||
* @author Toru Nagashima
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils"),
|
||||
keywords = require("./utils/keywords");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const PREV_TOKEN = /^[)\]}>]$/u;
|
||||
const NEXT_TOKEN = /^(?:[([{<~!]|\+\+?|--?)$/u;
|
||||
const PREV_TOKEN_M = /^[)\]}>*]$/u;
|
||||
const NEXT_TOKEN_M = /^[{*]$/u;
|
||||
const TEMPLATE_OPEN_PAREN = /\$\{$/u;
|
||||
const TEMPLATE_CLOSE_PAREN = /^\}/u;
|
||||
const CHECK_TYPE = /^(?:JSXElement|RegularExpression|String|Template|PrivateIdentifier)$/u;
|
||||
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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent spacing before and after keywords",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/keyword-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
before: { type: "boolean", default: true },
|
||||
after: { type: "boolean", default: true },
|
||||
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
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
expectedBefore: "Expected space(s) before \"{{value}}\".",
|
||||
expectedAfter: "Expected space(s) after \"{{value}}\".",
|
||||
unexpectedBefore: "Unexpected space(s) before \"{{value}}\".",
|
||||
unexpectedAfter: "Unexpected space(s) after \"{{value}}\"."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
const tokensToIgnore = new WeakSet();
|
||||
|
||||
/**
|
||||
* Reports a given token if there are not space(s) before the token.
|
||||
* @param {Token} token A token to report.
|
||||
* @param {RegExp} pattern A pattern of the previous token to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function expectSpaceBefore(token, pattern) {
|
||||
const prevToken = sourceCode.getTokenBefore(token);
|
||||
|
||||
if (prevToken &&
|
||||
(CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
|
||||
!isOpenParenOfTemplate(prevToken) &&
|
||||
!tokensToIgnore.has(prevToken) &&
|
||||
astUtils.isTokenOnSameLine(prevToken, token) &&
|
||||
!sourceCode.isSpaceBetweenTokens(prevToken, token)
|
||||
) {
|
||||
context.report({
|
||||
loc: token.loc,
|
||||
messageId: "expectedBefore",
|
||||
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} pattern A pattern of the previous token to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function unexpectSpaceBefore(token, pattern) {
|
||||
const prevToken = sourceCode.getTokenBefore(token);
|
||||
|
||||
if (prevToken &&
|
||||
(CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
|
||||
!isOpenParenOfTemplate(prevToken) &&
|
||||
!tokensToIgnore.has(prevToken) &&
|
||||
astUtils.isTokenOnSameLine(prevToken, token) &&
|
||||
sourceCode.isSpaceBetweenTokens(prevToken, token)
|
||||
) {
|
||||
context.report({
|
||||
loc: { start: prevToken.loc.end, end: token.loc.start },
|
||||
messageId: "unexpectedBefore",
|
||||
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} pattern A pattern of the next token to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function expectSpaceAfter(token, pattern) {
|
||||
const nextToken = sourceCode.getTokenAfter(token);
|
||||
|
||||
if (nextToken &&
|
||||
(CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
|
||||
!isCloseParenOfTemplate(nextToken) &&
|
||||
!tokensToIgnore.has(nextToken) &&
|
||||
astUtils.isTokenOnSameLine(token, nextToken) &&
|
||||
!sourceCode.isSpaceBetweenTokens(token, nextToken)
|
||||
) {
|
||||
context.report({
|
||||
loc: token.loc,
|
||||
messageId: "expectedAfter",
|
||||
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} pattern A pattern of the next token to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function unexpectSpaceAfter(token, pattern) {
|
||||
const nextToken = sourceCode.getTokenAfter(token);
|
||||
|
||||
if (nextToken &&
|
||||
(CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
|
||||
!isCloseParenOfTemplate(nextToken) &&
|
||||
!tokensToIgnore.has(nextToken) &&
|
||||
astUtils.isTokenOnSameLine(token, nextToken) &&
|
||||
sourceCode.isSpaceBetweenTokens(token, nextToken)
|
||||
) {
|
||||
|
||||
context.report({
|
||||
loc: { start: token.loc.end, end: nextToken.loc.start },
|
||||
messageId: "unexpectedAfter",
|
||||
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.before !== false;
|
||||
const after = 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} [pattern] Optional. A pattern of the previous
|
||||
* token to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingBefore(token, pattern) {
|
||||
checkMethodMap[token.value].before(token, pattern || PREV_TOKEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given token if usage of spacing preceded by the token is
|
||||
* invalid.
|
||||
* @param {Token} token A token to report.
|
||||
* @param {RegExp} [pattern] Optional. A pattern of the next
|
||||
* token to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingAfter(token, pattern) {
|
||||
checkMethodMap[token.value].after(token, pattern || NEXT_TOKEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
const inToken = sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken);
|
||||
const previousToken = sourceCode.getTokenBefore(inToken);
|
||||
|
||||
if (previousToken.type !== "PrivateIdentifier") {
|
||||
checkSpacingBefore(inToken);
|
||||
}
|
||||
|
||||
checkSpacingAfter(inToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (node.await) {
|
||||
checkSpacingBefore(sourceCode.getFirstToken(node, 0));
|
||||
checkSpacingAfter(sourceCode.getFirstToken(node, 1));
|
||||
} else {
|
||||
checkSpacingAroundFirstToken(node);
|
||||
}
|
||||
|
||||
const ofToken = sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken);
|
||||
const previousToken = sourceCode.getTokenBefore(ofToken);
|
||||
|
||||
if (previousToken.type !== "PrivateIdentifier") {
|
||||
checkSpacingBefore(ofToken);
|
||||
}
|
||||
|
||||
checkSpacingAfter(ofToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.type === "ExportDefaultDeclaration") {
|
||||
checkSpacingAround(sourceCode.getTokenAfter(firstToken));
|
||||
}
|
||||
|
||||
if (node.type === "ExportAllDeclaration" && node.exported) {
|
||||
const asToken = sourceCode.getTokenBefore(node.exported);
|
||||
|
||||
checkSpacingBefore(asToken, PREV_TOKEN_M);
|
||||
checkSpacingAfter(asToken, 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 An `ImportSpecifier` node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingForImportSpecifier(node) {
|
||||
if (node.imported.range[0] !== node.local.range[0]) {
|
||||
const asToken = sourceCode.getTokenBefore(node.local);
|
||||
|
||||
checkSpacingBefore(asToken, PREV_TOKEN_M);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports `as` keyword of a given node if usage of spacing around this
|
||||
* keyword is invalid.
|
||||
* @param {ASTNode} node An `ExportSpecifier` node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingForExportSpecifier(node) {
|
||||
if (node.local.range[0] !== node.exported.range[0]) {
|
||||
const asToken = sourceCode.getTokenBefore(node.exported);
|
||||
|
||||
checkSpacingBefore(asToken, PREV_TOKEN_M);
|
||||
checkSpacingAfter(asToken, 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.
|
||||
* @throws {Error} If unable to find token get, set, or async beside method name.
|
||||
* @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: checkSpacingForModuleDeclaration,
|
||||
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
|
||||
ImportSpecifier: checkSpacingForImportSpecifier,
|
||||
ExportSpecifier: checkSpacingForExportSpecifier,
|
||||
ImportNamespaceSpecifier: checkSpacingForImportNamespaceSpecifier,
|
||||
MethodDefinition: checkSpacingForProperty,
|
||||
PropertyDefinition: checkSpacingForProperty,
|
||||
StaticBlock: checkSpacingAroundFirstToken,
|
||||
Property: checkSpacingForProperty,
|
||||
|
||||
// To avoid conflicts with `space-infix-ops`, e.g. `a > this.b`
|
||||
"BinaryExpression[operator='>']"(node) {
|
||||
const operatorToken = sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken);
|
||||
|
||||
tokensToIgnore.add(operatorToken);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce the position of line comments
|
||||
* @author Alberto Rodríguez
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce position of line comments",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/line-comment-position"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["above", "beside"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
position: {
|
||||
enum: ["above", "beside"]
|
||||
},
|
||||
ignorePattern: {
|
||||
type: "string"
|
||||
},
|
||||
applyDefaultPatterns: {
|
||||
type: "boolean"
|
||||
},
|
||||
applyDefaultIgnorePatterns: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
above: "Expected comment to be above code.",
|
||||
beside: "Expected comment to be beside code."
|
||||
}
|
||||
},
|
||||
|
||||
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 || options.position === "above";
|
||||
ignorePattern = options.ignorePattern;
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(options, "applyDefaultIgnorePatterns")) {
|
||||
applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns;
|
||||
} else {
|
||||
applyDefaultIgnorePatterns = options.applyDefaultPatterns !== false;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN;
|
||||
const fallThroughRegExp = /^\s*falls?\s?through/u;
|
||||
const customIgnoreRegExp = new RegExp(ignorePattern, "u");
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program() {
|
||||
const comments = sourceCode.getAllComments();
|
||||
|
||||
comments.filter(token => token.type === "Line").forEach(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,
|
||||
messageId: "above"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!isOnSameLine) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "beside"
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce a single linebreak style.
|
||||
* @author Erik Mueller
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce consistent linebreak style",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/linebreak-style"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["unix", "windows"]
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
expectedLF: "Expected linebreaks to be 'LF' but found 'CRLF'.",
|
||||
expectedCRLF: "Expected linebreaks to be 'CRLF' but found 'LF'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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: {
|
||||
start: {
|
||||
line: i,
|
||||
column: sourceCode.lines[i - 1].length
|
||||
},
|
||||
end: {
|
||||
line: i + 1,
|
||||
column: 0
|
||||
}
|
||||
},
|
||||
messageId: expectedLF ? "expectedLF" : "expectedCRLF",
|
||||
fix: createFix(range, expectedLFChars)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+471
@@ -0,0 +1,471 @@
|
||||
/**
|
||||
* @fileoverview Enforces empty lines around comments.
|
||||
* @author Jamund Ferguson
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return an array 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 any line numbers that contain comments.
|
||||
* @param {Array} comments An array of comment tokens.
|
||||
* @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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Require empty lines around comments",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/lines-around-comment"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
beforeBlockComment: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
},
|
||||
afterBlockComment: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
beforeLineComment: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
afterLineComment: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
allowBlockStart: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
allowBlockEnd: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
allowClassStart: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowClassEnd: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowObjectStart: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowObjectEnd: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowArrayStart: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowArrayEnd: {
|
||||
type: "boolean"
|
||||
},
|
||||
ignorePattern: {
|
||||
type: "string"
|
||||
},
|
||||
applyDefaultIgnorePatterns: {
|
||||
type: "boolean"
|
||||
},
|
||||
afterHashbangComment: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
after: "Expected line after comment.",
|
||||
before: "Expected line before comment."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const options = Object.assign({}, context.options[0]);
|
||||
const ignorePattern = options.ignorePattern;
|
||||
const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN;
|
||||
const customIgnoreRegExp = new RegExp(ignorePattern, "u");
|
||||
const applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns !== false;
|
||||
|
||||
options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true;
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
const lines = sourceCode.lines,
|
||||
numLines = lines.length + 1,
|
||||
comments = sourceCode.getAllComments(),
|
||||
commentLines = getCommentLineNums(comments),
|
||||
emptyLines = getEmptyLineNums(lines),
|
||||
commentAndEmptyLines = new Set(commentLines.concat(emptyLines));
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are on lines starting with or ending with code
|
||||
* @param {token} token The comment token to check.
|
||||
* @returns {boolean} True if the comment is not alone.
|
||||
*/
|
||||
function codeAroundComment(token) {
|
||||
let currentToken = token;
|
||||
|
||||
do {
|
||||
currentToken = sourceCode.getTokenBefore(currentToken, { includeComments: true });
|
||||
} while (currentToken && astUtils.isCommentToken(currentToken));
|
||||
|
||||
if (currentToken && astUtils.isTokenOnSameLine(currentToken, token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
currentToken = token;
|
||||
do {
|
||||
currentToken = sourceCode.getTokenAfter(currentToken, { includeComments: true });
|
||||
} while (currentToken && astUtils.isCommentToken(currentToken));
|
||||
|
||||
if (currentToken && astUtils.isTokenOnSameLine(token, currentToken)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are inside a node type or not.
|
||||
* @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 isParentNodeType(parent, nodeType) {
|
||||
return parent.type === nodeType ||
|
||||
(parent.body && parent.body.type === nodeType) ||
|
||||
(parent.consequent && parent.consequent.type === nodeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent node that contains the given token.
|
||||
* @param {token} token The token to check.
|
||||
* @returns {ASTNode|null} The parent node that contains the given token.
|
||||
*/
|
||||
function getParentNodeOfToken(token) {
|
||||
const node = sourceCode.getNodeByRangeIndex(token.range[0]);
|
||||
|
||||
/*
|
||||
* For the purpose of this rule, the comment token is in a `StaticBlock` node only
|
||||
* if it's inside the braces of that `StaticBlock` node.
|
||||
*
|
||||
* Example where this function returns `null`:
|
||||
*
|
||||
* static
|
||||
* // comment
|
||||
* {
|
||||
* }
|
||||
*
|
||||
* Example where this function returns `StaticBlock` node:
|
||||
*
|
||||
* static
|
||||
* {
|
||||
* // comment
|
||||
* }
|
||||
*
|
||||
*/
|
||||
if (node && node.type === "StaticBlock") {
|
||||
const openingBrace = sourceCode.getFirstToken(node, { skip: 1 }); // skip the `static` token
|
||||
|
||||
return token.range[0] >= openingBrace.range[0]
|
||||
? node
|
||||
: null;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the parent start or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @param {string} nodeType The parent type to check against.
|
||||
* @returns {boolean} True if the comment is at parent start.
|
||||
*/
|
||||
function isCommentAtParentStart(token, nodeType) {
|
||||
const parent = getParentNodeOfToken(token);
|
||||
|
||||
if (parent && isParentNodeType(parent, nodeType)) {
|
||||
let parentStartNodeOrToken = parent;
|
||||
|
||||
if (parent.type === "StaticBlock") {
|
||||
parentStartNodeOrToken = sourceCode.getFirstToken(parent, { skip: 1 }); // opening brace of the static block
|
||||
} else if (parent.type === "SwitchStatement") {
|
||||
parentStartNodeOrToken = sourceCode.getTokenAfter(parent.discriminant, {
|
||||
filter: astUtils.isOpeningBraceToken
|
||||
}); // opening brace of the switch statement
|
||||
}
|
||||
|
||||
return token.loc.start.line - parentStartNodeOrToken.loc.start.line === 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the parent end or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @param {string} nodeType The parent type to check against.
|
||||
* @returns {boolean} True if the comment is at parent end.
|
||||
*/
|
||||
function isCommentAtParentEnd(token, nodeType) {
|
||||
const parent = getParentNodeOfToken(token);
|
||||
|
||||
return !!parent && isParentNodeType(parent, nodeType) &&
|
||||
parent.loc.end.line - token.loc.end.line === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the block start or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at block start.
|
||||
*/
|
||||
function isCommentAtBlockStart(token) {
|
||||
return (
|
||||
isCommentAtParentStart(token, "ClassBody") ||
|
||||
isCommentAtParentStart(token, "BlockStatement") ||
|
||||
isCommentAtParentStart(token, "StaticBlock") ||
|
||||
isCommentAtParentStart(token, "SwitchCase") ||
|
||||
isCommentAtParentStart(token, "SwitchStatement")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the block end or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at block end.
|
||||
*/
|
||||
function isCommentAtBlockEnd(token) {
|
||||
return (
|
||||
isCommentAtParentEnd(token, "ClassBody") ||
|
||||
isCommentAtParentEnd(token, "BlockStatement") ||
|
||||
isCommentAtParentEnd(token, "StaticBlock") ||
|
||||
isCommentAtParentEnd(token, "SwitchCase") ||
|
||||
isCommentAtParentEnd(token, "SwitchStatement")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the class start or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at class start.
|
||||
*/
|
||||
function isCommentAtClassStart(token) {
|
||||
return isCommentAtParentStart(token, "ClassBody");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the class end or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at class end.
|
||||
*/
|
||||
function isCommentAtClassEnd(token) {
|
||||
return isCommentAtParentEnd(token, "ClassBody");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the object start or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at object start.
|
||||
*/
|
||||
function isCommentAtObjectStart(token) {
|
||||
return isCommentAtParentStart(token, "ObjectExpression") || isCommentAtParentStart(token, "ObjectPattern");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the object end or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at object end.
|
||||
*/
|
||||
function isCommentAtObjectEnd(token) {
|
||||
return isCommentAtParentEnd(token, "ObjectExpression") || isCommentAtParentEnd(token, "ObjectPattern");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the array start or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at array start.
|
||||
*/
|
||||
function isCommentAtArrayStart(token) {
|
||||
return isCommentAtParentStart(token, "ArrayExpression") || isCommentAtParentStart(token, "ArrayPattern");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the array end or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at array end.
|
||||
*/
|
||||
function isCommentAtArrayEnd(token) {
|
||||
return isCommentAtParentEnd(token, "ArrayExpression") || isCommentAtParentEnd(token, "ArrayPattern");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a comment token has lines around it (ignores inline comments)
|
||||
* @param {token} token The Comment token.
|
||||
* @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(token, opts) {
|
||||
if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(token.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ignorePattern && customIgnoreRegExp.test(token.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let after = opts.after,
|
||||
before = opts.before;
|
||||
|
||||
const prevLineNum = token.loc.start.line - 1,
|
||||
nextLineNum = token.loc.end.line + 1,
|
||||
commentIsNotAlone = codeAroundComment(token);
|
||||
|
||||
const blockStartAllowed = options.allowBlockStart &&
|
||||
isCommentAtBlockStart(token) &&
|
||||
!(options.allowClassStart === false &&
|
||||
isCommentAtClassStart(token)),
|
||||
blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(token) && !(options.allowClassEnd === false && isCommentAtClassEnd(token)),
|
||||
classStartAllowed = options.allowClassStart && isCommentAtClassStart(token),
|
||||
classEndAllowed = options.allowClassEnd && isCommentAtClassEnd(token),
|
||||
objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(token),
|
||||
objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(token),
|
||||
arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(token),
|
||||
arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(token);
|
||||
|
||||
const exceptionStartAllowed = blockStartAllowed || classStartAllowed || objectStartAllowed || arrayStartAllowed;
|
||||
const exceptionEndAllowed = blockEndAllowed || classEndAllowed || 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(token, { includeComments: true });
|
||||
const nextTokenOrComment = sourceCode.getTokenAfter(token, { includeComments: true });
|
||||
|
||||
// check for newline before
|
||||
if (!exceptionStartAllowed && before && !commentAndEmptyLines.has(prevLineNum) &&
|
||||
!(astUtils.isCommentToken(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, token))) {
|
||||
const lineStart = token.range[0] - token.loc.start.column;
|
||||
const range = [lineStart, lineStart];
|
||||
|
||||
context.report({
|
||||
node: token,
|
||||
messageId: "before",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBeforeRange(range, "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// check for newline after
|
||||
if (!exceptionEndAllowed && after && !commentAndEmptyLines.has(nextLineNum) &&
|
||||
!(astUtils.isCommentToken(nextTokenOrComment) && astUtils.isTokenOnSameLine(token, nextTokenOrComment))) {
|
||||
context.report({
|
||||
node: token,
|
||||
messageId: "after",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(token, "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program() {
|
||||
comments.forEach(token => {
|
||||
if (token.type === "Line") {
|
||||
if (options.beforeLineComment || options.afterLineComment) {
|
||||
checkForEmptyLine(token, {
|
||||
after: options.afterLineComment,
|
||||
before: options.beforeLineComment
|
||||
});
|
||||
}
|
||||
} else if (token.type === "Block") {
|
||||
if (options.beforeBlockComment || options.afterBlockComment) {
|
||||
checkForEmptyLine(token, {
|
||||
after: options.afterBlockComment,
|
||||
before: options.beforeBlockComment
|
||||
});
|
||||
}
|
||||
} else if (token.type === "Shebang") {
|
||||
if (options.afterHashbangComment) {
|
||||
checkForEmptyLine(token, {
|
||||
after: options.afterHashbangComment,
|
||||
before: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* @fileoverview Require or disallow newlines around directives.
|
||||
* @author Kai Cataldo
|
||||
* @deprecated in ESLint v4.0.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Require or disallow newlines around directives",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/lines-around-directive"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["always", "never"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
before: {
|
||||
enum: ["always", "never"]
|
||||
},
|
||||
after: {
|
||||
enum: ["always", "never"]
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
minProperties: 2
|
||||
}
|
||||
]
|
||||
}],
|
||||
|
||||
fixable: "whitespace",
|
||||
messages: {
|
||||
expected: "Expected newline {{location}} \"{{value}}\" directive.",
|
||||
unexpected: "Unexpected newline {{location}} \"{{value}}\" directive."
|
||||
},
|
||||
deprecated: true,
|
||||
replacedBy: ["padding-line-between-statements"]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
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,
|
||||
messageId: expected ? "expected" : "unexpected",
|
||||
data: {
|
||||
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 leadingComments = sourceCode.getCommentsBefore(firstDirective);
|
||||
|
||||
/*
|
||||
* 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 (leadingComments.length) {
|
||||
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" &&
|
||||
!leadingComments.length &&
|
||||
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
|
||||
};
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+269
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* @fileoverview Rule to check empty newline between class members
|
||||
* @author 薛定谔的猫<hh_2013@foxmail.com>
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Types of class members.
|
||||
* Those have `test` method to check it matches to the given class member.
|
||||
* @private
|
||||
*/
|
||||
const ClassMemberTypes = {
|
||||
"*": { test: () => true },
|
||||
field: { test: node => node.type === "PropertyDefinition" },
|
||||
method: { test: node => node.type === "MethodDefinition" }
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Require or disallow an empty line between class members",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/lines-between-class-members"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
anyOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
enforce: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
blankLine: { enum: ["always", "never"] },
|
||||
prev: { enum: ["method", "field", "*"] },
|
||||
next: { enum: ["method", "field", "*"] }
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ["blankLine", "prev", "next"]
|
||||
},
|
||||
minItems: 1
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ["enforce"]
|
||||
},
|
||||
{
|
||||
enum: ["always", "never"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
exceptAfterSingleLine: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
never: "Unexpected blank line between class members.",
|
||||
always: "Expected blank line between class members."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const options = [];
|
||||
|
||||
options[0] = context.options[0] || "always";
|
||||
options[1] = context.options[1] || { exceptAfterSingleLine: false };
|
||||
|
||||
const configureList = typeof options[0] === "object" ? options[0].enforce : [{ blankLine: options[0], prev: "*", next: "*" }];
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Gets a pair of tokens that should be used to check lines between two class member nodes.
|
||||
*
|
||||
* In most cases, this returns the very last token of the current node and
|
||||
* the very first token of the next node.
|
||||
* For example:
|
||||
*
|
||||
* class C {
|
||||
* x = 1; // curLast: `;` nextFirst: `in`
|
||||
* in = 2
|
||||
* }
|
||||
*
|
||||
* There is only one exception. If the given node ends with a semicolon, and it looks like
|
||||
* a semicolon-less style's semicolon - one that is not on the same line as the preceding
|
||||
* token, but is on the line where the next class member starts - this returns the preceding
|
||||
* token and the semicolon as boundary tokens.
|
||||
* For example:
|
||||
*
|
||||
* class C {
|
||||
* x = 1 // curLast: `1` nextFirst: `;`
|
||||
* ;in = 2
|
||||
* }
|
||||
* When determining the desired layout of the code, we should treat this semicolon as
|
||||
* a part of the next class member node instead of the one it technically belongs to.
|
||||
* @param {ASTNode} curNode Current class member node.
|
||||
* @param {ASTNode} nextNode Next class member node.
|
||||
* @returns {Token} The actual last token of `node`.
|
||||
* @private
|
||||
*/
|
||||
function getBoundaryTokens(curNode, nextNode) {
|
||||
const lastToken = sourceCode.getLastToken(curNode);
|
||||
const prevToken = sourceCode.getTokenBefore(lastToken);
|
||||
const nextToken = sourceCode.getFirstToken(nextNode); // skip possible lone `;` between nodes
|
||||
|
||||
const isSemicolonLessStyle = (
|
||||
astUtils.isSemicolonToken(lastToken) &&
|
||||
!astUtils.isTokenOnSameLine(prevToken, lastToken) &&
|
||||
astUtils.isTokenOnSameLine(lastToken, nextToken)
|
||||
);
|
||||
|
||||
return isSemicolonLessStyle
|
||||
? { curLast: prevToken, nextFirst: lastToken }
|
||||
: { curLast: lastToken, nextFirst: nextToken };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last token among the consecutive tokens that have no exceed max line difference in between, before the first token in the next member.
|
||||
* @param {Token} prevLastToken The last token in the previous member node.
|
||||
* @param {Token} nextFirstToken The first token in the next member node.
|
||||
* @param {number} maxLine The maximum number of allowed line difference between consecutive tokens.
|
||||
* @returns {Token} The last token among the consecutive tokens.
|
||||
*/
|
||||
function findLastConsecutiveTokenAfter(prevLastToken, nextFirstToken, maxLine) {
|
||||
const after = sourceCode.getTokenAfter(prevLastToken, { includeComments: true });
|
||||
|
||||
if (after !== nextFirstToken && after.loc.start.line - prevLastToken.loc.end.line <= maxLine) {
|
||||
return findLastConsecutiveTokenAfter(after, nextFirstToken, maxLine);
|
||||
}
|
||||
return prevLastToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first token among the consecutive tokens that have no exceed max line difference in between, after the last token in the previous member.
|
||||
* @param {Token} nextFirstToken The first token in the next member node.
|
||||
* @param {Token} prevLastToken The last token in the previous member node.
|
||||
* @param {number} maxLine The maximum number of allowed line difference between consecutive tokens.
|
||||
* @returns {Token} The first token among the consecutive tokens.
|
||||
*/
|
||||
function findFirstConsecutiveTokenBefore(nextFirstToken, prevLastToken, maxLine) {
|
||||
const before = sourceCode.getTokenBefore(nextFirstToken, { includeComments: true });
|
||||
|
||||
if (before !== prevLastToken && nextFirstToken.loc.start.line - before.loc.end.line <= maxLine) {
|
||||
return findFirstConsecutiveTokenBefore(before, prevLastToken, maxLine);
|
||||
}
|
||||
return nextFirstToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is a token or comment between two tokens.
|
||||
* @param {Token} before The token before.
|
||||
* @param {Token} after The token after.
|
||||
* @returns {boolean} True if there is a token or comment between two tokens.
|
||||
*/
|
||||
function hasTokenOrCommentBetween(before, after) {
|
||||
return sourceCode.getTokensBetween(before, after, { includeComments: true }).length !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given node matches the given type.
|
||||
* @param {ASTNode} node The class member node to check.
|
||||
* @param {string} type The class member type to check.
|
||||
* @returns {boolean} `true` if the class member node matched the type.
|
||||
* @private
|
||||
*/
|
||||
function match(node, type) {
|
||||
return ClassMemberTypes[type].test(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the last matched configuration from the configureList.
|
||||
* @param {ASTNode} prevNode The previous node to match.
|
||||
* @param {ASTNode} nextNode The current node to match.
|
||||
* @returns {string|null} Padding type or `null` if no matches were found.
|
||||
* @private
|
||||
*/
|
||||
function getPaddingType(prevNode, nextNode) {
|
||||
for (let i = configureList.length - 1; i >= 0; --i) {
|
||||
const configure = configureList[i];
|
||||
const matched =
|
||||
match(prevNode, configure.prev) &&
|
||||
match(nextNode, configure.next);
|
||||
|
||||
if (matched) {
|
||||
return configure.blankLine;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
ClassBody(node) {
|
||||
const body = node.body;
|
||||
|
||||
for (let i = 0; i < body.length - 1; i++) {
|
||||
const curFirst = sourceCode.getFirstToken(body[i]);
|
||||
const { curLast, nextFirst } = getBoundaryTokens(body[i], body[i + 1]);
|
||||
const isMulti = !astUtils.isTokenOnSameLine(curFirst, curLast);
|
||||
const skip = !isMulti && options[1].exceptAfterSingleLine;
|
||||
const beforePadding = findLastConsecutiveTokenAfter(curLast, nextFirst, 1);
|
||||
const afterPadding = findFirstConsecutiveTokenBefore(nextFirst, curLast, 1);
|
||||
const isPadded = afterPadding.loc.start.line - beforePadding.loc.end.line > 1;
|
||||
const hasTokenInPadding = hasTokenOrCommentBetween(beforePadding, afterPadding);
|
||||
const curLineLastToken = findLastConsecutiveTokenAfter(curLast, nextFirst, 0);
|
||||
const paddingType = getPaddingType(body[i], body[i + 1]);
|
||||
|
||||
if (paddingType === "never" && isPadded) {
|
||||
context.report({
|
||||
node: body[i + 1],
|
||||
messageId: "never",
|
||||
|
||||
fix(fixer) {
|
||||
if (hasTokenInPadding) {
|
||||
return null;
|
||||
}
|
||||
return fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n");
|
||||
}
|
||||
});
|
||||
} else if (paddingType === "always" && !skip && !isPadded) {
|
||||
context.report({
|
||||
node: body[i + 1],
|
||||
messageId: "always",
|
||||
|
||||
fix(fixer) {
|
||||
if (hasTokenInPadding) {
|
||||
return null;
|
||||
}
|
||||
return fixer.insertTextAfter(curLineLastToken, "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+504
@@ -0,0 +1,504 @@
|
||||
/**
|
||||
* @fileoverview Rule to replace assignment expressions with logical operator assignment
|
||||
* @author Daniel Martens
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
const astUtils = require("./utils/ast-utils.js");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const baseTypes = new Set(["Identifier", "Super", "ThisExpression"]);
|
||||
|
||||
/**
|
||||
* Returns true iff either "undefined" or a void expression (eg. "void 0")
|
||||
* @param {ASTNode} expression Expression to check
|
||||
* @param {import('eslint-scope').Scope} scope Scope of the expression
|
||||
* @returns {boolean} True iff "undefined" or "void ..."
|
||||
*/
|
||||
function isUndefined(expression, scope) {
|
||||
if (expression.type === "Identifier" && expression.name === "undefined") {
|
||||
return astUtils.isReferenceToGlobalVariable(scope, expression);
|
||||
}
|
||||
|
||||
return expression.type === "UnaryExpression" &&
|
||||
expression.operator === "void" &&
|
||||
expression.argument.type === "Literal" &&
|
||||
expression.argument.value === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true iff the reference is either an identifier or member expression
|
||||
* @param {ASTNode} expression Expression to check
|
||||
* @returns {boolean} True for identifiers and member expressions
|
||||
*/
|
||||
function isReference(expression) {
|
||||
return (expression.type === "Identifier" && expression.name !== "undefined") ||
|
||||
expression.type === "MemberExpression";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true iff the expression checks for nullish with loose equals.
|
||||
* Examples: value == null, value == void 0
|
||||
* @param {ASTNode} expression Test condition
|
||||
* @param {import('eslint-scope').Scope} scope Scope of the expression
|
||||
* @returns {boolean} True iff implicit nullish comparison
|
||||
*/
|
||||
function isImplicitNullishComparison(expression, scope) {
|
||||
if (expression.type !== "BinaryExpression" || expression.operator !== "==") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const reference = isReference(expression.left) ? "left" : "right";
|
||||
const nullish = reference === "left" ? "right" : "left";
|
||||
|
||||
return isReference(expression[reference]) &&
|
||||
(astUtils.isNullLiteral(expression[nullish]) || isUndefined(expression[nullish], scope));
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition with two equal comparisons.
|
||||
* @param {ASTNode} expression Condition
|
||||
* @returns {boolean} True iff matches ? === ? || ? === ?
|
||||
*/
|
||||
function isDoubleComparison(expression) {
|
||||
return expression.type === "LogicalExpression" &&
|
||||
expression.operator === "||" &&
|
||||
expression.left.type === "BinaryExpression" &&
|
||||
expression.left.operator === "===" &&
|
||||
expression.right.type === "BinaryExpression" &&
|
||||
expression.right.operator === "===";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true iff the expression checks for undefined and null.
|
||||
* Example: value === null || value === undefined
|
||||
* @param {ASTNode} expression Test condition
|
||||
* @param {import('eslint-scope').Scope} scope Scope of the expression
|
||||
* @returns {boolean} True iff explicit nullish comparison
|
||||
*/
|
||||
function isExplicitNullishComparison(expression, scope) {
|
||||
if (!isDoubleComparison(expression)) {
|
||||
return false;
|
||||
}
|
||||
const leftReference = isReference(expression.left.left) ? "left" : "right";
|
||||
const leftNullish = leftReference === "left" ? "right" : "left";
|
||||
const rightReference = isReference(expression.right.left) ? "left" : "right";
|
||||
const rightNullish = rightReference === "left" ? "right" : "left";
|
||||
|
||||
return astUtils.isSameReference(expression.left[leftReference], expression.right[rightReference]) &&
|
||||
((astUtils.isNullLiteral(expression.left[leftNullish]) && isUndefined(expression.right[rightNullish], scope)) ||
|
||||
(isUndefined(expression.left[leftNullish], scope) && astUtils.isNullLiteral(expression.right[rightNullish])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true for Boolean(arg) calls
|
||||
* @param {ASTNode} expression Test condition
|
||||
* @param {import('eslint-scope').Scope} scope Scope of the expression
|
||||
* @returns {boolean} Whether the expression is a boolean cast
|
||||
*/
|
||||
function isBooleanCast(expression, scope) {
|
||||
return expression.type === "CallExpression" &&
|
||||
expression.callee.name === "Boolean" &&
|
||||
expression.arguments.length === 1 &&
|
||||
astUtils.isReferenceToGlobalVariable(scope, expression.callee);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true for:
|
||||
* truthiness checks: value, Boolean(value), !!value
|
||||
* falsiness checks: !value, !Boolean(value)
|
||||
* nullish checks: value == null, value === undefined || value === null
|
||||
* @param {ASTNode} expression Test condition
|
||||
* @param {import('eslint-scope').Scope} scope Scope of the expression
|
||||
* @returns {?{ reference: ASTNode, operator: '??'|'||'|'&&'}} Null if not a known existence
|
||||
*/
|
||||
function getExistence(expression, scope) {
|
||||
const isNegated = expression.type === "UnaryExpression" && expression.operator === "!";
|
||||
const base = isNegated ? expression.argument : expression;
|
||||
|
||||
switch (true) {
|
||||
case isReference(base):
|
||||
return { reference: base, operator: isNegated ? "||" : "&&" };
|
||||
case base.type === "UnaryExpression" && base.operator === "!" && isReference(base.argument):
|
||||
return { reference: base.argument, operator: "&&" };
|
||||
case isBooleanCast(base, scope) && isReference(base.arguments[0]):
|
||||
return { reference: base.arguments[0], operator: isNegated ? "||" : "&&" };
|
||||
case isImplicitNullishComparison(expression, scope):
|
||||
return { reference: isReference(expression.left) ? expression.left : expression.right, operator: "??" };
|
||||
case isExplicitNullishComparison(expression, scope):
|
||||
return { reference: isReference(expression.left.left) ? expression.left.left : expression.left.right, operator: "??" };
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true iff the node is inside a with block
|
||||
* @param {ASTNode} node Node to check
|
||||
* @returns {boolean} True iff passed node is inside a with block
|
||||
*/
|
||||
function isInsideWithBlock(node) {
|
||||
if (node.type === "Program") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return node.parent.type === "WithStatement" && node.parent.body === node ? true : isInsideWithBlock(node.parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the leftmost operand of a consecutive logical expression.
|
||||
* @param {SourceCode} sourceCode The ESLint source code object
|
||||
* @param {LogicalExpression} node LogicalExpression
|
||||
* @returns {Expression} Leftmost operand
|
||||
*/
|
||||
function getLeftmostOperand(sourceCode, node) {
|
||||
let left = node.left;
|
||||
|
||||
while (left.type === "LogicalExpression" && left.operator === node.operator) {
|
||||
|
||||
if (astUtils.isParenthesised(sourceCode, left)) {
|
||||
|
||||
/*
|
||||
* It should have associativity,
|
||||
* but ignore it if use parentheses to make the evaluation order clear.
|
||||
*/
|
||||
return left;
|
||||
}
|
||||
left = left.left;
|
||||
}
|
||||
return left;
|
||||
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require or disallow logical assignment operator shorthand",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/logical-assignment-operators"
|
||||
},
|
||||
|
||||
schema: {
|
||||
type: "array",
|
||||
oneOf: [{
|
||||
items: [
|
||||
{ const: "always" },
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
enforceForIfStatements: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
minItems: 0, // 0 for allowing passing no options
|
||||
maxItems: 2
|
||||
}, {
|
||||
items: [{ const: "never" }],
|
||||
minItems: 1,
|
||||
maxItems: 1
|
||||
}]
|
||||
},
|
||||
fixable: "code",
|
||||
hasSuggestions: true,
|
||||
messages: {
|
||||
assignment: "Assignment (=) can be replaced with operator assignment ({{operator}}).",
|
||||
useLogicalOperator: "Convert this assignment to use the operator {{ operator }}.",
|
||||
logical: "Logical expression can be replaced with an assignment ({{ operator }}).",
|
||||
convertLogical: "Replace this logical expression with an assignment with the operator {{ operator }}.",
|
||||
if: "'if' statement can be replaced with a logical operator assignment with operator {{ operator }}.",
|
||||
convertIf: "Replace this 'if' statement with a logical assignment with operator {{ operator }}.",
|
||||
unexpected: "Unexpected logical operator assignment ({{operator}}) shorthand.",
|
||||
separate: "Separate the logical assignment into an assignment with a logical operator."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const mode = context.options[0] === "never" ? "never" : "always";
|
||||
const checkIf = mode === "always" && context.options.length > 1 && context.options[1].enforceForIfStatements;
|
||||
const sourceCode = context.sourceCode;
|
||||
const isStrict = sourceCode.getScope(sourceCode.ast).isStrict;
|
||||
|
||||
/**
|
||||
* Returns false if the access could be a getter
|
||||
* @param {ASTNode} node Assignment expression
|
||||
* @returns {boolean} True iff the fix is safe
|
||||
*/
|
||||
function cannotBeGetter(node) {
|
||||
return node.type === "Identifier" &&
|
||||
(isStrict || !isInsideWithBlock(node));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether only a single property is accessed
|
||||
* @param {ASTNode} node reference
|
||||
* @returns {boolean} True iff a single property is accessed
|
||||
*/
|
||||
function accessesSingleProperty(node) {
|
||||
if (!isStrict && isInsideWithBlock(node)) {
|
||||
return node.type === "Identifier";
|
||||
}
|
||||
|
||||
return node.type === "MemberExpression" &&
|
||||
baseTypes.has(node.object.type) &&
|
||||
(!node.computed || (node.property.type !== "MemberExpression" && node.property.type !== "ChainExpression"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a fixer or suggestion whether on the fix is safe.
|
||||
* @param {{ messageId: string, node: ASTNode }} descriptor Report descriptor without fix or suggest
|
||||
* @param {{ messageId: string, fix: Function }} suggestion Adds the fix or the whole suggestion as only element in "suggest" to suggestion
|
||||
* @param {boolean} shouldBeFixed Fix iff the condition is true
|
||||
* @returns {Object} Descriptor with either an added fix or suggestion
|
||||
*/
|
||||
function createConditionalFixer(descriptor, suggestion, shouldBeFixed) {
|
||||
if (shouldBeFixed) {
|
||||
return {
|
||||
...descriptor,
|
||||
fix: suggestion.fix
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...descriptor,
|
||||
suggest: [suggestion]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the operator token for assignments and binary expressions
|
||||
* @param {ASTNode} node AssignmentExpression or BinaryExpression
|
||||
* @returns {import('eslint').AST.Token} Operator token between the left and right expression
|
||||
*/
|
||||
function getOperatorToken(node) {
|
||||
return sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
|
||||
}
|
||||
|
||||
if (mode === "never") {
|
||||
return {
|
||||
|
||||
// foo ||= bar
|
||||
"AssignmentExpression"(assignment) {
|
||||
if (!astUtils.isLogicalAssignmentOperator(assignment.operator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const descriptor = {
|
||||
messageId: "unexpected",
|
||||
node: assignment,
|
||||
data: { operator: assignment.operator }
|
||||
};
|
||||
const suggestion = {
|
||||
messageId: "separate",
|
||||
*fix(ruleFixer) {
|
||||
if (sourceCode.getCommentsInside(assignment).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const operatorToken = getOperatorToken(assignment);
|
||||
|
||||
// -> foo = bar
|
||||
yield ruleFixer.replaceText(operatorToken, "=");
|
||||
|
||||
const assignmentText = sourceCode.getText(assignment.left);
|
||||
const operator = assignment.operator.slice(0, -1);
|
||||
|
||||
// -> foo = foo || bar
|
||||
yield ruleFixer.insertTextAfter(operatorToken, ` ${assignmentText} ${operator}`);
|
||||
|
||||
const precedence = astUtils.getPrecedence(assignment.right) <= astUtils.getPrecedence({ type: "LogicalExpression", operator });
|
||||
|
||||
// ?? and || / && cannot be mixed but have same precedence
|
||||
const mixed = assignment.operator === "??=" && astUtils.isLogicalExpression(assignment.right);
|
||||
|
||||
if (!astUtils.isParenthesised(sourceCode, assignment.right) && (precedence || mixed)) {
|
||||
|
||||
// -> foo = foo || (bar)
|
||||
yield ruleFixer.insertTextBefore(assignment.right, "(");
|
||||
yield ruleFixer.insertTextAfter(assignment.right, ")");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
context.report(createConditionalFixer(descriptor, suggestion, cannotBeGetter(assignment.left)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
// foo = foo || bar
|
||||
"AssignmentExpression[operator='='][right.type='LogicalExpression']"(assignment) {
|
||||
const leftOperand = getLeftmostOperand(sourceCode, assignment.right);
|
||||
|
||||
if (!astUtils.isSameReference(assignment.left, leftOperand)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const descriptor = {
|
||||
messageId: "assignment",
|
||||
node: assignment,
|
||||
data: { operator: `${assignment.right.operator}=` }
|
||||
};
|
||||
const suggestion = {
|
||||
messageId: "useLogicalOperator",
|
||||
data: { operator: `${assignment.right.operator}=` },
|
||||
*fix(ruleFixer) {
|
||||
if (sourceCode.getCommentsInside(assignment).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No need for parenthesis around the assignment based on precedence as the precedence stays the same even with changed operator
|
||||
const assignmentOperatorToken = getOperatorToken(assignment);
|
||||
|
||||
// -> foo ||= foo || bar
|
||||
yield ruleFixer.insertTextBefore(assignmentOperatorToken, assignment.right.operator);
|
||||
|
||||
// -> foo ||= bar
|
||||
const logicalOperatorToken = getOperatorToken(leftOperand.parent);
|
||||
const firstRightOperandToken = sourceCode.getTokenAfter(logicalOperatorToken);
|
||||
|
||||
yield ruleFixer.removeRange([leftOperand.parent.range[0], firstRightOperandToken.range[0]]);
|
||||
}
|
||||
};
|
||||
|
||||
context.report(createConditionalFixer(descriptor, suggestion, cannotBeGetter(assignment.left)));
|
||||
},
|
||||
|
||||
// foo || (foo = bar)
|
||||
'LogicalExpression[right.type="AssignmentExpression"][right.operator="="]'(logical) {
|
||||
|
||||
// Right side has to be parenthesized, otherwise would be parsed as (foo || foo) = bar which is illegal
|
||||
if (isReference(logical.left) && astUtils.isSameReference(logical.left, logical.right.left)) {
|
||||
const descriptor = {
|
||||
messageId: "logical",
|
||||
node: logical,
|
||||
data: { operator: `${logical.operator}=` }
|
||||
};
|
||||
const suggestion = {
|
||||
messageId: "convertLogical",
|
||||
data: { operator: `${logical.operator}=` },
|
||||
*fix(ruleFixer) {
|
||||
if (sourceCode.getCommentsInside(logical).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentPrecedence = astUtils.getPrecedence(logical.parent);
|
||||
const requiresOuterParenthesis = logical.parent.type !== "ExpressionStatement" && (
|
||||
parentPrecedence === -1 ||
|
||||
astUtils.getPrecedence({ type: "AssignmentExpression" }) < parentPrecedence
|
||||
);
|
||||
|
||||
if (!astUtils.isParenthesised(sourceCode, logical) && requiresOuterParenthesis) {
|
||||
yield ruleFixer.insertTextBefore(logical, "(");
|
||||
yield ruleFixer.insertTextAfter(logical, ")");
|
||||
}
|
||||
|
||||
// Also removes all opening parenthesis
|
||||
yield ruleFixer.removeRange([logical.range[0], logical.right.range[0]]); // -> foo = bar)
|
||||
|
||||
// Also removes all ending parenthesis
|
||||
yield ruleFixer.removeRange([logical.right.range[1], logical.range[1]]); // -> foo = bar
|
||||
|
||||
const operatorToken = getOperatorToken(logical.right);
|
||||
|
||||
yield ruleFixer.insertTextBefore(operatorToken, logical.operator); // -> foo ||= bar
|
||||
}
|
||||
};
|
||||
const fix = cannotBeGetter(logical.left) || accessesSingleProperty(logical.left);
|
||||
|
||||
context.report(createConditionalFixer(descriptor, suggestion, fix));
|
||||
}
|
||||
},
|
||||
|
||||
// if (foo) foo = bar
|
||||
"IfStatement[alternate=null]"(ifNode) {
|
||||
if (!checkIf) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasBody = ifNode.consequent.type === "BlockStatement";
|
||||
|
||||
if (hasBody && ifNode.consequent.body.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = hasBody ? ifNode.consequent.body[0] : ifNode.consequent;
|
||||
const scope = sourceCode.getScope(ifNode);
|
||||
const existence = getExistence(ifNode.test, scope);
|
||||
|
||||
if (
|
||||
body.type === "ExpressionStatement" &&
|
||||
body.expression.type === "AssignmentExpression" &&
|
||||
body.expression.operator === "=" &&
|
||||
existence !== null &&
|
||||
astUtils.isSameReference(existence.reference, body.expression.left)
|
||||
) {
|
||||
const descriptor = {
|
||||
messageId: "if",
|
||||
node: ifNode,
|
||||
data: { operator: `${existence.operator}=` }
|
||||
};
|
||||
const suggestion = {
|
||||
messageId: "convertIf",
|
||||
data: { operator: `${existence.operator}=` },
|
||||
*fix(ruleFixer) {
|
||||
if (sourceCode.getCommentsInside(ifNode).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstBodyToken = sourceCode.getFirstToken(body);
|
||||
const prevToken = sourceCode.getTokenBefore(ifNode);
|
||||
|
||||
if (
|
||||
prevToken !== null &&
|
||||
prevToken.value !== ";" &&
|
||||
prevToken.value !== "{" &&
|
||||
firstBodyToken.type !== "Identifier" &&
|
||||
firstBodyToken.type !== "Keyword"
|
||||
) {
|
||||
|
||||
// Do not fix if the fixed statement could be part of the previous statement (eg. fn() if (a == null) (a) = b --> fn()(a) ??= b)
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const operatorToken = getOperatorToken(body.expression);
|
||||
|
||||
yield ruleFixer.insertTextBefore(operatorToken, existence.operator); // -> if (foo) foo ||= bar
|
||||
|
||||
yield ruleFixer.removeRange([ifNode.range[0], body.range[0]]); // -> foo ||= bar
|
||||
|
||||
yield ruleFixer.removeRange([body.range[1], ifNode.range[1]]); // -> foo ||= bar, only present if "if" had a body
|
||||
|
||||
const nextToken = sourceCode.getTokenAfter(body.expression);
|
||||
|
||||
if (hasBody && (nextToken !== null && nextToken.value !== ";")) {
|
||||
yield ruleFixer.insertTextAfter(ifNode, ";");
|
||||
}
|
||||
}
|
||||
};
|
||||
const shouldBeFixed = cannotBeGetter(existence.reference) ||
|
||||
(ifNode.test.type !== "LogicalExpression" && accessesSingleProperty(existence.reference));
|
||||
|
||||
context.report(createConditionalFixer(descriptor, suggestion, shouldBeFixed));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @fileoverview Enforce a maximum number of classes per file
|
||||
* @author James Garbutt <https://github.com/43081j>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce a maximum number of classes per file",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/max-classes-per-file"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 1
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
ignoreExpressions: {
|
||||
type: "boolean"
|
||||
},
|
||||
max: {
|
||||
type: "integer",
|
||||
minimum: 1
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
maximumExceeded: "File has too many classes ({{ classCount }}). Maximum allowed is {{ max }}."
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
const [option = {}] = context.options;
|
||||
const [ignoreExpressions, max] = typeof option === "number"
|
||||
? [false, option || 1]
|
||||
: [option.ignoreExpressions, option.max || 1];
|
||||
|
||||
let classCount = 0;
|
||||
|
||||
return {
|
||||
Program() {
|
||||
classCount = 0;
|
||||
},
|
||||
"Program:exit"(node) {
|
||||
if (classCount > max) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "maximumExceeded",
|
||||
data: {
|
||||
classCount,
|
||||
max
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
"ClassDeclaration"() {
|
||||
classCount++;
|
||||
},
|
||||
"ClassExpression"() {
|
||||
if (!ignoreExpressions) {
|
||||
classCount++;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* @fileoverview A rule to set the maximum depth block can be nested in a function.
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce a maximum depth that blocks can be nested",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/max-depth"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
maximum: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
max: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
tooDeeply: "Blocks are nested too deeply ({{depth}}). Maximum allowed is {{maxDepth}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
const functionStack = [],
|
||||
option = context.options[0];
|
||||
let maxDepth = 4;
|
||||
|
||||
if (
|
||||
typeof option === "object" &&
|
||||
(Object.prototype.hasOwnProperty.call(option, "maximum") || Object.prototype.hasOwnProperty.call(option, "max"))
|
||||
) {
|
||||
maxDepth = option.maximum || 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, messageId: "tooDeeply", data: { depth: len, maxDepth } });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop the saved block
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function popBlock() {
|
||||
functionStack[functionStack.length - 1]--;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program: startFunction,
|
||||
FunctionDeclaration: startFunction,
|
||||
FunctionExpression: startFunction,
|
||||
ArrowFunctionExpression: startFunction,
|
||||
StaticBlock: 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,
|
||||
"StaticBlock:exit": endFunction,
|
||||
"Program:exit": endFunction
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
/**
|
||||
* @fileoverview Rule to check for max length on a line.
|
||||
* @author Matt DuVall <http://www.mattduvall.com>
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce a maximum line length",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/max-len"
|
||||
},
|
||||
|
||||
schema: [
|
||||
OPTIONS_OR_INTEGER_SCHEMA,
|
||||
OPTIONS_OR_INTEGER_SCHEMA,
|
||||
OPTIONS_SCHEMA
|
||||
],
|
||||
messages: {
|
||||
max: "This line has a length of {{lineLength}}. Maximum allowed is {{maxLength}}.",
|
||||
maxComment: "This line has a comment length of {{lineLength}}. Maximum allowed is {{maxCommentLength}}."
|
||||
}
|
||||
},
|
||||
|
||||
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 = /[^:/?#]:\/\/[^?#]/u;
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* 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/gu, (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 options = Object.assign({}, context.options[context.options.length - 1]);
|
||||
|
||||
// …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 = typeof options.code === "number" ? options.code : 80,
|
||||
tabWidth = typeof options.tabWidth === "number" ? options.tabWidth : 4,
|
||||
ignoreComments = !!options.ignoreComments,
|
||||
ignoreStrings = !!options.ignoreStrings,
|
||||
ignoreTemplateLiterals = !!options.ignoreTemplateLiterals,
|
||||
ignoreRegExpLiterals = !!options.ignoreRegExpLiterals,
|
||||
ignoreTrailingComments = !!options.ignoreTrailingComments || !!options.ignoreComments,
|
||||
ignoreUrls = !!options.ignoreUrls,
|
||||
maxCommentLength = options.comments;
|
||||
let ignorePattern = options.ignorePattern || null;
|
||||
|
||||
if (ignorePattern) {
|
||||
ignorePattern = new RegExp(ignorePattern, "u");
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node is a JSXEmptyExpression contained in a single line JSXExpressionContainer.
|
||||
* @param {ASTNode} node A node to check.
|
||||
* @returns {boolean} True if the node is a JSXEmptyExpression contained in a single line JSXExpressionContainer.
|
||||
*/
|
||||
function isJSXEmptyExpressionInSingleLineContainer(node) {
|
||||
if (!node || !node.parent || node.type !== "JSXEmptyExpression" || node.parent.type !== "JSXExpressionContainer") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parent = node.parent;
|
||||
|
||||
return parent.loc.start.line === parent.loc.end.line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the line after the comment and any remaining trailing whitespace is
|
||||
* stripped.
|
||||
* @param {string} line The source line with a trailing comment
|
||||
* @param {ASTNode} comment The comment to remove
|
||||
* @returns {string} Line without comment and trailing whitespace
|
||||
*/
|
||||
function stripTrailingComment(line, comment) {
|
||||
|
||||
// loc.column is zero-indexed
|
||||
return line.slice(0, comment.loc.start.column).replace(/\s+$/u, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {any} 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" ||
|
||||
(token.type === "JSXText" && sourceCode.getNodeByRangeIndex(token.range[0] - 1).type === "JSXAttribute")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* reduce an array of AST nodes by line number, both start and end.
|
||||
* @param {ASTNode[]} arr array of AST nodes
|
||||
* @returns {Object} accululated AST nodes
|
||||
*/
|
||||
function groupArrayByLineNumber(arr) {
|
||||
const obj = {};
|
||||
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const node = arr[i];
|
||||
|
||||
for (let j = node.loc.start.line; j <= node.loc.end.line; ++j) {
|
||||
ensureArrayAndPush(obj, j, node);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all comments in the source code.
|
||||
* If the element in the array is a JSXEmptyExpression contained with a single line JSXExpressionContainer,
|
||||
* the element is changed with JSXExpressionContainer node.
|
||||
* @returns {ASTNode[]} An array of comment nodes
|
||||
*/
|
||||
function getAllComments() {
|
||||
const comments = [];
|
||||
|
||||
sourceCode.getAllComments()
|
||||
.forEach(commentNode => {
|
||||
const containingNode = sourceCode.getNodeByRangeIndex(commentNode.range[0]);
|
||||
|
||||
if (isJSXEmptyExpressionInSingleLineContainer(containingNode)) {
|
||||
|
||||
// push a unique node only
|
||||
if (comments[comments.length - 1] !== containingNode.parent) {
|
||||
comments.push(containingNode.parent);
|
||||
}
|
||||
} else {
|
||||
comments.push(commentNode);
|
||||
}
|
||||
});
|
||||
|
||||
return comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ? getAllComments() : [];
|
||||
|
||||
// we iterate over comments in parallel with the lines
|
||||
let commentsIndex = 0;
|
||||
|
||||
const strings = getAllStrings();
|
||||
const stringsByLine = groupArrayByLineNumber(strings);
|
||||
|
||||
const templateLiterals = getAllTemplateLiterals();
|
||||
const templateLiteralsByLine = groupArrayByLineNumber(templateLiterals);
|
||||
|
||||
const regExpLiterals = getAllRegExpLiterals();
|
||||
const regExpLiteralsByLine = groupArrayByLineNumber(regExpLiterals);
|
||||
|
||||
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;
|
||||
let textToMeasure;
|
||||
|
||||
/*
|
||||
* 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;
|
||||
textToMeasure = line;
|
||||
} else if (ignoreTrailingComments && isTrailingComment(line, lineNumber, comment)) {
|
||||
textToMeasure = stripTrailingComment(line, comment);
|
||||
|
||||
// ignore multiple trailing comments in the same line
|
||||
let lastIndex = commentsIndex;
|
||||
|
||||
while (isTrailingComment(textToMeasure, lineNumber, comments[--lastIndex])) {
|
||||
textToMeasure = stripTrailingComment(textToMeasure, comments[lastIndex]);
|
||||
}
|
||||
} else {
|
||||
textToMeasure = line;
|
||||
}
|
||||
} else {
|
||||
textToMeasure = line;
|
||||
}
|
||||
if (ignorePattern && ignorePattern.test(textToMeasure) ||
|
||||
ignoreUrls && URL_REGEXP.test(textToMeasure) ||
|
||||
ignoreStrings && stringsByLine[lineNumber] ||
|
||||
ignoreTemplateLiterals && templateLiteralsByLine[lineNumber] ||
|
||||
ignoreRegExpLiterals && regExpLiteralsByLine[lineNumber]
|
||||
) {
|
||||
|
||||
// ignore this line
|
||||
return;
|
||||
}
|
||||
|
||||
const lineLength = computeLineLength(textToMeasure, tabWidth);
|
||||
const commentLengthApplies = lineIsComment && maxCommentLength;
|
||||
|
||||
if (lineIsComment && ignoreComments) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loc = {
|
||||
start: {
|
||||
line: lineNumber,
|
||||
column: 0
|
||||
},
|
||||
end: {
|
||||
line: lineNumber,
|
||||
column: textToMeasure.length
|
||||
}
|
||||
};
|
||||
|
||||
if (commentLengthApplies) {
|
||||
if (lineLength > maxCommentLength) {
|
||||
context.report({
|
||||
node,
|
||||
loc,
|
||||
messageId: "maxComment",
|
||||
data: {
|
||||
lineLength,
|
||||
maxCommentLength
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (lineLength > maxLength) {
|
||||
context.report({
|
||||
node,
|
||||
loc,
|
||||
messageId: "max",
|
||||
data: {
|
||||
lineLength,
|
||||
maxLength
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program: checkProgramForMaxLength
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* @fileoverview A rule to set the maximum number of line of code in a function.
|
||||
* @author Pete Ward <peteward44@gmail.com>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
const { upperCaseFirst } = require("../shared/string-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const OPTIONS_SCHEMA = {
|
||||
type: "object",
|
||||
properties: {
|
||||
max: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
skipComments: {
|
||||
type: "boolean"
|
||||
},
|
||||
skipBlankLines: {
|
||||
type: "boolean"
|
||||
},
|
||||
IIFEs: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
};
|
||||
|
||||
const OPTIONS_OR_INTEGER_SCHEMA = {
|
||||
oneOf: [
|
||||
OPTIONS_SCHEMA,
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 1
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a list of comment nodes, return a map with numeric keys (source code line numbers) and comment token values.
|
||||
* @param {Array} comments An array of comment nodes.
|
||||
* @returns {Map<string, Node>} A map with numeric keys (source code line numbers) and comment token values.
|
||||
*/
|
||||
function getCommentLineNumbers(comments) {
|
||||
const map = new Map();
|
||||
|
||||
comments.forEach(comment => {
|
||||
for (let i = comment.loc.start.line; i <= comment.loc.end.line; i++) {
|
||||
map.set(i, comment);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce a maximum number of lines of code in a function",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/max-lines-per-function"
|
||||
},
|
||||
|
||||
schema: [
|
||||
OPTIONS_OR_INTEGER_SCHEMA
|
||||
],
|
||||
messages: {
|
||||
exceed: "{{name}} has too many lines ({{lineCount}}). Maximum allowed is {{maxLines}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const lines = sourceCode.lines;
|
||||
|
||||
const option = context.options[0];
|
||||
let maxLines = 50;
|
||||
let skipComments = false;
|
||||
let skipBlankLines = false;
|
||||
let IIFEs = false;
|
||||
|
||||
if (typeof option === "object") {
|
||||
maxLines = typeof option.max === "number" ? option.max : 50;
|
||||
skipComments = !!option.skipComments;
|
||||
skipBlankLines = !!option.skipBlankLines;
|
||||
IIFEs = !!option.IIFEs;
|
||||
} else if (typeof option === "number") {
|
||||
maxLines = option;
|
||||
}
|
||||
|
||||
const commentLineNumbers = getCommentLineNumbers(sourceCode.getAllComments());
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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 = start.line === lineNumber && !line.slice(0, start.column).trim(),
|
||||
isLastTokenOnLine = end.line === lineNumber && !line.slice(end.column).trim();
|
||||
|
||||
return comment &&
|
||||
(start.line < lineNumber || isFirstTokenOnLine) &&
|
||||
(end.line > lineNumber || isLastTokenOnLine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies is a node is a FunctionExpression which is part of an IIFE
|
||||
* @param {ASTNode} node Node to test
|
||||
* @returns {boolean} True if it's an IIFE
|
||||
*/
|
||||
function isIIFE(node) {
|
||||
return (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && node.parent && node.parent.type === "CallExpression" && node.parent.callee === node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies is a node is a FunctionExpression which is embedded within a MethodDefinition or Property
|
||||
* @param {ASTNode} node Node to test
|
||||
* @returns {boolean} True if it's a FunctionExpression embedded within a MethodDefinition or Property
|
||||
*/
|
||||
function isEmbedded(node) {
|
||||
if (!node.parent) {
|
||||
return false;
|
||||
}
|
||||
if (node !== node.parent.value) {
|
||||
return false;
|
||||
}
|
||||
if (node.parent.type === "MethodDefinition") {
|
||||
return true;
|
||||
}
|
||||
if (node.parent.type === "Property") {
|
||||
return node.parent.method === true || node.parent.kind === "get" || node.parent.kind === "set";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the lines in the function
|
||||
* @param {ASTNode} funcNode Function AST node
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function processFunction(funcNode) {
|
||||
const node = isEmbedded(funcNode) ? funcNode.parent : funcNode;
|
||||
|
||||
if (!IIFEs && isIIFE(node)) {
|
||||
return;
|
||||
}
|
||||
let lineCount = 0;
|
||||
|
||||
for (let i = node.loc.start.line - 1; i < node.loc.end.line; ++i) {
|
||||
const line = lines[i];
|
||||
|
||||
if (skipComments) {
|
||||
if (commentLineNumbers.has(i + 1) && isFullLineComment(line, i + 1, commentLineNumbers.get(i + 1))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (skipBlankLines) {
|
||||
if (line.match(/^\s*$/u)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
lineCount++;
|
||||
}
|
||||
|
||||
if (lineCount > maxLines) {
|
||||
const name = upperCaseFirst(astUtils.getFunctionNameWithKind(funcNode));
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "exceed",
|
||||
data: { name, lineCount, maxLines }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
FunctionDeclaration: processFunction,
|
||||
FunctionExpression: processFunction,
|
||||
ArrowFunctionExpression: processFunction
|
||||
};
|
||||
}
|
||||
};
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* @fileoverview enforce a maximum file length
|
||||
* @author Alberto Rodríguez
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates an array of numbers from `start` up to, but not including, `end`
|
||||
* @param {number} start The start of the range
|
||||
* @param {number} end The end of the range
|
||||
* @returns {number[]} The range of numbers
|
||||
*/
|
||||
function range(start, end) {
|
||||
return [...Array(end - start).keys()].map(x => x + start);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce a maximum number of lines per file",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/max-lines"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
max: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
skipComments: {
|
||||
type: "boolean"
|
||||
},
|
||||
skipBlankLines: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
exceed:
|
||||
"File has too many lines ({{actual}}). Maximum allowed is {{max}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const option = context.options[0];
|
||||
let max = 300;
|
||||
|
||||
if (
|
||||
typeof option === "object" &&
|
||||
Object.prototype.hasOwnProperty.call(option, "max")
|
||||
) {
|
||||
max = option.max;
|
||||
} else if (typeof option === "number") {
|
||||
max = option;
|
||||
}
|
||||
|
||||
const skipComments = option && option.skipComments;
|
||||
const skipBlankLines = option && option.skipBlankLines;
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* 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 {number[]} 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 range(start, end + 1);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
return {
|
||||
"Program:exit"() {
|
||||
let lines = sourceCode.lines.map((text, i) => ({
|
||||
lineNumber: i + 1,
|
||||
text
|
||||
}));
|
||||
|
||||
/*
|
||||
* If file ends with a linebreak, `sourceCode.lines` will have one extra empty line at the end.
|
||||
* That isn't a real line, so we shouldn't count it.
|
||||
*/
|
||||
if (lines.length > 1 && lines[lines.length - 1].text === "") {
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
if (skipBlankLines) {
|
||||
lines = lines.filter(l => l.text.trim() !== "");
|
||||
}
|
||||
|
||||
if (skipComments) {
|
||||
const comments = sourceCode.getAllComments();
|
||||
|
||||
const commentLines = new Set(comments.flatMap(getLinesWithoutCode));
|
||||
|
||||
lines = lines.filter(
|
||||
l => !commentLines.has(l.lineNumber)
|
||||
);
|
||||
}
|
||||
|
||||
if (lines.length > max) {
|
||||
const loc = {
|
||||
start: {
|
||||
line: lines[max].lineNumber,
|
||||
column: 0
|
||||
},
|
||||
end: {
|
||||
line: sourceCode.lines.length,
|
||||
column: sourceCode.lines[sourceCode.lines.length - 1].length
|
||||
}
|
||||
};
|
||||
|
||||
context.report({
|
||||
loc,
|
||||
messageId: "exceed",
|
||||
data: {
|
||||
max,
|
||||
actual: lines.length
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce a maximum number of nested callbacks.
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce a maximum depth that callbacks can be nested",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/max-nested-callbacks"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
maximum: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
max: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
exceed: "Too many nested callbacks ({{num}}). Maximum allowed is {{max}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Constants
|
||||
//--------------------------------------------------------------------------
|
||||
const option = context.options[0];
|
||||
let THRESHOLD = 10;
|
||||
|
||||
if (
|
||||
typeof option === "object" &&
|
||||
(Object.prototype.hasOwnProperty.call(option, "maximum") || Object.prototype.hasOwnProperty.call(option, "max"))
|
||||
) {
|
||||
THRESHOLD = option.maximum || option.max;
|
||||
} else 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, messageId: "exceed", 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
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag when a function has too many parameters
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
const { upperCaseFirst } = require("../shared/string-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce a maximum number of parameters in function definitions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/max-params"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
maximum: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
max: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
exceed: "{{name}} has too many parameters ({{count}}). Maximum allowed is {{max}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const option = context.options[0];
|
||||
let numParams = 3;
|
||||
|
||||
if (
|
||||
typeof option === "object" &&
|
||||
(Object.prototype.hasOwnProperty.call(option, "maximum") || Object.prototype.hasOwnProperty.call(option, "max"))
|
||||
) {
|
||||
numParams = option.maximum || 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({
|
||||
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
|
||||
node,
|
||||
messageId: "exceed",
|
||||
data: {
|
||||
name: upperCaseFirst(astUtils.getFunctionNameWithKind(node)),
|
||||
count: node.params.length,
|
||||
max: numParams
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
FunctionDeclaration: checkFunction,
|
||||
ArrowFunctionExpression: checkFunction,
|
||||
FunctionExpression: checkFunction
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* @fileoverview Specify the maximum number of statements allowed per line.
|
||||
* @author Kenneth Williams
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce a maximum number of statements allowed per line",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/max-statements-per-line"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
max: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
exceed: "This line has {{numberOfStatementsOnThisLine}} {{statements}}. Maximum allowed is {{maxStatementsPerLine}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode,
|
||||
options = context.options[0] || {},
|
||||
maxStatementsPerLine = typeof options.max !== "undefined" ? options.max : 1;
|
||||
|
||||
let lastStatementLine = 0,
|
||||
numberOfStatementsOnThisLine = 0,
|
||||
firstExtraStatement;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
const SINGLE_CHILD_ALLOWED = /^(?:(?:DoWhile|For|ForIn|ForOf|If|Labeled|While)Statement|Export(?:Default|Named)Declaration)$/u;
|
||||
|
||||
/**
|
||||
* Reports with the first extra statement, and clears it.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportFirstExtraStatementAndClear() {
|
||||
if (firstExtraStatement) {
|
||||
context.report({
|
||||
node: firstExtraStatement,
|
||||
messageId: "exceed",
|
||||
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
|
||||
};
|
||||
}
|
||||
};
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* @fileoverview A rule to set the maximum number of statements in a function.
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
const { upperCaseFirst } = require("../shared/string-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce a maximum number of statements allowed in function blocks",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/max-statements"
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
exceed: "{{name}} has too many statements ({{count}}). Maximum allowed is {{max}}."
|
||||
}
|
||||
},
|
||||
|
||||
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" &&
|
||||
(Object.prototype.hasOwnProperty.call(option, "maximum") || Object.prototype.hasOwnProperty.call(option, "max"))
|
||||
) {
|
||||
maxStatements = option.maximum || option.max;
|
||||
} else 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 = upperCaseFirst(astUtils.getFunctionNameWithKind(node));
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "exceed",
|
||||
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();
|
||||
|
||||
/*
|
||||
* This rule does not apply to class static blocks, but we have to track them so
|
||||
* that statements in them do not count as statements in the enclosing function.
|
||||
*/
|
||||
if (node.type === "StaticBlock") {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
StaticBlock: startFunction,
|
||||
|
||||
BlockStatement: countStatements,
|
||||
|
||||
"FunctionDeclaration:exit": endFunction,
|
||||
"FunctionExpression:exit": endFunction,
|
||||
"ArrowFunctionExpression:exit": endFunction,
|
||||
"StaticBlock:exit": endFunction,
|
||||
|
||||
"Program:exit"() {
|
||||
if (topLevelFunctions.length === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
topLevelFunctions.forEach(element => {
|
||||
const count = element.count;
|
||||
const node = element.node;
|
||||
|
||||
reportIfTooManyStatements(node, count, maxStatements);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+474
@@ -0,0 +1,474 @@
|
||||
/**
|
||||
* @fileoverview enforce a particular style for multiline comments
|
||||
* @author Teddy Katz
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Enforce a particular style for multiline comments",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/multiline-comment-style"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
schema: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["starred-block", "bare-block"]
|
||||
}
|
||||
],
|
||||
additionalItems: false
|
||||
},
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["separate-lines"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
checkJSDoc: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
additionalItems: false
|
||||
}
|
||||
]
|
||||
},
|
||||
messages: {
|
||||
expectedBlock: "Expected a block comment instead of consecutive line comments.",
|
||||
expectedBareBlock: "Expected a block comment without padding stars.",
|
||||
startNewline: "Expected a linebreak after '/*'.",
|
||||
endNewline: "Expected a linebreak before '*/'.",
|
||||
missingStar: "Expected a '*' at the start of this line.",
|
||||
alignment: "Expected this line to be aligned with the start of the comment.",
|
||||
expectedLines: "Expected multiple line comments instead of a block comment."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const option = context.options[0] || "starred-block";
|
||||
const params = context.options[1] || {};
|
||||
const checkJSDoc = !!params.checkJSDoc;
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Helpers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks if a comment line is starred.
|
||||
* @param {string} line A string representing a comment line.
|
||||
* @returns {boolean} Whether or not the comment line is starred.
|
||||
*/
|
||||
function isStarredCommentLine(line) {
|
||||
return /^\s*\*/u.test(line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a comment group is in starred-block form.
|
||||
* @param {Token[]} commentGroup A group of comments, containing either multiple line comments or a single block comment.
|
||||
* @returns {boolean} Whether or not the comment group is in starred block form.
|
||||
*/
|
||||
function isStarredBlockComment([firstComment]) {
|
||||
if (firstComment.type !== "Block") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lines = firstComment.value.split(astUtils.LINEBREAK_MATCHER);
|
||||
|
||||
// The first and last lines can only contain whitespace.
|
||||
return lines.length > 0 && lines.every((line, i) => (i === 0 || i === lines.length - 1 ? /^\s*$/u : /^\s*\*/u).test(line));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a comment group is in JSDoc form.
|
||||
* @param {Token[]} commentGroup A group of comments, containing either multiple line comments or a single block comment.
|
||||
* @returns {boolean} Whether or not the comment group is in JSDoc form.
|
||||
*/
|
||||
function isJSDocComment([firstComment]) {
|
||||
if (firstComment.type !== "Block") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lines = firstComment.value.split(astUtils.LINEBREAK_MATCHER);
|
||||
|
||||
return /^\*\s*$/u.test(lines[0]) &&
|
||||
lines.slice(1, -1).every(line => /^\s* /u.test(line)) &&
|
||||
/^\s*$/u.test(lines[lines.length - 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a comment group that is currently in separate-line form, calculating the offset for each line.
|
||||
* @param {Token[]} commentGroup A group of comments containing multiple line comments.
|
||||
* @returns {string[]} An array of the processed lines.
|
||||
*/
|
||||
function processSeparateLineComments(commentGroup) {
|
||||
const allLinesHaveLeadingSpace = commentGroup
|
||||
.map(({ value }) => value)
|
||||
.filter(line => line.trim().length)
|
||||
.every(line => line.startsWith(" "));
|
||||
|
||||
return commentGroup.map(({ value }) => (allLinesHaveLeadingSpace ? value.replace(/^ /u, "") : value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a comment group that is currently in starred-block form, calculating the offset for each line.
|
||||
* @param {Token} comment A single block comment token in starred-block form.
|
||||
* @returns {string[]} An array of the processed lines.
|
||||
*/
|
||||
function processStarredBlockComment(comment) {
|
||||
const lines = comment.value.split(astUtils.LINEBREAK_MATCHER)
|
||||
.filter((line, i, linesArr) => !(i === 0 || i === linesArr.length - 1))
|
||||
.map(line => line.replace(/^\s*$/u, ""));
|
||||
const allLinesHaveLeadingSpace = lines
|
||||
.map(line => line.replace(/\s*\*/u, ""))
|
||||
.filter(line => line.trim().length)
|
||||
.every(line => line.startsWith(" "));
|
||||
|
||||
return lines.map(line => line.replace(allLinesHaveLeadingSpace ? /\s*\* ?/u : /\s*\*/u, ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a comment group that is currently in bare-block form, calculating the offset for each line.
|
||||
* @param {Token} comment A single block comment token in bare-block form.
|
||||
* @returns {string[]} An array of the processed lines.
|
||||
*/
|
||||
function processBareBlockComment(comment) {
|
||||
const lines = comment.value.split(astUtils.LINEBREAK_MATCHER).map(line => line.replace(/^\s*$/u, ""));
|
||||
const leadingWhitespace = `${sourceCode.text.slice(comment.range[0] - comment.loc.start.column, comment.range[0])} `;
|
||||
let offset = "";
|
||||
|
||||
/*
|
||||
* Calculate the offset of the least indented line and use that as the basis for offsetting all the lines.
|
||||
* The first line should not be checked because it is inline with the opening block comment delimiter.
|
||||
*/
|
||||
for (const [i, line] of lines.entries()) {
|
||||
if (!line.trim().length || i === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [, lineOffset] = line.match(/^(\s*\*?\s*)/u);
|
||||
|
||||
if (lineOffset.length < leadingWhitespace.length) {
|
||||
const newOffset = leadingWhitespace.slice(lineOffset.length - leadingWhitespace.length);
|
||||
|
||||
if (newOffset.length > offset.length) {
|
||||
offset = newOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.map(line => {
|
||||
const match = line.match(/^(\s*\*?\s*)(.*)/u);
|
||||
const [, lineOffset, lineContents] = match;
|
||||
|
||||
if (lineOffset.length > leadingWhitespace.length) {
|
||||
return `${lineOffset.slice(leadingWhitespace.length - (offset.length + lineOffset.length))}${lineContents}`;
|
||||
}
|
||||
|
||||
if (lineOffset.length < leadingWhitespace.length) {
|
||||
return `${lineOffset.slice(leadingWhitespace.length)}${lineContents}`;
|
||||
}
|
||||
|
||||
return lineContents;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of comment lines in a group, formatting leading whitespace as necessary.
|
||||
* @param {Token[]} commentGroup A group of comments containing either multiple line comments or a single block comment.
|
||||
* @returns {string[]} A list of comment lines.
|
||||
*/
|
||||
function getCommentLines(commentGroup) {
|
||||
const [firstComment] = commentGroup;
|
||||
|
||||
if (firstComment.type === "Line") {
|
||||
return processSeparateLineComments(commentGroup);
|
||||
}
|
||||
|
||||
if (isStarredBlockComment(commentGroup)) {
|
||||
return processStarredBlockComment(firstComment);
|
||||
}
|
||||
|
||||
return processBareBlockComment(firstComment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the initial offset (whitespace) from the beginning of a line to a given comment token.
|
||||
* @param {Token} comment The token to check.
|
||||
* @returns {string} The offset from the beginning of a line to the token.
|
||||
*/
|
||||
function getInitialOffset(comment) {
|
||||
return sourceCode.text.slice(comment.range[0] - comment.loc.start.column, comment.range[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a comment into starred-block form
|
||||
* @param {Token} firstComment The first comment of the group being converted
|
||||
* @param {string[]} commentLinesList A list of lines to appear in the new starred-block comment
|
||||
* @returns {string} A representation of the comment value in starred-block form, excluding start and end markers
|
||||
*/
|
||||
function convertToStarredBlock(firstComment, commentLinesList) {
|
||||
const initialOffset = getInitialOffset(firstComment);
|
||||
|
||||
return `/*\n${commentLinesList.map(line => `${initialOffset} * ${line}`).join("\n")}\n${initialOffset} */`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a comment into separate-line form
|
||||
* @param {Token} firstComment The first comment of the group being converted
|
||||
* @param {string[]} commentLinesList A list of lines to appear in the new starred-block comment
|
||||
* @returns {string} A representation of the comment value in separate-line form
|
||||
*/
|
||||
function convertToSeparateLines(firstComment, commentLinesList) {
|
||||
return commentLinesList.map(line => `// ${line}`).join(`\n${getInitialOffset(firstComment)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a comment into bare-block form
|
||||
* @param {Token} firstComment The first comment of the group being converted
|
||||
* @param {string[]} commentLinesList A list of lines to appear in the new starred-block comment
|
||||
* @returns {string} A representation of the comment value in bare-block form
|
||||
*/
|
||||
function convertToBlock(firstComment, commentLinesList) {
|
||||
return `/* ${commentLinesList.join(`\n${getInitialOffset(firstComment)} `)} */`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Each method checks a group of comments to see if it's valid according to the given option.
|
||||
* @param {Token[]} commentGroup A list of comments that appear together. This will either contain a single
|
||||
* block comment or multiple line comments.
|
||||
* @returns {void}
|
||||
*/
|
||||
const commentGroupCheckers = {
|
||||
"starred-block"(commentGroup) {
|
||||
const [firstComment] = commentGroup;
|
||||
const commentLines = getCommentLines(commentGroup);
|
||||
|
||||
if (commentLines.some(value => value.includes("*/"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (commentGroup.length > 1) {
|
||||
context.report({
|
||||
loc: {
|
||||
start: firstComment.loc.start,
|
||||
end: commentGroup[commentGroup.length - 1].loc.end
|
||||
},
|
||||
messageId: "expectedBlock",
|
||||
fix(fixer) {
|
||||
const range = [firstComment.range[0], commentGroup[commentGroup.length - 1].range[1]];
|
||||
|
||||
return commentLines.some(value => value.startsWith("/"))
|
||||
? null
|
||||
: fixer.replaceTextRange(range, convertToStarredBlock(firstComment, commentLines));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const lines = firstComment.value.split(astUtils.LINEBREAK_MATCHER);
|
||||
const expectedLeadingWhitespace = getInitialOffset(firstComment);
|
||||
const expectedLinePrefix = `${expectedLeadingWhitespace} *`;
|
||||
|
||||
if (!/^\*?\s*$/u.test(lines[0])) {
|
||||
const start = firstComment.value.startsWith("*") ? firstComment.range[0] + 1 : firstComment.range[0];
|
||||
|
||||
context.report({
|
||||
loc: {
|
||||
start: firstComment.loc.start,
|
||||
end: { line: firstComment.loc.start.line, column: firstComment.loc.start.column + 2 }
|
||||
},
|
||||
messageId: "startNewline",
|
||||
fix: fixer => fixer.insertTextAfterRange([start, start + 2], `\n${expectedLinePrefix}`)
|
||||
});
|
||||
}
|
||||
|
||||
if (!/^\s*$/u.test(lines[lines.length - 1])) {
|
||||
context.report({
|
||||
loc: {
|
||||
start: { line: firstComment.loc.end.line, column: firstComment.loc.end.column - 2 },
|
||||
end: firstComment.loc.end
|
||||
},
|
||||
messageId: "endNewline",
|
||||
fix: fixer => fixer.replaceTextRange([firstComment.range[1] - 2, firstComment.range[1]], `\n${expectedLinePrefix}/`)
|
||||
});
|
||||
}
|
||||
|
||||
for (let lineNumber = firstComment.loc.start.line + 1; lineNumber <= firstComment.loc.end.line; lineNumber++) {
|
||||
const lineText = sourceCode.lines[lineNumber - 1];
|
||||
const errorType = isStarredCommentLine(lineText)
|
||||
? "alignment"
|
||||
: "missingStar";
|
||||
|
||||
if (!lineText.startsWith(expectedLinePrefix)) {
|
||||
context.report({
|
||||
loc: {
|
||||
start: { line: lineNumber, column: 0 },
|
||||
end: { line: lineNumber, column: lineText.length }
|
||||
},
|
||||
messageId: errorType,
|
||||
fix(fixer) {
|
||||
const lineStartIndex = sourceCode.getIndexFromLoc({ line: lineNumber, column: 0 });
|
||||
|
||||
if (errorType === "alignment") {
|
||||
const [, commentTextPrefix = ""] = lineText.match(/^(\s*\*)/u) || [];
|
||||
const commentTextStartIndex = lineStartIndex + commentTextPrefix.length;
|
||||
|
||||
return fixer.replaceTextRange([lineStartIndex, commentTextStartIndex], expectedLinePrefix);
|
||||
}
|
||||
|
||||
const [, commentTextPrefix = ""] = lineText.match(/^(\s*)/u) || [];
|
||||
const commentTextStartIndex = lineStartIndex + commentTextPrefix.length;
|
||||
let offset;
|
||||
|
||||
for (const [idx, line] of lines.entries()) {
|
||||
if (!/\S+/u.test(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lineTextToAlignWith = sourceCode.lines[firstComment.loc.start.line - 1 + idx];
|
||||
const [, prefix = "", initialOffset = ""] = lineTextToAlignWith.match(/^(\s*(?:\/?\*)?(\s*))/u) || [];
|
||||
|
||||
offset = `${commentTextPrefix.slice(prefix.length)}${initialOffset}`;
|
||||
|
||||
if (/^\s*\//u.test(lineText) && offset.length === 0) {
|
||||
offset += " ";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return fixer.replaceTextRange([lineStartIndex, commentTextStartIndex], `${expectedLinePrefix}${offset}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"separate-lines"(commentGroup) {
|
||||
const [firstComment] = commentGroup;
|
||||
|
||||
const isJSDoc = isJSDocComment(commentGroup);
|
||||
|
||||
if (firstComment.type !== "Block" || (!checkJSDoc && isJSDoc)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let commentLines = getCommentLines(commentGroup);
|
||||
|
||||
if (isJSDoc) {
|
||||
commentLines = commentLines.slice(1, commentLines.length - 1);
|
||||
}
|
||||
|
||||
const tokenAfter = sourceCode.getTokenAfter(firstComment, { includeComments: true });
|
||||
|
||||
if (tokenAfter && firstComment.loc.end.line === tokenAfter.loc.start.line) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
loc: {
|
||||
start: firstComment.loc.start,
|
||||
end: { line: firstComment.loc.start.line, column: firstComment.loc.start.column + 2 }
|
||||
},
|
||||
messageId: "expectedLines",
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(firstComment, convertToSeparateLines(firstComment, commentLines));
|
||||
}
|
||||
});
|
||||
},
|
||||
"bare-block"(commentGroup) {
|
||||
if (isJSDocComment(commentGroup)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [firstComment] = commentGroup;
|
||||
const commentLines = getCommentLines(commentGroup);
|
||||
|
||||
// Disallows consecutive line comments in favor of using a block comment.
|
||||
if (firstComment.type === "Line" && commentLines.length > 1 &&
|
||||
!commentLines.some(value => value.includes("*/"))) {
|
||||
context.report({
|
||||
loc: {
|
||||
start: firstComment.loc.start,
|
||||
end: commentGroup[commentGroup.length - 1].loc.end
|
||||
},
|
||||
messageId: "expectedBlock",
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange(
|
||||
[firstComment.range[0], commentGroup[commentGroup.length - 1].range[1]],
|
||||
convertToBlock(firstComment, commentLines)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Prohibits block comments from having a * at the beginning of each line.
|
||||
if (isStarredBlockComment(commentGroup)) {
|
||||
context.report({
|
||||
loc: {
|
||||
start: firstComment.loc.start,
|
||||
end: { line: firstComment.loc.start.line, column: firstComment.loc.start.column + 2 }
|
||||
},
|
||||
messageId: "expectedBareBlock",
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(firstComment, convertToBlock(firstComment, commentLines));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program() {
|
||||
return sourceCode.getAllComments()
|
||||
.filter(comment => comment.type !== "Shebang")
|
||||
.filter(comment => !astUtils.COMMENTS_IGNORE_PATTERN.test(comment.value))
|
||||
.filter(comment => {
|
||||
const tokenBefore = sourceCode.getTokenBefore(comment, { includeComments: true });
|
||||
|
||||
return !tokenBefore || tokenBefore.loc.end.line < comment.loc.start.line;
|
||||
})
|
||||
.reduce((commentGroups, comment, index, commentList) => {
|
||||
const tokenBefore = sourceCode.getTokenBefore(comment, { includeComments: true });
|
||||
|
||||
if (
|
||||
comment.type === "Line" &&
|
||||
index && commentList[index - 1].type === "Line" &&
|
||||
tokenBefore && tokenBefore.loc.end.line === comment.loc.start.line - 1 &&
|
||||
tokenBefore === commentList[index - 1]
|
||||
) {
|
||||
commentGroups[commentGroups.length - 1].push(comment);
|
||||
} else {
|
||||
commentGroups.push([comment]);
|
||||
}
|
||||
|
||||
return commentGroups;
|
||||
}, [])
|
||||
.filter(commentGroup => !(commentGroup.length === 1 && commentGroup[0].loc.start.line === commentGroup[0].loc.end.line))
|
||||
.forEach(commentGroupCheckers[option]);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @fileoverview Enforce newlines between operands of ternary expressions
|
||||
* @author Kai Cataldo
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce newlines between operands of ternary expressions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/multiline-ternary"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "always-multiline", "never"]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
expectedTestCons: "Expected newline between test and consequent of ternary expression.",
|
||||
expectedConsAlt: "Expected newline between consequent and alternate of ternary expression.",
|
||||
unexpectedTestCons: "Unexpected newline between test and consequent of ternary expression.",
|
||||
unexpectedConsAlt: "Unexpected newline between consequent and alternate of ternary expression."
|
||||
},
|
||||
|
||||
fixable: "whitespace"
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const option = context.options[0];
|
||||
const multiline = option !== "never";
|
||||
const allowSingleLine = option === "always-multiline";
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
ConditionalExpression(node) {
|
||||
const questionToken = sourceCode.getTokenAfter(node.test, astUtils.isNotClosingParenToken);
|
||||
const colonToken = sourceCode.getTokenAfter(node.consequent, astUtils.isNotClosingParenToken);
|
||||
|
||||
const firstTokenOfTest = sourceCode.getFirstToken(node);
|
||||
const lastTokenOfTest = sourceCode.getTokenBefore(questionToken);
|
||||
const firstTokenOfConsequent = sourceCode.getTokenAfter(questionToken);
|
||||
const lastTokenOfConsequent = sourceCode.getTokenBefore(colonToken);
|
||||
const firstTokenOfAlternate = sourceCode.getTokenAfter(colonToken);
|
||||
|
||||
const areTestAndConsequentOnSameLine = astUtils.isTokenOnSameLine(lastTokenOfTest, firstTokenOfConsequent);
|
||||
const areConsequentAndAlternateOnSameLine = astUtils.isTokenOnSameLine(lastTokenOfConsequent, firstTokenOfAlternate);
|
||||
|
||||
const hasComments = !!sourceCode.getCommentsInside(node).length;
|
||||
|
||||
if (!multiline) {
|
||||
if (!areTestAndConsequentOnSameLine) {
|
||||
context.report({
|
||||
node: node.test,
|
||||
loc: {
|
||||
start: firstTokenOfTest.loc.start,
|
||||
end: lastTokenOfTest.loc.end
|
||||
},
|
||||
messageId: "unexpectedTestCons",
|
||||
fix(fixer) {
|
||||
if (hasComments) {
|
||||
return null;
|
||||
}
|
||||
const fixers = [];
|
||||
const areTestAndQuestionOnSameLine = astUtils.isTokenOnSameLine(lastTokenOfTest, questionToken);
|
||||
const areQuestionAndConsOnSameLine = astUtils.isTokenOnSameLine(questionToken, firstTokenOfConsequent);
|
||||
|
||||
if (!areTestAndQuestionOnSameLine) {
|
||||
fixers.push(fixer.removeRange([lastTokenOfTest.range[1], questionToken.range[0]]));
|
||||
}
|
||||
if (!areQuestionAndConsOnSameLine) {
|
||||
fixers.push(fixer.removeRange([questionToken.range[1], firstTokenOfConsequent.range[0]]));
|
||||
}
|
||||
|
||||
return fixers;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!areConsequentAndAlternateOnSameLine) {
|
||||
context.report({
|
||||
node: node.consequent,
|
||||
loc: {
|
||||
start: firstTokenOfConsequent.loc.start,
|
||||
end: lastTokenOfConsequent.loc.end
|
||||
},
|
||||
messageId: "unexpectedConsAlt",
|
||||
fix(fixer) {
|
||||
if (hasComments) {
|
||||
return null;
|
||||
}
|
||||
const fixers = [];
|
||||
const areConsAndColonOnSameLine = astUtils.isTokenOnSameLine(lastTokenOfConsequent, colonToken);
|
||||
const areColonAndAltOnSameLine = astUtils.isTokenOnSameLine(colonToken, firstTokenOfAlternate);
|
||||
|
||||
if (!areConsAndColonOnSameLine) {
|
||||
fixers.push(fixer.removeRange([lastTokenOfConsequent.range[1], colonToken.range[0]]));
|
||||
}
|
||||
if (!areColonAndAltOnSameLine) {
|
||||
fixers.push(fixer.removeRange([colonToken.range[1], firstTokenOfAlternate.range[0]]));
|
||||
}
|
||||
|
||||
return fixers;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (allowSingleLine && node.loc.start.line === node.loc.end.line) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (areTestAndConsequentOnSameLine) {
|
||||
context.report({
|
||||
node: node.test,
|
||||
loc: {
|
||||
start: firstTokenOfTest.loc.start,
|
||||
end: lastTokenOfTest.loc.end
|
||||
},
|
||||
messageId: "expectedTestCons",
|
||||
fix: fixer => (hasComments ? null : (
|
||||
fixer.replaceTextRange(
|
||||
[
|
||||
lastTokenOfTest.range[1],
|
||||
questionToken.range[0]
|
||||
],
|
||||
"\n"
|
||||
)
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
if (areConsequentAndAlternateOnSameLine) {
|
||||
context.report({
|
||||
node: node.consequent,
|
||||
loc: {
|
||||
start: firstTokenOfConsequent.loc.start,
|
||||
end: lastTokenOfConsequent.loc.end
|
||||
},
|
||||
messageId: "expectedConsAlt",
|
||||
fix: (fixer => (hasComments ? null : (
|
||||
fixer.replaceTextRange(
|
||||
[
|
||||
lastTokenOfConsequent.range[1],
|
||||
colonToken.range[0]
|
||||
],
|
||||
"\n"
|
||||
)
|
||||
)))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of constructors without capital letters
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const CAPS_ALLOWED = [
|
||||
"Array",
|
||||
"Boolean",
|
||||
"Date",
|
||||
"Error",
|
||||
"Function",
|
||||
"Number",
|
||||
"Object",
|
||||
"RegExp",
|
||||
"String",
|
||||
"Symbol",
|
||||
"BigInt"
|
||||
];
|
||||
|
||||
/**
|
||||
* 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 {any} fallback If obj[key] is not present, this will be returned.
|
||||
* @throws {TypeError} If key is not an own array type property of `obj`.
|
||||
* @returns {string[]} Returns obj[key] if it's an Array, otherwise `fallback`
|
||||
*/
|
||||
function checkArray(obj, key, fallback) {
|
||||
|
||||
/* c8 ignore start */
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) {
|
||||
throw new TypeError(`${key}, if provided, must be an Array`);
|
||||
}/* c8 ignore stop */
|
||||
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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Require constructor names to begin with a capital letter",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/new-cap"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
newIsCap: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
},
|
||||
capIsNew: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
},
|
||||
newIsCapExceptions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
newIsCapExceptionPattern: {
|
||||
type: "string"
|
||||
},
|
||||
capIsNewExceptions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
capIsNewExceptionPattern: {
|
||||
type: "string"
|
||||
},
|
||||
properties: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
upper: "A function with a name starting with an uppercase letter should only be used as a constructor.",
|
||||
lower: "A constructor name should not start with a lowercase letter."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const config = 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, "u") : null;
|
||||
|
||||
const capIsNewExceptions = calculateCapIsNewExceptions(config);
|
||||
const capIsNewExceptionPattern = config.capIsNewExceptionPattern ? new RegExp(config.capIsNewExceptionPattern, "u") : null;
|
||||
|
||||
const listeners = {};
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get exact callee name from expression
|
||||
* @param {ASTNode} node CallExpression or NewExpression node
|
||||
* @returns {string} name
|
||||
*/
|
||||
function extractNameFromExpression(node) {
|
||||
return node.callee.type === "Identifier"
|
||||
? node.callee.name
|
||||
: astUtils.getStaticPropertyName(node.callee) || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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";
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
const callee = astUtils.skipChainExpression(node.callee);
|
||||
|
||||
if (calleeName === "UTC" && callee.type === "MemberExpression") {
|
||||
|
||||
// allow if callee is Date.UTC
|
||||
return callee.object.type === "Identifier" &&
|
||||
callee.object.name === "Date";
|
||||
}
|
||||
|
||||
return skipProperties && callee.type === "MemberExpression";
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the given messageId 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} messageId The messageId to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(node, messageId) {
|
||||
let callee = astUtils.skipChainExpression(node.callee);
|
||||
|
||||
if (callee.type === "MemberExpression") {
|
||||
callee = callee.property;
|
||||
}
|
||||
|
||||
context.report({ node, loc: callee.loc, messageId });
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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, "lower");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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, "upper");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return listeners;
|
||||
}
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag when using constructor without parentheses
|
||||
* @author Ilya Volodin
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Enforce or disallow parentheses when invoking a constructor with no arguments",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/new-parens"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "never"]
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
missing: "Missing '()' invoking a constructor.",
|
||||
unnecessary: "Unnecessary '()' invoking a constructor with no arguments."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options;
|
||||
const always = options[0] !== "never"; // Default is always
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
NewExpression(node) {
|
||||
if (node.arguments.length !== 0) {
|
||||
return; // if there are arguments, there have to be parens
|
||||
}
|
||||
|
||||
const lastToken = sourceCode.getLastToken(node);
|
||||
const hasLastParen = lastToken && astUtils.isClosingParenToken(lastToken);
|
||||
|
||||
// `hasParens` is true only if the new expression ends with its own parens, e.g., new new foo() does not end with its own parens
|
||||
const hasParens = hasLastParen &&
|
||||
astUtils.isOpeningParenToken(sourceCode.getTokenBefore(lastToken)) &&
|
||||
node.callee.range[1] < node.range[1];
|
||||
|
||||
if (always) {
|
||||
if (!hasParens) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "missing",
|
||||
fix: fixer => fixer.insertTextAfter(node, "()")
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (hasParens) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unnecessary",
|
||||
fix: fixer => [
|
||||
fixer.remove(sourceCode.getTokenBefore(lastToken)),
|
||||
fixer.remove(lastToken),
|
||||
fixer.insertTextBefore(node, "("),
|
||||
fixer.insertTextAfter(node, ")")
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* @fileoverview Rule to check empty newline after "var" statement
|
||||
* @author Gopal Venkatesan
|
||||
* @deprecated in ESLint v4.0.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Require or disallow an empty line after variable declarations",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/newline-after-var"
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
enum: ["never", "always"]
|
||||
}
|
||||
],
|
||||
fixable: "whitespace",
|
||||
messages: {
|
||||
expected: "Expected blank line after variable declarations.",
|
||||
unexpected: "Unexpected blank line after variable declarations."
|
||||
},
|
||||
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: ["padding-line-between-statements"]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
// 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,
|
||||
messageId: "unexpected",
|
||||
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,
|
||||
messageId: "expected",
|
||||
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
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* @fileoverview Rule to require newlines before `return` statement
|
||||
* @author Kai Cataldo
|
||||
* @deprecated in ESLint v4.0.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Require an empty line before `return` statements",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/newline-before-return"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
schema: [],
|
||||
messages: {
|
||||
expected: "Expected newline before return statement."
|
||||
},
|
||||
|
||||
deprecated: true,
|
||||
replacedBy: ["padding-line-between-statements"]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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.includes(tokenBefore.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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", ")"]);
|
||||
}
|
||||
if (parentType === "DoWhileStatement") {
|
||||
return isPrecededByTokens(node, ["do"]);
|
||||
}
|
||||
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.getCommentsBefore(node);
|
||||
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.getCommentsBefore(node);
|
||||
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,
|
||||
messageId: "expected",
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+126
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* @fileoverview Rule to ensure newline per method call when chaining calls
|
||||
* @author Rajendra Patil
|
||||
* @author Burak Yigit Kaya
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Require a newline after each call in a method chain",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/newline-per-chained-call"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
ignoreChainWithDepth: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 10,
|
||||
default: 2
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
messages: {
|
||||
expected: "Expected line break before `{{callee}}`."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const options = context.options[0] || {},
|
||||
ignoreChainWithDepth = options.ignoreChainWithDepth || 2;
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Get the prefix of a given MemberExpression node.
|
||||
* If the MemberExpression node is a computed value it returns a
|
||||
* left bracket. If not it returns a period.
|
||||
* @param {ASTNode} node A MemberExpression node to get
|
||||
* @returns {string} The prefix of the node.
|
||||
*/
|
||||
function getPrefix(node) {
|
||||
if (node.computed) {
|
||||
if (node.optional) {
|
||||
return "?.[";
|
||||
}
|
||||
return "[";
|
||||
}
|
||||
if (node.optional) {
|
||||
return "?.";
|
||||
}
|
||||
return ".";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = getPrefix(node);
|
||||
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) {
|
||||
const callee = astUtils.skipChainExpression(node.callee);
|
||||
|
||||
if (callee.type !== "MemberExpression") {
|
||||
return;
|
||||
}
|
||||
|
||||
let parent = astUtils.skipChainExpression(callee.object);
|
||||
let depth = 1;
|
||||
|
||||
while (parent && parent.callee) {
|
||||
depth += 1;
|
||||
parent = astUtils.skipChainExpression(astUtils.skipChainExpression(parent.callee).object);
|
||||
}
|
||||
|
||||
if (depth > ignoreChainWithDepth && astUtils.isTokenOnSameLine(callee.object, callee.property)) {
|
||||
const firstTokenAfterObject = sourceCode.getTokenAfter(callee.object, astUtils.isNotClosingParenToken);
|
||||
|
||||
context.report({
|
||||
node: callee.property,
|
||||
loc: {
|
||||
start: firstTokenAfterObject.loc.start,
|
||||
end: callee.loc.end
|
||||
},
|
||||
messageId: "expected",
|
||||
data: {
|
||||
callee: getPropertyText(callee)
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(firstTokenAfterObject, "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of alert, confirm, prompt
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
getStaticPropertyName: getPropertyName,
|
||||
getVariableByName,
|
||||
skipChainExpression
|
||||
} = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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)$/u.test(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the eslint-scope 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 {string} 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {string} node The identifier node to check
|
||||
* @returns {boolean} Whether or not the node is a reference to the global object.
|
||||
*/
|
||||
function isGlobalThisReferenceOrGlobalWindow(scope, node) {
|
||||
if (scope.type === "global" && node.type === "ThisExpression") {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
node.type === "Identifier" &&
|
||||
(
|
||||
node.name === "window" ||
|
||||
(node.name === "globalThis" && getVariableByName(scope, "globalThis"))
|
||||
)
|
||||
) {
|
||||
return !isShadowed(scope, node);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow the use of `alert`, `confirm`, and `prompt`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-alert"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected {{name}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const callee = skipChainExpression(node.callee),
|
||||
currentScope = sourceCode.getScope(node);
|
||||
|
||||
// without window.
|
||||
if (callee.type === "Identifier") {
|
||||
const name = callee.name;
|
||||
|
||||
if (!isShadowed(currentScope, callee) && isProhibitedIdentifier(callee.name)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpected",
|
||||
data: { name }
|
||||
});
|
||||
}
|
||||
|
||||
} else if (callee.type === "MemberExpression" && isGlobalThisReferenceOrGlobalWindow(currentScope, callee.object)) {
|
||||
const name = getPropertyName(callee);
|
||||
|
||||
if (isProhibitedIdentifier(name)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpected",
|
||||
data: { name }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* @fileoverview Disallow construction of dense arrays using the Array constructor
|
||||
* @author Matt DuVall <http://www.mattduvall.com/>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
getVariableByName,
|
||||
isClosingParenToken,
|
||||
isOpeningParenToken,
|
||||
isStartOfExpressionStatement,
|
||||
needsPrecedingSemicolon
|
||||
} = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `Array` constructors",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-array-constructor"
|
||||
},
|
||||
|
||||
hasSuggestions: true,
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
preferLiteral: "The array literal notation [] is preferable.",
|
||||
useLiteral: "Replace with an array literal.",
|
||||
useLiteralAfterSemicolon: "Replace with an array literal, add preceding semicolon."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Gets the text between the calling parentheses of a CallExpression or NewExpression.
|
||||
* @param {ASTNode} node A CallExpression or NewExpression node.
|
||||
* @returns {string} The text between the calling parentheses, or an empty string if there are none.
|
||||
*/
|
||||
function getArgumentsText(node) {
|
||||
const lastToken = sourceCode.getLastToken(node);
|
||||
|
||||
if (!isClosingParenToken(lastToken)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let firstToken = node.callee;
|
||||
|
||||
do {
|
||||
firstToken = sourceCode.getTokenAfter(firstToken);
|
||||
if (!firstToken || firstToken === lastToken) {
|
||||
return "";
|
||||
}
|
||||
} while (!isOpeningParenToken(firstToken));
|
||||
|
||||
return sourceCode.text.slice(firstToken.range[1], lastToken.range[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallow construction of dense arrays using the Array constructor
|
||||
* @param {ASTNode} node node to evaluate
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function check(node) {
|
||||
if (
|
||||
node.callee.type !== "Identifier" ||
|
||||
node.callee.name !== "Array" ||
|
||||
node.arguments.length === 1 &&
|
||||
node.arguments[0].type !== "SpreadElement") {
|
||||
return;
|
||||
}
|
||||
|
||||
const variable = getVariableByName(sourceCode.getScope(node), "Array");
|
||||
|
||||
/*
|
||||
* Check if `Array` is a predefined global variable: predefined globals have no declarations,
|
||||
* meaning that the `identifiers` list of the variable object is empty.
|
||||
*/
|
||||
if (variable && variable.identifiers.length === 0) {
|
||||
const argsText = getArgumentsText(node);
|
||||
let fixText;
|
||||
let messageId;
|
||||
|
||||
/*
|
||||
* Check if the suggested change should include a preceding semicolon or not.
|
||||
* Due to JavaScript's ASI rules, a missing semicolon may be inserted automatically
|
||||
* before an expression like `Array()` or `new Array()`, but not when the expression
|
||||
* is changed into an array literal like `[]`.
|
||||
*/
|
||||
if (isStartOfExpressionStatement(node) && needsPrecedingSemicolon(sourceCode, node)) {
|
||||
fixText = `;[${argsText}]`;
|
||||
messageId = "useLiteralAfterSemicolon";
|
||||
} else {
|
||||
fixText = `[${argsText}]`;
|
||||
messageId = "useLiteral";
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "preferLiteral",
|
||||
suggest: [
|
||||
{
|
||||
messageId,
|
||||
fix: fixer => fixer.replaceText(node, fixText)
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression: check,
|
||||
NewExpression: check
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @fileoverview disallow using an async function as a Promise executor
|
||||
* @author Teddy Katz
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow using an async function as a Promise executor",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-async-promise-executor"
|
||||
},
|
||||
|
||||
fixable: null,
|
||||
schema: [],
|
||||
messages: {
|
||||
async: "Promise executor functions should not be async."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
"NewExpression[callee.name='Promise'][arguments.0.async=true]"(node) {
|
||||
context.report({
|
||||
node: context.sourceCode.getFirstToken(node.arguments[0], token => token.value === "async"),
|
||||
messageId: "async"
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow uses of await inside of loops.
|
||||
* @author Nat Mote (nmote)
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Check whether it should stop traversing ancestors at the given node.
|
||||
* @param {ASTNode} node A node to check.
|
||||
* @returns {boolean} `true` if it should stop traversing.
|
||||
*/
|
||||
function isBoundary(node) {
|
||||
const t = node.type;
|
||||
|
||||
return (
|
||||
t === "FunctionDeclaration" ||
|
||||
t === "FunctionExpression" ||
|
||||
t === "ArrowFunctionExpression" ||
|
||||
|
||||
/*
|
||||
* Don't report the await expressions on for-await-of loop since it's
|
||||
* asynchronous iteration intentionally.
|
||||
*/
|
||||
(t === "ForOfStatement" && node.await === true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given node is in loop.
|
||||
* @param {ASTNode} node A node to check.
|
||||
* @param {ASTNode} parent A parent node to check.
|
||||
* @returns {boolean} `true` if the node is in loop.
|
||||
*/
|
||||
function isLooped(node, parent) {
|
||||
switch (parent.type) {
|
||||
case "ForStatement":
|
||||
return (
|
||||
node === parent.test ||
|
||||
node === parent.update ||
|
||||
node === parent.body
|
||||
);
|
||||
|
||||
case "ForOfStatement":
|
||||
case "ForInStatement":
|
||||
return node === parent.body;
|
||||
|
||||
case "WhileStatement":
|
||||
case "DoWhileStatement":
|
||||
return node === parent.test || node === parent.body;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `await` inside of loops",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-await-in-loop"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpectedAwait: "Unexpected `await` inside a loop."
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
|
||||
/**
|
||||
* Validate an await expression.
|
||||
* @param {ASTNode} awaitNode An AwaitExpression or ForOfStatement node to validate.
|
||||
* @returns {void}
|
||||
*/
|
||||
function validate(awaitNode) {
|
||||
if (awaitNode.type === "ForOfStatement" && !awaitNode.await) {
|
||||
return;
|
||||
}
|
||||
|
||||
let node = awaitNode;
|
||||
let parent = node.parent;
|
||||
|
||||
while (parent && !isBoundary(parent)) {
|
||||
if (isLooped(node, parent)) {
|
||||
context.report({
|
||||
node: awaitNode,
|
||||
messageId: "unexpectedAwait"
|
||||
});
|
||||
return;
|
||||
}
|
||||
node = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
AwaitExpression: validate,
|
||||
ForOfStatement: validate
|
||||
};
|
||||
}
|
||||
};
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag bitwise identifiers
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/*
|
||||
*
|
||||
* Set of bitwise operators.
|
||||
*
|
||||
*/
|
||||
const BITWISE_OPERATORS = [
|
||||
"^", "|", "&", "<<", ">>", ">>>",
|
||||
"^=", "|=", "&=", "<<=", ">>=", ">>>=",
|
||||
"~"
|
||||
];
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow bitwise operators",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-bitwise"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allow: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: BITWISE_OPERATORS
|
||||
},
|
||||
uniqueItems: true
|
||||
},
|
||||
int32Hint: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected use of '{{operator}}'."
|
||||
}
|
||||
},
|
||||
|
||||
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, messageId: "unexpected", 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.includes(node.operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.includes(node.operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @fileoverview disallow use of the Buffer() constructor
|
||||
* @author Teddy Katz
|
||||
* @deprecated in ESLint v7.0.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: [],
|
||||
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow use of the `Buffer()` constructor",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-buffer-constructor"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
deprecated: "{{expr}} is deprecated. Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
"CallExpression[callee.name='Buffer'], NewExpression[callee.name='Buffer']"(node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "deprecated",
|
||||
data: { expr: node.type === "CallExpression" ? "Buffer()" : "new Buffer()" }
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of arguments.callee and arguments.caller.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow the use of `arguments.caller` or `arguments.callee`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-caller"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Avoid arguments.{{prop}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
|
||||
MemberExpression(node) {
|
||||
const objectName = node.object.name,
|
||||
propertyName = node.property.name;
|
||||
|
||||
if (objectName === "arguments" && !node.computed && propertyName && propertyName.match(/^calle[er]$/u)) {
|
||||
context.report({ node, messageId: "unexpected", data: { prop: propertyName } });
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of an lexical declarations inside a case clause
|
||||
* @author Erik Arvidsson
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow lexical declarations in case clauses",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-case-declarations"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected lexical declaration in case block."
|
||||
}
|
||||
},
|
||||
|
||||
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: statement,
|
||||
messageId: "unexpected"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag variable leak in CatchClauses in IE 8 and earlier
|
||||
* @author Ian Christian Myers
|
||||
* @deprecated in ESLint v5.1.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `catch` clause parameters from shadowing variables in the outer scope",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-catch-shadow"
|
||||
},
|
||||
|
||||
replacedBy: ["no-shadow"],
|
||||
|
||||
deprecated: true,
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
mutable: "Value of '{{name}}' may be overwritten in IE 8 and earlier."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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[param!=null]"(node) {
|
||||
let scope = sourceCode.getScope(node);
|
||||
|
||||
/*
|
||||
* When ecmaVersion >= 6, 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, messageId: "mutable", data: { name: node.param.name } });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @fileoverview A rule to disallow modifying variables of class declarations
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow reassigning class members",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-class-assign"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
class: "'{{name}}' is a class."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* 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, messageId: "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) {
|
||||
sourceCode.getDeclaredVariables(node).forEach(checkVariable);
|
||||
}
|
||||
|
||||
return {
|
||||
ClassDeclaration: checkForClass,
|
||||
ClassExpression: checkForClass
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @fileoverview The rule should warn against code that tries to compare against -0.
|
||||
* @author Aladdin-ADD <hh_2013@foxmail.com>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow comparing against -0",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-compare-neg-zero"
|
||||
},
|
||||
|
||||
fixable: null,
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Do not use the '{{operator}}' operator to compare against -0."
|
||||
}
|
||||
},
|
||||
|
||||
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,
|
||||
messageId: "unexpected",
|
||||
data: { operator: node.operator }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag assignment in a conditional statement's test expression
|
||||
* @author Stephen Murray <spmurrayzzz>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const TEST_CONDITION_PARENT_TYPES = new Set(["IfStatement", "WhileStatement", "DoWhileStatement", "ForStatement", "ConditionalExpression"]);
|
||||
|
||||
const NODE_DESCRIPTIONS = {
|
||||
DoWhileStatement: "a 'do...while' statement",
|
||||
ForStatement: "a 'for' statement",
|
||||
IfStatement: "an 'if' statement",
|
||||
WhileStatement: "a 'while' statement"
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow assignment operators in conditional expressions",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-cond-assign"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["except-parens", "always"]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected assignment within {{type}}.",
|
||||
|
||||
// must match JSHint's error message
|
||||
missing: "Expected a conditional expression and instead saw an assignment."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const prohibitAssign = (context.options[0] || "except-parens");
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* 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 &&
|
||||
TEST_CONDITION_PARENT_TYPES.has(node.parent.type) &&
|
||||
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) &&
|
||||
previousToken && 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)
|
||||
)
|
||||
) {
|
||||
|
||||
context.report({
|
||||
node: node.test,
|
||||
messageId: "missing"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
messageId: "unexpected",
|
||||
data: {
|
||||
type: NODE_DESCRIPTIONS[ancestor.type] || ancestor.type
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (prohibitAssign === "always") {
|
||||
return {
|
||||
AssignmentExpression: testForConditionalAncestor
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
DoWhileStatement: testForAssign,
|
||||
ForStatement: testForAssign,
|
||||
IfStatement: testForAssign,
|
||||
WhileStatement: testForAssign,
|
||||
ConditionalExpression: testForAssign
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @fileoverview A rule to warn against using arrow functions when they could be
|
||||
* confused with comparisons
|
||||
* @author Jxck <https://github.com/Jxck>
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow arrow functions where they could be confused with comparisons",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-confusing-arrow"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowParens: { type: "boolean", default: true },
|
||||
onlyOneSimpleParam: { type: "boolean", default: false }
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
confusing: "Arrow function used ambiguously with a conditional expression."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0] || {};
|
||||
const allowParens = config.allowParens || (config.allowParens === void 0);
|
||||
const onlyOneSimpleParam = config.onlyOneSimpleParam;
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
|
||||
/**
|
||||
* 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) &&
|
||||
!(allowParens && astUtils.isParenthesised(sourceCode, body)) &&
|
||||
!(onlyOneSimpleParam && !(node.params.length === 1 && node.params[0].type === "Identifier"))) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "confusing",
|
||||
fix(fixer) {
|
||||
|
||||
// if `allowParens` is not set to true don't bother wrapping in parens
|
||||
return allowParens && fixer.replaceText(node.body, `(${sourceCode.getText(node.body)})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ArrowFunctionExpression: checkArrowFunc
|
||||
};
|
||||
}
|
||||
};
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of console object
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow the use of `console`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-console"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allow: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
},
|
||||
minItems: 1,
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
hasSuggestions: true,
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected console statement.",
|
||||
removeConsole: "Remove the console.{{ propertyName }}()."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const allowed = options.allow || [];
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Checks whether the given reference is 'console' or not.
|
||||
* @param {eslint-scope.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.includes(propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given reference is a member access which is not
|
||||
* allowed by options or not.
|
||||
* @param {eslint-scope.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)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if removing the ExpressionStatement node will cause ASI to
|
||||
* break.
|
||||
* eg.
|
||||
* foo()
|
||||
* console.log();
|
||||
* [1, 2, 3].forEach(a => doSomething(a))
|
||||
*
|
||||
* Removing the console.log(); statement should leave two statements, but
|
||||
* here the two statements will become one because [ causes continuation after
|
||||
* foo().
|
||||
* @param {ASTNode} node The ExpressionStatement node to check.
|
||||
* @returns {boolean} `true` if ASI will break after removing the ExpressionStatement
|
||||
* node.
|
||||
*/
|
||||
function maybeAsiHazard(node) {
|
||||
const SAFE_TOKENS_BEFORE = /^[:;{]$/u; // One of :;{
|
||||
const UNSAFE_CHARS_AFTER = /^[-[(/+`]/u; // One of [(/+-`
|
||||
|
||||
const tokenBefore = sourceCode.getTokenBefore(node);
|
||||
const tokenAfter = sourceCode.getTokenAfter(node);
|
||||
|
||||
return (
|
||||
Boolean(tokenAfter) &&
|
||||
UNSAFE_CHARS_AFTER.test(tokenAfter.value) &&
|
||||
tokenAfter.value !== "++" &&
|
||||
tokenAfter.value !== "--" &&
|
||||
Boolean(tokenBefore) &&
|
||||
!SAFE_TOKENS_BEFORE.test(tokenBefore.value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the MemberExpression node's parent.parent.parent is a
|
||||
* Program, BlockStatement, StaticBlock, or SwitchCase node. This check
|
||||
* is necessary to avoid providing a suggestion that might cause a syntax error.
|
||||
*
|
||||
* eg. if (a) console.log(b), removing console.log() here will lead to a
|
||||
* syntax error.
|
||||
* if (a) { console.log(b) }, removing console.log() here is acceptable.
|
||||
*
|
||||
* Additionally, it checks if the callee of the CallExpression node is
|
||||
* the node itself.
|
||||
*
|
||||
* eg. foo(console.log), cannot provide a suggestion here.
|
||||
* @param {ASTNode} node The MemberExpression node to check.
|
||||
* @returns {boolean} `true` if a suggestion can be provided for a node.
|
||||
*/
|
||||
function canProvideSuggestions(node) {
|
||||
return (
|
||||
node.parent.type === "CallExpression" &&
|
||||
node.parent.callee === node &&
|
||||
node.parent.parent.type === "ExpressionStatement" &&
|
||||
astUtils.STATEMENT_LIST_PARENTS.has(node.parent.parent.parent.type) &&
|
||||
!maybeAsiHazard(node.parent.parent)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the given reference as a violation.
|
||||
* @param {eslint-scope.Reference} reference The reference to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(reference) {
|
||||
const node = reference.identifier.parent;
|
||||
|
||||
const propertyName = astUtils.getStaticPropertyName(node);
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: node.loc,
|
||||
messageId: "unexpected",
|
||||
suggest: canProvideSuggestions(node)
|
||||
? [{
|
||||
messageId: "removeConsole",
|
||||
data: { propertyName },
|
||||
fix(fixer) {
|
||||
return fixer.remove(node.parent.parent);
|
||||
}
|
||||
}]
|
||||
: []
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
"Program:exit"(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @fileoverview A rule to disallow modifying variables that are declared using `const`
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow reassigning `const` variables",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-const-assign"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
const: "'{{name}}' is constant."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* 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, messageId: "const", data: { name: reference.identifier.name } });
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
VariableDeclaration(node) {
|
||||
if (node.kind === "const") {
|
||||
sourceCode.getDeclaredVariables(node).forEach(checkVariable);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+509
@@ -0,0 +1,509 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag constant comparisons and logical expressions that always/never short circuit
|
||||
* @author Jordan Eldredge <https://jordaneldredge.com>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const globals = require("globals");
|
||||
const { isNullLiteral, isConstant, isReferenceToGlobalVariable, isLogicalAssignmentOperator } = require("./utils/ast-utils");
|
||||
|
||||
const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set(["+", "-", "*", "/", "%", "|", "^", "&", "**", "<<", ">>", ">>>"]);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not a node is `null` or `undefined`. Similar to the one
|
||||
* found in ast-utils.js, but this one correctly handles the edge case that
|
||||
* `undefined` has been redefined.
|
||||
* @param {Scope} scope Scope in which the expression was found.
|
||||
* @param {ASTNode} node A node to check.
|
||||
* @returns {boolean} Whether or not the node is a `null` or `undefined`.
|
||||
* @public
|
||||
*/
|
||||
function isNullOrUndefined(scope, node) {
|
||||
return (
|
||||
isNullLiteral(node) ||
|
||||
(node.type === "Identifier" && node.name === "undefined" && isReferenceToGlobalVariable(scope, node)) ||
|
||||
(node.type === "UnaryExpression" && node.operator === "void")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if an AST node has a statically knowable constant nullishness. Meaning,
|
||||
* it will always resolve to a constant value of either: `null`, `undefined`
|
||||
* or not `null` _or_ `undefined`. An expression that can vary between those
|
||||
* three states at runtime would return `false`.
|
||||
* @param {Scope} scope The scope in which the node was found.
|
||||
* @param {ASTNode} node The AST node being tested.
|
||||
* @param {boolean} nonNullish if `true` then nullish values are not considered constant.
|
||||
* @returns {boolean} Does `node` have constant nullishness?
|
||||
*/
|
||||
function hasConstantNullishness(scope, node, nonNullish) {
|
||||
if (nonNullish && isNullOrUndefined(scope, node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case "ObjectExpression": // Objects are never nullish
|
||||
case "ArrayExpression": // Arrays are never nullish
|
||||
case "ArrowFunctionExpression": // Functions never nullish
|
||||
case "FunctionExpression": // Functions are never nullish
|
||||
case "ClassExpression": // Classes are never nullish
|
||||
case "NewExpression": // Objects are never nullish
|
||||
case "Literal": // Nullish, or non-nullish, literals never change
|
||||
case "TemplateLiteral": // A string is never nullish
|
||||
case "UpdateExpression": // Numbers are never nullish
|
||||
case "BinaryExpression": // Numbers, strings, or booleans are never nullish
|
||||
return true;
|
||||
case "CallExpression": {
|
||||
if (node.callee.type !== "Identifier") {
|
||||
return false;
|
||||
}
|
||||
const functionName = node.callee.name;
|
||||
|
||||
return (functionName === "Boolean" || functionName === "String" || functionName === "Number") &&
|
||||
isReferenceToGlobalVariable(scope, node.callee);
|
||||
}
|
||||
case "LogicalExpression": {
|
||||
return node.operator === "??" && hasConstantNullishness(scope, node.right, true);
|
||||
}
|
||||
case "AssignmentExpression":
|
||||
if (node.operator === "=") {
|
||||
return hasConstantNullishness(scope, node.right, nonNullish);
|
||||
}
|
||||
|
||||
/*
|
||||
* Handling short-circuiting assignment operators would require
|
||||
* walking the scope. We won't attempt that (for now...) /
|
||||
*/
|
||||
if (isLogicalAssignmentOperator(node.operator)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* The remaining assignment expressions all result in a numeric or
|
||||
* string (non-nullish) value:
|
||||
* "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="
|
||||
*/
|
||||
|
||||
return true;
|
||||
case "UnaryExpression":
|
||||
|
||||
/*
|
||||
* "void" Always returns `undefined`
|
||||
* "typeof" All types are strings, and thus non-nullish
|
||||
* "!" Boolean is never nullish
|
||||
* "delete" Returns a boolean, which is never nullish
|
||||
* Math operators always return numbers or strings, neither of which
|
||||
* are non-nullish "+", "-", "~"
|
||||
*/
|
||||
|
||||
return true;
|
||||
case "SequenceExpression": {
|
||||
const last = node.expressions[node.expressions.length - 1];
|
||||
|
||||
return hasConstantNullishness(scope, last, nonNullish);
|
||||
}
|
||||
case "Identifier":
|
||||
return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
|
||||
case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
|
||||
case "JSXFragment":
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if an AST node is a boolean value that never changes. Specifically we
|
||||
* test for:
|
||||
* 1. Literal booleans (`true` or `false`)
|
||||
* 2. Unary `!` expressions with a constant value
|
||||
* 3. Constant booleans created via the `Boolean` global function
|
||||
* @param {Scope} scope The scope in which the node was found.
|
||||
* @param {ASTNode} node The node to test
|
||||
* @returns {boolean} Is `node` guaranteed to be a boolean?
|
||||
*/
|
||||
function isStaticBoolean(scope, node) {
|
||||
switch (node.type) {
|
||||
case "Literal":
|
||||
return typeof node.value === "boolean";
|
||||
case "CallExpression":
|
||||
return node.callee.type === "Identifier" && node.callee.name === "Boolean" &&
|
||||
isReferenceToGlobalVariable(scope, node.callee) &&
|
||||
(node.arguments.length === 0 || isConstant(scope, node.arguments[0], true));
|
||||
case "UnaryExpression":
|
||||
return node.operator === "!" && isConstant(scope, node.argument, true);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test if an AST node will always give the same result when compared to a
|
||||
* boolean value. Note that comparison to boolean values is different than
|
||||
* truthiness.
|
||||
* https://262.ecma-international.org/5.1/#sec-11.9.3
|
||||
*
|
||||
* Javascript `==` operator works by converting the boolean to `1` (true) or
|
||||
* `+0` (false) and then checks the values `==` equality to that number.
|
||||
* @param {Scope} scope The scope in which node was found.
|
||||
* @param {ASTNode} node The node to test.
|
||||
* @returns {boolean} Will `node` always coerce to the same boolean value?
|
||||
*/
|
||||
function hasConstantLooseBooleanComparison(scope, node) {
|
||||
switch (node.type) {
|
||||
case "ObjectExpression":
|
||||
case "ClassExpression":
|
||||
|
||||
/**
|
||||
* In theory objects like:
|
||||
*
|
||||
* `{toString: () => a}`
|
||||
* `{valueOf: () => a}`
|
||||
*
|
||||
* Or a classes like:
|
||||
*
|
||||
* `class { static toString() { return a } }`
|
||||
* `class { static valueOf() { return a } }`
|
||||
*
|
||||
* Are not constant verifiably when `inBooleanPosition` is
|
||||
* false, but it's an edge case we've opted not to handle.
|
||||
*/
|
||||
return true;
|
||||
case "ArrayExpression": {
|
||||
const nonSpreadElements = node.elements.filter(e =>
|
||||
|
||||
// Elements can be `null` in sparse arrays: `[,,]`;
|
||||
e !== null && e.type !== "SpreadElement");
|
||||
|
||||
|
||||
/*
|
||||
* Possible future direction if needed: We could check if the
|
||||
* single value would result in variable boolean comparison.
|
||||
* For now we will err on the side of caution since `[x]` could
|
||||
* evaluate to `[0]` or `[1]`.
|
||||
*/
|
||||
return node.elements.length === 0 || nonSpreadElements.length > 1;
|
||||
}
|
||||
case "ArrowFunctionExpression":
|
||||
case "FunctionExpression":
|
||||
return true;
|
||||
case "UnaryExpression":
|
||||
if (node.operator === "void" || // Always returns `undefined`
|
||||
node.operator === "typeof" // All `typeof` strings, when coerced to number, are not 0 or 1.
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (node.operator === "!") {
|
||||
return isConstant(scope, node.argument, true);
|
||||
}
|
||||
|
||||
/*
|
||||
* We won't try to reason about +, -, ~, or delete
|
||||
* In theory, for the mathematical operators, we could look at the
|
||||
* argument and try to determine if it coerces to a constant numeric
|
||||
* value.
|
||||
*/
|
||||
return false;
|
||||
case "NewExpression": // Objects might have custom `.valueOf` or `.toString`.
|
||||
return false;
|
||||
case "CallExpression": {
|
||||
if (node.callee.type === "Identifier" &&
|
||||
node.callee.name === "Boolean" &&
|
||||
isReferenceToGlobalVariable(scope, node.callee)
|
||||
) {
|
||||
return node.arguments.length === 0 || isConstant(scope, node.arguments[0], true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "Literal": // True or false, literals never change
|
||||
return true;
|
||||
case "Identifier":
|
||||
return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
|
||||
case "TemplateLiteral":
|
||||
|
||||
/*
|
||||
* In theory we could try to check if the quasi are sufficient to
|
||||
* prove that the expression will always be true, but it would be
|
||||
* tricky to get right. For example: `000.${foo}000`
|
||||
*/
|
||||
return node.expressions.length === 0;
|
||||
case "AssignmentExpression":
|
||||
if (node.operator === "=") {
|
||||
return hasConstantLooseBooleanComparison(scope, node.right);
|
||||
}
|
||||
|
||||
/*
|
||||
* Handling short-circuiting assignment operators would require
|
||||
* walking the scope. We won't attempt that (for now...)
|
||||
*
|
||||
* The remaining assignment expressions all result in a numeric or
|
||||
* string (non-nullish) values which could be truthy or falsy:
|
||||
* "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="
|
||||
*/
|
||||
return false;
|
||||
case "SequenceExpression": {
|
||||
const last = node.expressions[node.expressions.length - 1];
|
||||
|
||||
return hasConstantLooseBooleanComparison(scope, last);
|
||||
}
|
||||
case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
|
||||
case "JSXFragment":
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test if an AST node will always give the same result when _strictly_ compared
|
||||
* to a boolean value. This can happen if the expression can never be boolean, or
|
||||
* if it is always the same boolean value.
|
||||
* @param {Scope} scope The scope in which the node was found.
|
||||
* @param {ASTNode} node The node to test
|
||||
* @returns {boolean} Will `node` always give the same result when compared to a
|
||||
* static boolean value?
|
||||
*/
|
||||
function hasConstantStrictBooleanComparison(scope, node) {
|
||||
switch (node.type) {
|
||||
case "ObjectExpression": // Objects are not booleans
|
||||
case "ArrayExpression": // Arrays are not booleans
|
||||
case "ArrowFunctionExpression": // Functions are not booleans
|
||||
case "FunctionExpression":
|
||||
case "ClassExpression": // Classes are not booleans
|
||||
case "NewExpression": // Objects are not booleans
|
||||
case "TemplateLiteral": // Strings are not booleans
|
||||
case "Literal": // True, false, or not boolean, literals never change.
|
||||
case "UpdateExpression": // Numbers are not booleans
|
||||
return true;
|
||||
case "BinaryExpression":
|
||||
return NUMERIC_OR_STRING_BINARY_OPERATORS.has(node.operator);
|
||||
case "UnaryExpression": {
|
||||
if (node.operator === "delete") {
|
||||
return false;
|
||||
}
|
||||
if (node.operator === "!") {
|
||||
return isConstant(scope, node.argument, true);
|
||||
}
|
||||
|
||||
/*
|
||||
* The remaining operators return either strings or numbers, neither
|
||||
* of which are boolean.
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
case "SequenceExpression": {
|
||||
const last = node.expressions[node.expressions.length - 1];
|
||||
|
||||
return hasConstantStrictBooleanComparison(scope, last);
|
||||
}
|
||||
case "Identifier":
|
||||
return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
|
||||
case "AssignmentExpression":
|
||||
if (node.operator === "=") {
|
||||
return hasConstantStrictBooleanComparison(scope, node.right);
|
||||
}
|
||||
|
||||
/*
|
||||
* Handling short-circuiting assignment operators would require
|
||||
* walking the scope. We won't attempt that (for now...)
|
||||
*/
|
||||
if (isLogicalAssignmentOperator(node.operator)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* The remaining assignment expressions all result in either a number
|
||||
* or a string, neither of which can ever be boolean.
|
||||
*/
|
||||
return true;
|
||||
case "CallExpression": {
|
||||
if (node.callee.type !== "Identifier") {
|
||||
return false;
|
||||
}
|
||||
const functionName = node.callee.name;
|
||||
|
||||
if (
|
||||
(functionName === "String" || functionName === "Number") &&
|
||||
isReferenceToGlobalVariable(scope, node.callee)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (functionName === "Boolean" && isReferenceToGlobalVariable(scope, node.callee)) {
|
||||
return (
|
||||
node.arguments.length === 0 || isConstant(scope, node.arguments[0], true));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
|
||||
case "JSXFragment":
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if an AST node will always result in a newly constructed object
|
||||
* @param {Scope} scope The scope in which the node was found.
|
||||
* @param {ASTNode} node The node to test
|
||||
* @returns {boolean} Will `node` always be new?
|
||||
*/
|
||||
function isAlwaysNew(scope, node) {
|
||||
switch (node.type) {
|
||||
case "ObjectExpression":
|
||||
case "ArrayExpression":
|
||||
case "ArrowFunctionExpression":
|
||||
case "FunctionExpression":
|
||||
case "ClassExpression":
|
||||
return true;
|
||||
case "NewExpression": {
|
||||
if (node.callee.type !== "Identifier") {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* All the built-in constructors are always new, but
|
||||
* user-defined constructors could return a sentinel
|
||||
* object.
|
||||
*
|
||||
* Catching these is especially useful for primitive constructors
|
||||
* which return boxed values, a surprising gotcha' in JavaScript.
|
||||
*/
|
||||
return Object.hasOwnProperty.call(globals.builtin, node.callee.name) &&
|
||||
isReferenceToGlobalVariable(scope, node.callee);
|
||||
}
|
||||
case "Literal":
|
||||
|
||||
// Regular expressions are objects, and thus always new
|
||||
return typeof node.regex === "object";
|
||||
case "SequenceExpression": {
|
||||
const last = node.expressions[node.expressions.length - 1];
|
||||
|
||||
return isAlwaysNew(scope, last);
|
||||
}
|
||||
case "AssignmentExpression":
|
||||
if (node.operator === "=") {
|
||||
return isAlwaysNew(scope, node.right);
|
||||
}
|
||||
return false;
|
||||
case "ConditionalExpression":
|
||||
return isAlwaysNew(scope, node.consequent) && isAlwaysNew(scope, node.alternate);
|
||||
case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
|
||||
case "JSXFragment":
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if one operand will cause the result to be constant.
|
||||
* @param {Scope} scope Scope in which the expression was found.
|
||||
* @param {ASTNode} a One side of the expression
|
||||
* @param {ASTNode} b The other side of the expression
|
||||
* @param {string} operator The binary expression operator
|
||||
* @returns {ASTNode | null} The node which will cause the expression to have a constant result.
|
||||
*/
|
||||
function findBinaryExpressionConstantOperand(scope, a, b, operator) {
|
||||
if (operator === "==" || operator === "!=") {
|
||||
if (
|
||||
(isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b, false)) ||
|
||||
(isStaticBoolean(scope, a) && hasConstantLooseBooleanComparison(scope, b))
|
||||
) {
|
||||
return b;
|
||||
}
|
||||
} else if (operator === "===" || operator === "!==") {
|
||||
if (
|
||||
(isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b, false)) ||
|
||||
(isStaticBoolean(scope, a) && hasConstantStrictBooleanComparison(scope, b))
|
||||
) {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow expressions where the operation doesn't affect the value",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-constant-binary-expression"
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
constantBinaryOperand: "Unexpected constant binary expression. Compares constantly with the {{otherSide}}-hand side of the `{{operator}}`.",
|
||||
constantShortCircuit: "Unexpected constant {{property}} on the left-hand side of a `{{operator}}` expression.",
|
||||
alwaysNew: "Unexpected comparison to newly constructed object. These two values can never be equal.",
|
||||
bothAlwaysNew: "Unexpected comparison of two newly constructed objects. These two values can never be equal."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
LogicalExpression(node) {
|
||||
const { operator, left } = node;
|
||||
const scope = sourceCode.getScope(node);
|
||||
|
||||
if ((operator === "&&" || operator === "||") && isConstant(scope, left, true)) {
|
||||
context.report({ node: left, messageId: "constantShortCircuit", data: { property: "truthiness", operator } });
|
||||
} else if (operator === "??" && hasConstantNullishness(scope, left, false)) {
|
||||
context.report({ node: left, messageId: "constantShortCircuit", data: { property: "nullishness", operator } });
|
||||
}
|
||||
},
|
||||
BinaryExpression(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
const { right, left, operator } = node;
|
||||
const rightConstantOperand = findBinaryExpressionConstantOperand(scope, left, right, operator);
|
||||
const leftConstantOperand = findBinaryExpressionConstantOperand(scope, right, left, operator);
|
||||
|
||||
if (rightConstantOperand) {
|
||||
context.report({ node: rightConstantOperand, messageId: "constantBinaryOperand", data: { operator, otherSide: "left" } });
|
||||
} else if (leftConstantOperand) {
|
||||
context.report({ node: leftConstantOperand, messageId: "constantBinaryOperand", data: { operator, otherSide: "right" } });
|
||||
} else if (operator === "===" || operator === "!==") {
|
||||
if (isAlwaysNew(scope, left)) {
|
||||
context.report({ node: left, messageId: "alwaysNew" });
|
||||
} else if (isAlwaysNew(scope, right)) {
|
||||
context.report({ node: right, messageId: "alwaysNew" });
|
||||
}
|
||||
} else if (operator === "==" || operator === "!=") {
|
||||
|
||||
/*
|
||||
* If both sides are "new", then both sides are objects and
|
||||
* therefore they will be compared by reference even with `==`
|
||||
* equality.
|
||||
*/
|
||||
if (isAlwaysNew(scope, left) && isAlwaysNew(scope, right)) {
|
||||
context.report({ node: left, messageId: "bothAlwaysNew" });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* In theory we could handle short-circuiting assignment operators,
|
||||
* for some constant values, but that would require walking the
|
||||
* scope to find the value of the variable being assigned. This is
|
||||
* dependant on https://github.com/eslint/eslint/issues/13776
|
||||
*
|
||||
* AssignmentExpression() {},
|
||||
*/
|
||||
};
|
||||
}
|
||||
};
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use constant conditions
|
||||
* @author Christian Schulz <http://rndm.de>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const { isConstant } = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow constant expressions in conditions",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-constant-condition"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
checkLoops: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected constant condition."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {},
|
||||
checkLoops = options.checkLoops !== false,
|
||||
loopSetStack = [];
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
let loopsInCurrentScope = new Set();
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Tracks when the given node contains a constant condition.
|
||||
* @param {ASTNode} node The AST node to check.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function trackConstantConditionLoop(node) {
|
||||
if (node.test && isConstant(sourceCode.getScope(node), node.test, true)) {
|
||||
loopsInCurrentScope.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports when the set contains the given constant condition node
|
||||
* @param {ASTNode} node The AST node to check.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkConstantConditionLoopInSet(node) {
|
||||
if (loopsInCurrentScope.has(node)) {
|
||||
loopsInCurrentScope.delete(node);
|
||||
context.report({ node: node.test, messageId: "unexpected" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports when the given node contains a constant condition.
|
||||
* @param {ASTNode} node The AST node to check.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function reportIfConstant(node) {
|
||||
if (node.test && isConstant(sourceCode.getScope(node), node.test, true)) {
|
||||
context.report({ node: node.test, messageId: "unexpected" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores current set of constant loops in loopSetStack temporarily
|
||||
* and uses a new set to track constant loops
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function enterFunction() {
|
||||
loopSetStack.push(loopsInCurrentScope);
|
||||
loopsInCurrentScope = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports when the set still contains stored constant conditions
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function exitFunction() {
|
||||
loopsInCurrentScope = loopSetStack.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks node when checkLoops option is enabled
|
||||
* @param {ASTNode} node The AST node to check.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkLoop(node) {
|
||||
if (checkLoops) {
|
||||
trackConstantConditionLoop(node);
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
ConditionalExpression: reportIfConstant,
|
||||
IfStatement: reportIfConstant,
|
||||
WhileStatement: checkLoop,
|
||||
"WhileStatement:exit": checkConstantConditionLoopInSet,
|
||||
DoWhileStatement: checkLoop,
|
||||
"DoWhileStatement:exit": checkConstantConditionLoopInSet,
|
||||
ForStatement: checkLoop,
|
||||
"ForStatement > .test": node => checkLoop(node.parent),
|
||||
"ForStatement:exit": checkConstantConditionLoopInSet,
|
||||
FunctionDeclaration: enterFunction,
|
||||
"FunctionDeclaration:exit": exitFunction,
|
||||
FunctionExpression: enterFunction,
|
||||
"FunctionExpression:exit": exitFunction,
|
||||
YieldExpression: () => loopsInCurrentScope.clear()
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow returning value from constructor.
|
||||
* @author Pig Fang <https://github.com/g-plane>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow returning value from constructor",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-constructor-return"
|
||||
},
|
||||
|
||||
schema: {},
|
||||
|
||||
fixable: null,
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected return statement in constructor."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const stack = [];
|
||||
|
||||
return {
|
||||
onCodePathStart(_, node) {
|
||||
stack.push(node);
|
||||
},
|
||||
onCodePathEnd() {
|
||||
stack.pop();
|
||||
},
|
||||
ReturnStatement(node) {
|
||||
const last = stack[stack.length - 1];
|
||||
|
||||
if (!last.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
last.parent.type === "MethodDefinition" &&
|
||||
last.parent.kind === "constructor" &&
|
||||
(node.parent.parent === last || node.argument)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpected"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of continue statement
|
||||
* @author Borislav Zhivkov
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `continue` statements",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-continue"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected use of continue statement."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
ContinueStatement(node) {
|
||||
context.report({ node, messageId: "unexpected" });
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @fileoverview Rule to forbid control characters from regular expressions.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const RegExpValidator = require("@eslint-community/regexpp").RegExpValidator;
|
||||
const collector = new (class {
|
||||
constructor() {
|
||||
this._source = "";
|
||||
this._controlChars = [];
|
||||
this._validator = new RegExpValidator(this);
|
||||
}
|
||||
|
||||
onPatternEnter() {
|
||||
|
||||
/*
|
||||
* `RegExpValidator` may parse the pattern twice in one `validatePattern`.
|
||||
* So `this._controlChars` should be cleared here as well.
|
||||
*
|
||||
* For example, the `/(?<a>\x1f)/` regex will parse the pattern twice.
|
||||
* This is based on the content described in Annex B.
|
||||
* If the regex contains a `GroupName` and the `u` flag is not used, `ParseText` will be called twice.
|
||||
* See https://tc39.es/ecma262/2023/multipage/additional-ecmascript-features-for-web-browsers.html#sec-parsepattern-annexb
|
||||
*/
|
||||
this._controlChars = [];
|
||||
}
|
||||
|
||||
onCharacter(start, end, cp) {
|
||||
if (cp >= 0x00 &&
|
||||
cp <= 0x1F &&
|
||||
(
|
||||
this._source.codePointAt(start) === cp ||
|
||||
this._source.slice(start, end).startsWith("\\x") ||
|
||||
this._source.slice(start, end).startsWith("\\u")
|
||||
)
|
||||
) {
|
||||
this._controlChars.push(`\\x${`0${cp.toString(16)}`.slice(-2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
collectControlChars(regexpStr, flags) {
|
||||
const uFlag = typeof flags === "string" && flags.includes("u");
|
||||
const vFlag = typeof flags === "string" && flags.includes("v");
|
||||
|
||||
this._controlChars = [];
|
||||
this._source = regexpStr;
|
||||
|
||||
try {
|
||||
this._validator.validatePattern(regexpStr, void 0, void 0, { unicode: uFlag, unicodeSets: vFlag }); // Call onCharacter hook
|
||||
} catch {
|
||||
|
||||
// Ignore syntax errors in RegExp.
|
||||
}
|
||||
return this._controlChars;
|
||||
}
|
||||
})();
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow control characters in regular expressions",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-control-regex"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected control character(s) in regular expression: {{controlChars}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/**
|
||||
* Get the regex expression
|
||||
* @param {ASTNode} node `Literal` node to evaluate
|
||||
* @returns {{ pattern: string, flags: string | null } | null} Regex if found (the given node is either a regex literal
|
||||
* or a string literal that is the pattern argument of a RegExp constructor call). Otherwise `null`. If flags cannot be determined,
|
||||
* the `flags` property will be `null`.
|
||||
* @private
|
||||
*/
|
||||
function getRegExp(node) {
|
||||
if (node.regex) {
|
||||
return node.regex;
|
||||
}
|
||||
if (typeof node.value === "string" &&
|
||||
(node.parent.type === "NewExpression" || node.parent.type === "CallExpression") &&
|
||||
node.parent.callee.type === "Identifier" &&
|
||||
node.parent.callee.name === "RegExp" &&
|
||||
node.parent.arguments[0] === node
|
||||
) {
|
||||
const pattern = node.value;
|
||||
const flags =
|
||||
node.parent.arguments.length > 1 &&
|
||||
node.parent.arguments[1].type === "Literal" &&
|
||||
typeof node.parent.arguments[1].value === "string"
|
||||
? node.parent.arguments[1].value
|
||||
: null;
|
||||
|
||||
return { pattern, flags };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
Literal(node) {
|
||||
const regExp = getRegExp(node);
|
||||
|
||||
if (regExp) {
|
||||
const { pattern, flags } = regExp;
|
||||
const controlCharacters = collector.collectControlChars(pattern, flags);
|
||||
|
||||
if (controlCharacters.length > 0) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpected",
|
||||
data: {
|
||||
controlChars: controlCharacters.join(", ")
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of a debugger statement
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow the use of `debugger`",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-debugger"
|
||||
},
|
||||
|
||||
fixable: null,
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected 'debugger' statement."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
DebuggerStatement(node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpected"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag when deleting variables
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow deleting variables",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-delete-var"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Variables should not be deleted."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
|
||||
UnaryExpression(node) {
|
||||
if (node.operator === "delete" && node.argument.type === "Identifier") {
|
||||
context.report({ node, messageId: "unexpected" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @fileoverview Rule to check for ambiguous div operator in regexes
|
||||
* @author Matt DuVall <http://www.mattduvall.com>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow equal signs explicitly at the beginning of regular expressions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-div-regex"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "A regular expression literal can be confused with '/='."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
|
||||
Literal(node) {
|
||||
const token = sourceCode.getFirstToken(node);
|
||||
|
||||
if (token.type === "RegularExpression" && token.value[1] === "=") {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpected",
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([token.range[0] + 1, token.range[0] + 2], "[=]");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag duplicate arguments
|
||||
* @author Jamund Ferguson
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow duplicate arguments in `function` definitions",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-dupe-args"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Duplicate param '{{name}}'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not a given definition is a parameter's.
|
||||
* @param {eslint-scope.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 = sourceCode.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,
|
||||
messageId: "unexpected",
|
||||
data: { name: variable.name }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
FunctionDeclaration: checkParams,
|
||||
FunctionExpression: checkParams
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* @fileoverview A rule to disallow duplicate name in class members.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow duplicate class members",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-dupe-class-members"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Duplicate name '{{name}}'."
|
||||
}
|
||||
},
|
||||
|
||||
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"];
|
||||
}
|
||||
|
||||
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, PropertyDefinition"(node) {
|
||||
const name = astUtils.getStaticPropertyName(node);
|
||||
const kind = node.type === "MethodDefinition" ? node.kind : "field";
|
||||
|
||||
if (name === null || kind === "constructor") {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = getState(name, node.static);
|
||||
let isDuplicate = false;
|
||||
|
||||
if (kind === "get") {
|
||||
isDuplicate = (state.init || state.get);
|
||||
state.get = true;
|
||||
} else if (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, messageId: "unexpected", data: { name } });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow duplicate conditions in if-else-if chains
|
||||
* @author Milos Djermanovic
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines whether the first given array is a subset of the second given array.
|
||||
* @param {Function} comparator A function to compare two elements, should return `true` if they are equal.
|
||||
* @param {Array} arrA The array to compare from.
|
||||
* @param {Array} arrB The array to compare against.
|
||||
* @returns {boolean} `true` if the array `arrA` is a subset of the array `arrB`.
|
||||
*/
|
||||
function isSubsetByComparator(comparator, arrA, arrB) {
|
||||
return arrA.every(a => arrB.some(b => comparator(a, b)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the given node by the given logical operator.
|
||||
* @param {string} operator Logical operator `||` or `&&`.
|
||||
* @param {ASTNode} node The node to split.
|
||||
* @returns {ASTNode[]} Array of conditions that makes the node when joined by the operator.
|
||||
*/
|
||||
function splitByLogicalOperator(operator, node) {
|
||||
if (node.type === "LogicalExpression" && node.operator === operator) {
|
||||
return [...splitByLogicalOperator(operator, node.left), ...splitByLogicalOperator(operator, node.right)];
|
||||
}
|
||||
return [node];
|
||||
}
|
||||
|
||||
const splitByOr = splitByLogicalOperator.bind(null, "||");
|
||||
const splitByAnd = splitByLogicalOperator.bind(null, "&&");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow duplicate conditions in if-else-if chains",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-dupe-else-if"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "This branch can never execute. Its condition is a duplicate or covered by previous conditions in the if-else-if chain."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Determines whether the two given nodes are considered to be equal. In particular, given that the nodes
|
||||
* represent expressions in a boolean context, `||` and `&&` can be considered as commutative operators.
|
||||
* @param {ASTNode} a First node.
|
||||
* @param {ASTNode} b Second node.
|
||||
* @returns {boolean} `true` if the nodes are considered to be equal.
|
||||
*/
|
||||
function equal(a, b) {
|
||||
if (a.type !== b.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
a.type === "LogicalExpression" &&
|
||||
(a.operator === "||" || a.operator === "&&") &&
|
||||
a.operator === b.operator
|
||||
) {
|
||||
return equal(a.left, b.left) && equal(a.right, b.right) ||
|
||||
equal(a.left, b.right) && equal(a.right, b.left);
|
||||
}
|
||||
|
||||
return astUtils.equalTokens(a, b, sourceCode);
|
||||
}
|
||||
|
||||
const isSubset = isSubsetByComparator.bind(null, equal);
|
||||
|
||||
return {
|
||||
IfStatement(node) {
|
||||
const test = node.test,
|
||||
conditionsToCheck = test.type === "LogicalExpression" && test.operator === "&&"
|
||||
? [test, ...splitByAnd(test)]
|
||||
: [test];
|
||||
let current = node,
|
||||
listToCheck = conditionsToCheck.map(c => splitByOr(c).map(splitByAnd));
|
||||
|
||||
while (current.parent && current.parent.type === "IfStatement" && current.parent.alternate === current) {
|
||||
current = current.parent;
|
||||
|
||||
const currentOrOperands = splitByOr(current.test).map(splitByAnd);
|
||||
|
||||
listToCheck = listToCheck.map(orOperands => orOperands.filter(
|
||||
orOperand => !currentOrOperands.some(currentOrOperand => isSubset(currentOrOperand, orOperand))
|
||||
));
|
||||
|
||||
if (listToCheck.some(orOperands => orOperands.length === 0)) {
|
||||
context.report({ node: test, messageId: "unexpected" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of duplicate keys in an object.
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const GET_KIND = /^(?:init|get)$/u;
|
||||
const SET_KIND = /^(?:init|set)$/u;
|
||||
|
||||
/**
|
||||
* 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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow duplicate keys in object literals",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-dupe-keys"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Duplicate key '{{name}}'."
|
||||
}
|
||||
},
|
||||
|
||||
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 === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reports if the name is defined already.
|
||||
if (info.isPropertyDefined(node)) {
|
||||
context.report({
|
||||
node: info.node,
|
||||
loc: node.key.loc,
|
||||
messageId: "unexpected",
|
||||
data: { name }
|
||||
});
|
||||
}
|
||||
|
||||
// Update info.
|
||||
info.defineProperty(node);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow a duplicate case label.
|
||||
* @author Dieter Oberkofler
|
||||
* @author Burak Yigit Kaya
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow duplicate case labels",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-duplicate-case"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Duplicate case label."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Determines whether the two given nodes are considered to be equal.
|
||||
* @param {ASTNode} a First node.
|
||||
* @param {ASTNode} b Second node.
|
||||
* @returns {boolean} `true` if the nodes are considered to be equal.
|
||||
*/
|
||||
function equal(a, b) {
|
||||
if (a.type !== b.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return astUtils.equalTokens(a, b, sourceCode);
|
||||
}
|
||||
return {
|
||||
SwitchStatement(node) {
|
||||
const previousTests = [];
|
||||
|
||||
for (const switchCase of node.cases) {
|
||||
if (switchCase.test) {
|
||||
const test = switchCase.test;
|
||||
|
||||
if (previousTests.some(previousTest => equal(previousTest, test))) {
|
||||
context.report({ node: switchCase, messageId: "unexpected" });
|
||||
} else {
|
||||
previousTests.push(test);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* @fileoverview Restrict usage of duplicate imports.
|
||||
* @author Simen Bekkhus
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const NAMED_TYPES = ["ImportSpecifier", "ExportSpecifier"];
|
||||
const NAMESPACE_TYPES = [
|
||||
"ImportNamespaceSpecifier",
|
||||
"ExportNamespaceSpecifier"
|
||||
];
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if an import/export type belongs to (ImportSpecifier|ExportSpecifier) or (ImportNamespaceSpecifier|ExportNamespaceSpecifier).
|
||||
* @param {string} importExportType An import/export type to check.
|
||||
* @param {string} type Can be "named" or "namespace"
|
||||
* @returns {boolean} True if import/export type belongs to (ImportSpecifier|ExportSpecifier) or (ImportNamespaceSpecifier|ExportNamespaceSpecifier) and false if it doesn't.
|
||||
*/
|
||||
function isImportExportSpecifier(importExportType, type) {
|
||||
const arrayToCheck = type === "named" ? NAMED_TYPES : NAMESPACE_TYPES;
|
||||
|
||||
return arrayToCheck.includes(importExportType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type of (import|export).
|
||||
* @param {ASTNode} node A node to get.
|
||||
* @returns {string} The type of the (import|export).
|
||||
*/
|
||||
function getImportExportType(node) {
|
||||
if (node.specifiers && node.specifiers.length > 0) {
|
||||
const nodeSpecifiers = node.specifiers;
|
||||
const index = nodeSpecifiers.findIndex(
|
||||
({ type }) =>
|
||||
isImportExportSpecifier(type, "named") ||
|
||||
isImportExportSpecifier(type, "namespace")
|
||||
);
|
||||
const i = index > -1 ? index : 0;
|
||||
|
||||
return nodeSpecifiers[i].type;
|
||||
}
|
||||
if (node.type === "ExportAllDeclaration") {
|
||||
if (node.exported) {
|
||||
return "ExportNamespaceSpecifier";
|
||||
}
|
||||
return "ExportAll";
|
||||
}
|
||||
return "SideEffectImport";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean indicates if two (import|export) can be merged
|
||||
* @param {ASTNode} node1 A node to check.
|
||||
* @param {ASTNode} node2 A node to check.
|
||||
* @returns {boolean} True if two (import|export) can be merged, false if they can't.
|
||||
*/
|
||||
function isImportExportCanBeMerged(node1, node2) {
|
||||
const importExportType1 = getImportExportType(node1);
|
||||
const importExportType2 = getImportExportType(node2);
|
||||
|
||||
if (
|
||||
(importExportType1 === "ExportAll" &&
|
||||
importExportType2 !== "ExportAll" &&
|
||||
importExportType2 !== "SideEffectImport") ||
|
||||
(importExportType1 !== "ExportAll" &&
|
||||
importExportType1 !== "SideEffectImport" &&
|
||||
importExportType2 === "ExportAll")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(isImportExportSpecifier(importExportType1, "namespace") &&
|
||||
isImportExportSpecifier(importExportType2, "named")) ||
|
||||
(isImportExportSpecifier(importExportType2, "namespace") &&
|
||||
isImportExportSpecifier(importExportType1, "named"))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean if we should report (import|export).
|
||||
* @param {ASTNode} node A node to be reported or not.
|
||||
* @param {[ASTNode]} previousNodes An array contains previous nodes of the module imported or exported.
|
||||
* @returns {boolean} True if the (import|export) should be reported.
|
||||
*/
|
||||
function shouldReportImportExport(node, previousNodes) {
|
||||
let i = 0;
|
||||
|
||||
while (i < previousNodes.length) {
|
||||
if (isImportExportCanBeMerged(node, previousNodes[i])) {
|
||||
return true;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array contains only nodes with declarations types equal to type.
|
||||
* @param {[{node: ASTNode, declarationType: string}]} nodes An array contains objects, each object contains a node and a declaration type.
|
||||
* @param {string} type Declaration type.
|
||||
* @returns {[ASTNode]} An array contains only nodes with declarations types equal to type.
|
||||
*/
|
||||
function getNodesByDeclarationType(nodes, type) {
|
||||
return nodes
|
||||
.filter(({ declarationType }) => declarationType === type)
|
||||
.map(({ node }) => node);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 getModule(node) {
|
||||
if (node && node.source && node.source.value) {
|
||||
return node.source.value.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the (import|export) can be merged with at least one import or one export, and reports if so.
|
||||
* @param {RuleContext} context The ESLint rule context object.
|
||||
* @param {ASTNode} node A node to get.
|
||||
* @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type.
|
||||
* @param {string} declarationType A declaration type can be an import or export.
|
||||
* @param {boolean} includeExports Whether or not to check for exports in addition to imports.
|
||||
* @returns {void} No return value.
|
||||
*/
|
||||
function checkAndReport(
|
||||
context,
|
||||
node,
|
||||
modules,
|
||||
declarationType,
|
||||
includeExports
|
||||
) {
|
||||
const module = getModule(node);
|
||||
|
||||
if (modules.has(module)) {
|
||||
const previousNodes = modules.get(module);
|
||||
const messagesIds = [];
|
||||
const importNodes = getNodesByDeclarationType(previousNodes, "import");
|
||||
let exportNodes;
|
||||
|
||||
if (includeExports) {
|
||||
exportNodes = getNodesByDeclarationType(previousNodes, "export");
|
||||
}
|
||||
if (declarationType === "import") {
|
||||
if (shouldReportImportExport(node, importNodes)) {
|
||||
messagesIds.push("import");
|
||||
}
|
||||
if (includeExports) {
|
||||
if (shouldReportImportExport(node, exportNodes)) {
|
||||
messagesIds.push("importAs");
|
||||
}
|
||||
}
|
||||
} else if (declarationType === "export") {
|
||||
if (shouldReportImportExport(node, exportNodes)) {
|
||||
messagesIds.push("export");
|
||||
}
|
||||
if (shouldReportImportExport(node, importNodes)) {
|
||||
messagesIds.push("exportAs");
|
||||
}
|
||||
}
|
||||
messagesIds.forEach(messageId =>
|
||||
context.report({
|
||||
node,
|
||||
messageId,
|
||||
data: {
|
||||
module
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback nodeCallback
|
||||
* @param {ASTNode} node A node to handle.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a function handling the (imports|exports) of a given file
|
||||
* @param {RuleContext} context The ESLint rule context object.
|
||||
* @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type.
|
||||
* @param {string} declarationType A declaration type can be an import or export.
|
||||
* @param {boolean} includeExports Whether or not to check for exports in addition to imports.
|
||||
* @returns {nodeCallback} A function passed to ESLint to handle the statement.
|
||||
*/
|
||||
function handleImportsExports(
|
||||
context,
|
||||
modules,
|
||||
declarationType,
|
||||
includeExports
|
||||
) {
|
||||
return function(node) {
|
||||
const module = getModule(node);
|
||||
|
||||
if (module) {
|
||||
checkAndReport(
|
||||
context,
|
||||
node,
|
||||
modules,
|
||||
declarationType,
|
||||
includeExports
|
||||
);
|
||||
const currentNode = { node, declarationType };
|
||||
let nodes = [currentNode];
|
||||
|
||||
if (modules.has(module)) {
|
||||
const previousNodes = modules.get(module);
|
||||
|
||||
nodes = [...previousNodes, currentNode];
|
||||
}
|
||||
modules.set(module, nodes);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow duplicate module imports",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-duplicate-imports"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
includeExports: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
import: "'{{module}}' import is duplicated.",
|
||||
importAs: "'{{module}}' import is duplicated as export.",
|
||||
export: "'{{module}}' export is duplicated.",
|
||||
exportAs: "'{{module}}' export is duplicated as import."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const includeExports = (context.options[0] || {}).includeExports,
|
||||
modules = new Map();
|
||||
const handlers = {
|
||||
ImportDeclaration: handleImportsExports(
|
||||
context,
|
||||
modules,
|
||||
"import",
|
||||
includeExports
|
||||
)
|
||||
};
|
||||
|
||||
if (includeExports) {
|
||||
handlers.ExportNamedDeclaration = handleImportsExports(
|
||||
context,
|
||||
modules,
|
||||
"export",
|
||||
includeExports
|
||||
);
|
||||
handlers.ExportAllDeclaration = handleImportsExports(
|
||||
context,
|
||||
modules,
|
||||
"export",
|
||||
includeExports
|
||||
);
|
||||
}
|
||||
return handlers;
|
||||
}
|
||||
};
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag `else` after a `return` in `if`
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
const FixTracker = require("./utils/fix-tracker");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `else` blocks after `return` statements in `if` statements",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-else-return"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowElseIf: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
fixable: "code",
|
||||
|
||||
messages: {
|
||||
unexpected: "Unnecessary 'else' after 'return'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether the given names can be safely used to declare block-scoped variables
|
||||
* in the given scope. Name collisions can produce redeclaration syntax errors,
|
||||
* or silently change references and modify behavior of the original code.
|
||||
*
|
||||
* This is not a generic function. In particular, it is assumed that the scope is a function scope or
|
||||
* a function's inner scope, and that the names can be valid identifiers in the given scope.
|
||||
* @param {string[]} names Array of variable names.
|
||||
* @param {eslint-scope.Scope} scope Function scope or a function's inner scope.
|
||||
* @returns {boolean} True if all names can be safely declared, false otherwise.
|
||||
*/
|
||||
function isSafeToDeclare(names, scope) {
|
||||
|
||||
if (names.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const functionScope = scope.variableScope;
|
||||
|
||||
/*
|
||||
* If this is a function scope, scope.variables will contain parameters, implicit variables such as "arguments",
|
||||
* all function-scoped variables ('var'), and block-scoped variables defined in the scope.
|
||||
* If this is an inner scope, scope.variables will contain block-scoped variables defined in the scope.
|
||||
*
|
||||
* Redeclaring any of these would cause a syntax error, except for the implicit variables.
|
||||
*/
|
||||
const declaredVariables = scope.variables.filter(({ defs }) => defs.length > 0);
|
||||
|
||||
if (declaredVariables.some(({ name }) => names.includes(name))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Redeclaring a catch variable would also cause a syntax error.
|
||||
if (scope !== functionScope && scope.upper.type === "catch") {
|
||||
if (scope.upper.variables.some(({ name }) => names.includes(name))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Redeclaring an implicit variable, such as "arguments", would not cause a syntax error.
|
||||
* However, if the variable was used, declaring a new one with the same name would change references
|
||||
* and modify behavior.
|
||||
*/
|
||||
const usedImplicitVariables = scope.variables.filter(({ defs, references }) =>
|
||||
defs.length === 0 && references.length > 0);
|
||||
|
||||
if (usedImplicitVariables.some(({ name }) => names.includes(name))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Declaring a variable with a name that was already used to reference a variable from an upper scope
|
||||
* would change references and modify behavior.
|
||||
*/
|
||||
if (scope.through.some(t => names.includes(t.identifier.name))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the scope is an inner scope (not the function scope), an uninitialized `var` variable declared inside
|
||||
* the scope node (directly or in one of its descendants) is neither declared nor 'through' in the scope.
|
||||
*
|
||||
* For example, this would be a syntax error "Identifier 'a' has already been declared":
|
||||
* function foo() { if (bar) { let a; if (baz) { var a; } } }
|
||||
*/
|
||||
if (scope !== functionScope) {
|
||||
const scopeNodeRange = scope.block.range;
|
||||
const variablesToCheck = functionScope.variables.filter(({ name }) => names.includes(name));
|
||||
|
||||
if (variablesToCheck.some(v => v.defs.some(({ node: { range } }) =>
|
||||
scopeNodeRange[0] <= range[0] && range[1] <= scopeNodeRange[1]))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether the removal of `else` and its braces is safe from variable name collisions.
|
||||
* @param {Node} node The 'else' node.
|
||||
* @param {eslint-scope.Scope} scope The scope in which the node and the whole 'if' statement is.
|
||||
* @returns {boolean} True if it is safe, false otherwise.
|
||||
*/
|
||||
function isSafeFromNameCollisions(node, scope) {
|
||||
|
||||
if (node.type === "FunctionDeclaration") {
|
||||
|
||||
// Conditional function declaration. Scope and hoisting are unpredictable, different engines work differently.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.type !== "BlockStatement") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const elseBlockScope = scope.childScopes.find(({ block }) => block === node);
|
||||
|
||||
if (!elseBlockScope) {
|
||||
|
||||
// ecmaVersion < 6, `else` block statement cannot have its own scope, no possible collisions.
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* elseBlockScope is supposed to merge into its upper scope. elseBlockScope.variables array contains
|
||||
* only block-scoped variables (such as let and const variables or class and function declarations)
|
||||
* defined directly in the elseBlockScope. These are exactly the only names that could cause collisions.
|
||||
*/
|
||||
const namesToCheck = elseBlockScope.variables.map(({ name }) => name);
|
||||
|
||||
return isSafeToDeclare(namesToCheck, scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the context report if rule is violated
|
||||
* @param {Node} elseNode The 'else' node
|
||||
* @returns {void}
|
||||
*/
|
||||
function displayReport(elseNode) {
|
||||
const currentScope = sourceCode.getScope(elseNode.parent);
|
||||
|
||||
context.report({
|
||||
node: elseNode,
|
||||
messageId: "unexpected",
|
||||
fix(fixer) {
|
||||
|
||||
if (!isSafeFromNameCollisions(elseNode, currentScope)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startToken = sourceCode.getFirstToken(elseNode);
|
||||
const elseToken = sourceCode.getTokenBefore(startToken);
|
||||
const source = sourceCode.getText(elseNode);
|
||||
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 = elseNode.parent.consequent.type !== "BlockStatement" && lastIfToken.value !== ";";
|
||||
const elseBlockUnsafe = /^[([/+`-]/u.test(firstTokenOfElseBlock.value);
|
||||
|
||||
if (ifBlockMaybeUnsafe && elseBlockUnsafe) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const endToken = sourceCode.getLastToken(elseNode);
|
||||
const lastTokenOfElseBlock = sourceCode.getTokenBefore(endToken);
|
||||
|
||||
if (lastTokenOfElseBlock.value !== ";") {
|
||||
const nextToken = sourceCode.getTokenAfter(endToken);
|
||||
|
||||
const nextTokenUnsafe = nextToken && /^[([/+`-]/u.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
|
||||
*
|
||||
* Also, to avoid name collisions between two else blocks.
|
||||
*/
|
||||
return new FixTracker(fixer, sourceCode)
|
||||
.retainEnclosingFunction(elseNode)
|
||||
.replaceTextRange([elseToken.range[0], elseNode.range[1]], 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.
|
||||
* @param {Node} node The node being evaluated
|
||||
* @returns {boolean} True if the node is valid
|
||||
*/
|
||||
function hasElse(node) {
|
||||
return node.alternate && node.consequent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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, but don't catch else-if blocks.
|
||||
* @returns {void}
|
||||
* @param {Node} node The node for the if statement to check
|
||||
* @private
|
||||
*/
|
||||
function checkIfWithoutElse(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
const consequents = [];
|
||||
let alternate;
|
||||
|
||||
for (let currentNode = node; currentNode.type === "IfStatement"; currentNode = currentNode.alternate) {
|
||||
if (!currentNode.alternate) {
|
||||
return;
|
||||
}
|
||||
consequents.push(currentNode.consequent);
|
||||
alternate = currentNode.alternate;
|
||||
}
|
||||
|
||||
if (consequents.every(alwaysReturns)) {
|
||||
displayReport(alternate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the if statement
|
||||
* @returns {void}
|
||||
* @param {Node} node The node for the if statement to check
|
||||
* @private
|
||||
*/
|
||||
function checkIfWithElse(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
const alternate = node.alternate;
|
||||
|
||||
if (alternate && alwaysReturns(node.consequent)) {
|
||||
displayReport(alternate);
|
||||
}
|
||||
}
|
||||
|
||||
const allowElseIf = !(context.options[0] && context.options[0].allowElseIf === false);
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
|
||||
"IfStatement:exit": allowElseIf ? checkIfWithoutElse : checkIfWithElse
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag the use of empty character classes in regular expressions
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const parser = new RegExpParser();
|
||||
const QUICK_TEST_REGEX = /\[\]/u;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow empty character classes in regular expressions",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-empty-character-class"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Empty class."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
"Literal[regex]"(node) {
|
||||
const { pattern, flags } = node.regex;
|
||||
|
||||
if (!QUICK_TEST_REGEX.test(pattern)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let regExpAST;
|
||||
|
||||
try {
|
||||
regExpAST = parser.parsePattern(pattern, 0, pattern.length, {
|
||||
unicode: flags.includes("u"),
|
||||
unicodeSets: flags.includes("v")
|
||||
});
|
||||
} catch {
|
||||
|
||||
// Ignore regular expressions that regexpp cannot parse
|
||||
return;
|
||||
}
|
||||
|
||||
visitRegExpAST(regExpAST, {
|
||||
onCharacterClassEnter(characterClass) {
|
||||
if (!characterClass.negate && characterClass.elements.length === 0) {
|
||||
context.report({ node, messageId: "unexpected" });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow empty functions.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const ALLOW_OPTIONS = Object.freeze([
|
||||
"functions",
|
||||
"arrowFunctions",
|
||||
"generatorFunctions",
|
||||
"methods",
|
||||
"generatorMethods",
|
||||
"getters",
|
||||
"setters",
|
||||
"constructors",
|
||||
"asyncFunctions",
|
||||
"asyncMethods"
|
||||
]);
|
||||
|
||||
/**
|
||||
* 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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow empty functions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-empty-function"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allow: {
|
||||
type: "array",
|
||||
items: { enum: ALLOW_OPTIONS },
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected empty {{name}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const allowed = options.allow || [];
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
const innerComments = sourceCode.getTokens(node.body, {
|
||||
includeComments: true,
|
||||
filter: astUtils.isCommentToken
|
||||
});
|
||||
|
||||
if (!allowed.includes(kind) &&
|
||||
node.body.type === "BlockStatement" &&
|
||||
node.body.body.length === 0 &&
|
||||
innerComments.length === 0
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
loc: node.body.loc,
|
||||
messageId: "unexpected",
|
||||
data: { name }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ArrowFunctionExpression: reportIfEmpty,
|
||||
FunctionDeclaration: reportIfEmpty,
|
||||
FunctionExpression: reportIfEmpty
|
||||
};
|
||||
}
|
||||
};
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow an empty pattern
|
||||
* @author Alberto Rodríguez
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow empty destructuring patterns",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-empty-pattern"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowObjectPatternsAsParameters: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected empty {{type}} pattern."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {},
|
||||
allowObjectPatternsAsParameters = options.allowObjectPatternsAsParameters || false;
|
||||
|
||||
return {
|
||||
ObjectPattern(node) {
|
||||
|
||||
if (node.properties.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow {} and {} = {} empty object patterns as parameters when allowObjectPatternsAsParameters is true
|
||||
if (
|
||||
allowObjectPatternsAsParameters &&
|
||||
(
|
||||
astUtils.isFunction(node.parent) ||
|
||||
(
|
||||
node.parent.type === "AssignmentPattern" &&
|
||||
astUtils.isFunction(node.parent.parent) &&
|
||||
node.parent.right.type === "ObjectExpression" &&
|
||||
node.parent.right.properties.length === 0
|
||||
)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({ node, messageId: "unexpected", data: { type: "object" } });
|
||||
},
|
||||
ArrayPattern(node) {
|
||||
if (node.elements.length === 0) {
|
||||
context.report({ node, messageId: "unexpected", data: { type: "array" } });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow empty static blocks.
|
||||
* @author Sosuke Suzuki
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow empty static blocks",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-empty-static-block"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected empty static block."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
StaticBlock(node) {
|
||||
if (node.body.length === 0) {
|
||||
const closingBrace = sourceCode.getLastToken(node);
|
||||
|
||||
if (sourceCode.getCommentsBefore(closingBrace).length === 0) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpected"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of an empty block statement
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
hasSuggestions: true,
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow empty block statements",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-empty"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowEmptyCatch: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpected: "Empty {{type}} statement.",
|
||||
suggestComment: "Add comment inside empty {{type}} statement."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {},
|
||||
allowEmptyCatch = options.allowEmptyCatch || false;
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
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.getCommentsInside(node).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpected",
|
||||
data: { type: "block" },
|
||||
suggest: [
|
||||
{
|
||||
messageId: "suggestComment",
|
||||
data: { type: "block" },
|
||||
fix(fixer) {
|
||||
const range = [node.range[0] + 1, node.range[1] - 1];
|
||||
|
||||
return fixer.replaceTextRange(range, " /* empty */ ");
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
|
||||
SwitchStatement(node) {
|
||||
|
||||
if (typeof node.cases === "undefined" || node.cases.length === 0) {
|
||||
context.report({ node, messageId: "unexpected", data: { type: "switch" } });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag comparisons to null without a type-checking
|
||||
* operator.
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `null` comparisons without type-checking operators",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-eq-null"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Use '===' to compare with null."
|
||||
}
|
||||
},
|
||||
|
||||
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, messageId: "unexpected" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of eval() statement
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const candidatesOfGlobalObject = Object.freeze([
|
||||
"global",
|
||||
"window",
|
||||
"globalThis"
|
||||
]);
|
||||
|
||||
/**
|
||||
* 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 astUtils.isSpecificMemberAccess(node, null, name);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow the use of `eval()`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-eval"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowIndirect: { type: "boolean", default: false }
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpected: "eval can be harmful."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const allowIndirect = Boolean(
|
||||
context.options[0] &&
|
||||
context.options[0].allowIndirect
|
||||
);
|
||||
const sourceCode = context.sourceCode;
|
||||
let funcInfo = null;
|
||||
|
||||
/**
|
||||
* Pushes a `this` scope (non-arrow function, class static block, or class field initializer) information to the stack.
|
||||
* Top-level scopes are handled separately.
|
||||
*
|
||||
* 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.
|
||||
* For functions, this is one of FunctionDeclaration, FunctionExpression.
|
||||
* For class static blocks, this is StaticBlock.
|
||||
* For class field initializers, this can be any node that is PropertyDefinition#value.
|
||||
* @returns {void}
|
||||
*/
|
||||
function enterThisScope(node) {
|
||||
const strict = sourceCode.getScope(node).isStrict;
|
||||
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
node,
|
||||
strict,
|
||||
isTopLevelOfScript: false,
|
||||
defaultThis: false,
|
||||
initialized: strict
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops a variable scope from the stack.
|
||||
* @returns {void}
|
||||
*/
|
||||
function exitThisScope() {
|
||||
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) {
|
||||
const parent = node.parent;
|
||||
const locationNode = node.type === "MemberExpression"
|
||||
? node.property
|
||||
: node;
|
||||
|
||||
const reportNode = parent.type === "CallExpression" && parent.callee === node
|
||||
? parent
|
||||
: node;
|
||||
|
||||
context.report({
|
||||
node: reportNode,
|
||||
loc: locationNode.loc,
|
||||
messageId: "unexpected"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports accesses of `eval` via the global object.
|
||||
* @param {eslint-scope.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 {eslint-scope.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;
|
||||
|
||||
/*
|
||||
* Optional call (`eval?.("code")`) is not direct eval.
|
||||
* The direct eval is only step 6.a.vi of https://tc39.es/ecma262/#sec-function-calls-runtime-semantics-evaluation
|
||||
* But the optional call is https://tc39.es/ecma262/#sec-optional-chaining-chain-evaluation
|
||||
*/
|
||||
if (!node.optional && astUtils.isSpecificId(callee, "eval")) {
|
||||
report(callee);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
"CallExpression:exit"(node) {
|
||||
const callee = node.callee;
|
||||
|
||||
if (astUtils.isSpecificId(callee, "eval")) {
|
||||
report(callee);
|
||||
}
|
||||
},
|
||||
|
||||
Program(node) {
|
||||
const scope = sourceCode.getScope(node),
|
||||
features = context.parserOptions.ecmaFeatures || {},
|
||||
strict =
|
||||
scope.isStrict ||
|
||||
node.sourceType === "module" ||
|
||||
(features.globalReturn && scope.childScopes[0].isStrict),
|
||||
isTopLevelOfScript = node.sourceType !== "module" && !features.globalReturn;
|
||||
|
||||
funcInfo = {
|
||||
upper: null,
|
||||
node,
|
||||
strict,
|
||||
isTopLevelOfScript,
|
||||
defaultThis: true,
|
||||
initialized: true
|
||||
};
|
||||
},
|
||||
|
||||
"Program:exit"(node) {
|
||||
const globalScope = sourceCode.getScope(node);
|
||||
|
||||
exitThisScope();
|
||||
reportAccessingEval(globalScope);
|
||||
reportAccessingEvalViaGlobalObject(globalScope);
|
||||
},
|
||||
|
||||
FunctionDeclaration: enterThisScope,
|
||||
"FunctionDeclaration:exit": exitThisScope,
|
||||
FunctionExpression: enterThisScope,
|
||||
"FunctionExpression:exit": exitThisScope,
|
||||
"PropertyDefinition > *.value": enterThisScope,
|
||||
"PropertyDefinition > *.value:exit": exitThisScope,
|
||||
StaticBlock: enterThisScope,
|
||||
"StaticBlock:exit": exitThisScope,
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
// `this` at the top level of scripts always refers to the global object
|
||||
if (funcInfo.isTopLevelOfScript || (!funcInfo.strict && funcInfo.defaultThis)) {
|
||||
|
||||
// `this.eval` is possible built-in `eval`.
|
||||
report(node.parent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag assignment of the exception parameter
|
||||
* @author Stephen Murray <spmurrayzzz>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow reassigning exceptions in `catch` clauses",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-ex-assign"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Do not assign to the exception parameter."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* 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, messageId: "unexpected" });
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
CatchClause(node) {
|
||||
sourceCode.getDeclaredVariables(node).forEach(checkVariable);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag adding properties to native object's prototypes.
|
||||
* @author David Nelson
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
const globals = require("globals");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow extending native types",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-extend-native"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
exceptions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
},
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpected: "{{builtin}} prototype is read only, properties should not be added."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const config = context.options[0] || {};
|
||||
const sourceCode = context.sourceCode;
|
||||
const exceptions = new Set(config.exceptions || []);
|
||||
const modifiedBuiltins = new Set(
|
||||
Object.keys(globals.builtin)
|
||||
.filter(builtin => builtin[0].toUpperCase() === builtin[0])
|
||||
.filter(builtin => !exceptions.has(builtin))
|
||||
);
|
||||
|
||||
/**
|
||||
* Reports a lint error for the given node.
|
||||
* @param {ASTNode} node The node to report.
|
||||
* @param {string} builtin The name of the native builtin being extended.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportNode(node, builtin) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpected",
|
||||
data: {
|
||||
builtin
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the `prototype` property of the given object
|
||||
* identifier node is being accessed.
|
||||
* @param {ASTNode} identifierNode The Identifier representing the object
|
||||
* to check.
|
||||
* @returns {boolean} True if the identifier is the object of a
|
||||
* MemberExpression and its `prototype` property is being accessed,
|
||||
* false otherwise.
|
||||
*/
|
||||
function isPrototypePropertyAccessed(identifierNode) {
|
||||
return Boolean(
|
||||
identifierNode &&
|
||||
identifierNode.parent &&
|
||||
identifierNode.parent.type === "MemberExpression" &&
|
||||
identifierNode.parent.object === identifierNode &&
|
||||
astUtils.getStaticPropertyName(identifierNode.parent) === "prototype"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it's an assignment to the property of the given node.
|
||||
* Example: `*.prop = 0` // the `*` is the given node.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} True if an assignment to the property of the node.
|
||||
*/
|
||||
function isAssigningToPropertyOf(node) {
|
||||
return (
|
||||
node.parent.type === "MemberExpression" &&
|
||||
node.parent.object === node &&
|
||||
node.parent.parent.type === "AssignmentExpression" &&
|
||||
node.parent.parent.left === node.parent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} True if the node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`.
|
||||
*/
|
||||
function isInDefinePropertyCall(node) {
|
||||
return (
|
||||
node.parent.type === "CallExpression" &&
|
||||
node.parent.arguments[0] === node &&
|
||||
astUtils.isSpecificMemberAccess(node.parent.callee, "Object", /^definePropert(?:y|ies)$/u)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if object prototype access is part of a prototype
|
||||
* extension. There are three ways a prototype can be extended:
|
||||
* 1. Assignment to prototype property (Object.prototype.foo = 1)
|
||||
* 2. Object.defineProperty()/Object.defineProperties() on a prototype
|
||||
* If prototype extension is detected, report the AssignmentExpression
|
||||
* or CallExpression node.
|
||||
* @param {ASTNode} identifierNode The Identifier representing the object
|
||||
* which prototype is being accessed and possibly extended.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkAndReportPrototypeExtension(identifierNode) {
|
||||
if (!isPrototypePropertyAccessed(identifierNode)) {
|
||||
return; // This is not `*.prototype` access.
|
||||
}
|
||||
|
||||
/*
|
||||
* `identifierNode.parent` is a MemberExpression `*.prototype`.
|
||||
* If it's an optional member access, it may be wrapped by a `ChainExpression` node.
|
||||
*/
|
||||
const prototypeNode =
|
||||
identifierNode.parent.parent.type === "ChainExpression"
|
||||
? identifierNode.parent.parent
|
||||
: identifierNode.parent;
|
||||
|
||||
if (isAssigningToPropertyOf(prototypeNode)) {
|
||||
|
||||
// `*.prototype` -> MemberExpression -> AssignmentExpression
|
||||
reportNode(prototypeNode.parent.parent, identifierNode.name);
|
||||
} else if (isInDefinePropertyCall(prototypeNode)) {
|
||||
|
||||
// `*.prototype` -> CallExpression
|
||||
reportNode(prototypeNode.parent, identifierNode.name);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
"Program:exit"(node) {
|
||||
const globalScope = sourceCode.getScope(node);
|
||||
|
||||
modifiedBuiltins.forEach(builtin => {
|
||||
const builtinVar = globalScope.set.get(builtin);
|
||||
|
||||
if (builtinVar && builtinVar.references) {
|
||||
builtinVar.references
|
||||
.map(ref => ref.identifier)
|
||||
.forEach(checkAndReportPrototypeExtension);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag unnecessary bind calls
|
||||
* @author Bence Dányi <bence@danyi.me>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const SIDE_EFFECT_FREE_NODE_TYPES = new Set(["Literal", "Identifier", "ThisExpression", "FunctionExpression"]);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow unnecessary calls to `.bind()`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-extra-bind"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
fixable: "code",
|
||||
|
||||
messages: {
|
||||
unexpected: "The function binding is unnecessary."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
let scopeInfo = null;
|
||||
|
||||
/**
|
||||
* Checks if a node is free of side effects.
|
||||
*
|
||||
* This check is stricter than it needs to be, in order to keep the implementation simple.
|
||||
* @param {ASTNode} node A node to check.
|
||||
* @returns {boolean} True if the node is known to be side-effect free, false otherwise.
|
||||
*/
|
||||
function isSideEffectFree(node) {
|
||||
return SIDE_EFFECT_FREE_NODE_TYPES.has(node.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given function node.
|
||||
* @param {ASTNode} node A node to report. This is a FunctionExpression or
|
||||
* an ArrowFunctionExpression.
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(node) {
|
||||
const memberNode = node.parent;
|
||||
const callNode = memberNode.parent.type === "ChainExpression"
|
||||
? memberNode.parent.parent
|
||||
: memberNode.parent;
|
||||
|
||||
context.report({
|
||||
node: callNode,
|
||||
messageId: "unexpected",
|
||||
loc: memberNode.property.loc,
|
||||
|
||||
fix(fixer) {
|
||||
if (!isSideEffectFree(callNode.arguments[0])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* The list of the first/last token pair of a removal range.
|
||||
* This is two parts because closing parentheses may exist between the method name and arguments.
|
||||
* E.g. `(function(){}.bind ) (obj)`
|
||||
* ^^^^^ ^^^^^ < removal ranges
|
||||
* E.g. `(function(){}?.['bind'] ) ?.(obj)`
|
||||
* ^^^^^^^^^^ ^^^^^^^ < removal ranges
|
||||
*/
|
||||
const tokenPairs = [
|
||||
[
|
||||
|
||||
// `.`, `?.`, or `[` token.
|
||||
sourceCode.getTokenAfter(
|
||||
memberNode.object,
|
||||
astUtils.isNotClosingParenToken
|
||||
),
|
||||
|
||||
// property name or `]` token.
|
||||
sourceCode.getLastToken(memberNode)
|
||||
],
|
||||
[
|
||||
|
||||
// `?.` or `(` token of arguments.
|
||||
sourceCode.getTokenAfter(
|
||||
memberNode,
|
||||
astUtils.isNotClosingParenToken
|
||||
),
|
||||
|
||||
// `)` token of arguments.
|
||||
sourceCode.getLastToken(callNode)
|
||||
]
|
||||
];
|
||||
const firstTokenToRemove = tokenPairs[0][0];
|
||||
const lastTokenToRemove = tokenPairs[1][1];
|
||||
|
||||
if (sourceCode.commentsExistBetween(firstTokenToRemove, lastTokenToRemove)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tokenPairs.map(([start, end]) =>
|
||||
fixer.removeRange([start.range[0], end.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) {
|
||||
if (!astUtils.isSpecificMemberAccess(node.parent, null, "bind")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The node of `*.bind` member access.
|
||||
const bindNode = node.parent.parent.type === "ChainExpression"
|
||||
? node.parent.parent
|
||||
: node.parent;
|
||||
|
||||
return (
|
||||
bindNode.parent.type === "CallExpression" &&
|
||||
bindNode.parent.callee === bindNode &&
|
||||
bindNode.parent.arguments.length === 1 &&
|
||||
bindNode.parent.arguments[0].type !== "SpreadElement"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
};
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag unnecessary double negation in Boolean contexts
|
||||
* @author Brandon Mills
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
const eslintUtils = require("@eslint-community/eslint-utils");
|
||||
|
||||
const precedence = astUtils.getPrecedence;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow unnecessary boolean casts",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-extra-boolean-cast"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
enforceForLogicalOperands: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
fixable: "code",
|
||||
|
||||
messages: {
|
||||
unexpectedCall: "Redundant Boolean call.",
|
||||
unexpectedNegation: "Redundant double negation."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
// Node types which have a test which will coerce values to booleans.
|
||||
const BOOLEAN_NODE_TYPES = new Set([
|
||||
"IfStatement",
|
||||
"DoWhileStatement",
|
||||
"WhileStatement",
|
||||
"ConditionalExpression",
|
||||
"ForStatement"
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check if a node is a Boolean function or constructor.
|
||||
* @param {ASTNode} node the node
|
||||
* @returns {boolean} If the node is Boolean function or constructor
|
||||
*/
|
||||
function isBooleanFunctionOrConstructorCall(node) {
|
||||
|
||||
// Boolean(<bool>) and new Boolean(<bool>)
|
||||
return (node.type === "CallExpression" || node.type === "NewExpression") &&
|
||||
node.callee.type === "Identifier" &&
|
||||
node.callee.name === "Boolean";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the node is a logical expression and that the option is enabled
|
||||
* @param {ASTNode} node the node
|
||||
* @returns {boolean} if the node is a logical expression and option is enabled
|
||||
*/
|
||||
function isLogicalContext(node) {
|
||||
return node.type === "LogicalExpression" &&
|
||||
(node.operator === "||" || node.operator === "&&") &&
|
||||
(context.options.length && context.options[0].enforceForLogicalOperands === true);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if a node is in a context where its value would be coerced to a boolean at runtime.
|
||||
* @param {ASTNode} node The node
|
||||
* @returns {boolean} If it is in a boolean context
|
||||
*/
|
||||
function isInBooleanContext(node) {
|
||||
return (
|
||||
(isBooleanFunctionOrConstructorCall(node.parent) &&
|
||||
node === node.parent.arguments[0]) ||
|
||||
|
||||
(BOOLEAN_NODE_TYPES.has(node.parent.type) &&
|
||||
node === node.parent.test) ||
|
||||
|
||||
// !<bool>
|
||||
(node.parent.type === "UnaryExpression" &&
|
||||
node.parent.operator === "!")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the node is a context that should report an error
|
||||
* Acts recursively if it is in a logical context
|
||||
* @param {ASTNode} node the node
|
||||
* @returns {boolean} If the node is in one of the flagged contexts
|
||||
*/
|
||||
function isInFlaggedContext(node) {
|
||||
if (node.parent.type === "ChainExpression") {
|
||||
return isInFlaggedContext(node.parent);
|
||||
}
|
||||
|
||||
return isInBooleanContext(node) ||
|
||||
(isLogicalContext(node.parent) &&
|
||||
|
||||
// For nested logical statements
|
||||
isInFlaggedContext(node.parent)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if a node has comments inside.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} `true` if it has comments inside.
|
||||
*/
|
||||
function hasCommentsInside(node) {
|
||||
return Boolean(sourceCode.getCommentsInside(node).length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given node is wrapped in grouping parentheses. Parentheses for constructs such as if() don't count.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} `true` if the node is parenthesized.
|
||||
* @private
|
||||
*/
|
||||
function isParenthesized(node) {
|
||||
return eslintUtils.isParenthesized(1, node, sourceCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given node needs to be parenthesized when replacing the previous node.
|
||||
* It assumes that `previousNode` is the node to be reported by this rule, so it has a limited list
|
||||
* of possible parent node types. By the same assumption, the node's role in a particular parent is already known.
|
||||
* For example, if the parent is `ConditionalExpression`, `previousNode` must be its `test` child.
|
||||
* @param {ASTNode} previousNode Previous node.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @throws {Error} (Unreachable.)
|
||||
* @returns {boolean} `true` if the node needs to be parenthesized.
|
||||
*/
|
||||
function needsParens(previousNode, node) {
|
||||
if (previousNode.parent.type === "ChainExpression") {
|
||||
return needsParens(previousNode.parent, node);
|
||||
}
|
||||
if (isParenthesized(previousNode)) {
|
||||
|
||||
// parentheses around the previous node will stay, so there is no need for an additional pair
|
||||
return false;
|
||||
}
|
||||
|
||||
// parent of the previous node will become parent of the replacement node
|
||||
const parent = previousNode.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
case "CallExpression":
|
||||
case "NewExpression":
|
||||
return node.type === "SequenceExpression";
|
||||
case "IfStatement":
|
||||
case "DoWhileStatement":
|
||||
case "WhileStatement":
|
||||
case "ForStatement":
|
||||
return false;
|
||||
case "ConditionalExpression":
|
||||
return precedence(node) <= precedence(parent);
|
||||
case "UnaryExpression":
|
||||
return precedence(node) < precedence(parent);
|
||||
case "LogicalExpression":
|
||||
if (astUtils.isMixedLogicalAndCoalesceExpressions(node, parent)) {
|
||||
return true;
|
||||
}
|
||||
if (previousNode === parent.left) {
|
||||
return precedence(node) < precedence(parent);
|
||||
}
|
||||
return precedence(node) <= precedence(parent);
|
||||
|
||||
/* c8 ignore next */
|
||||
default:
|
||||
throw new Error(`Unexpected parent type: ${parent.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
UnaryExpression(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
|
||||
// Exit early if it's guaranteed not to match
|
||||
if (node.operator !== "!" ||
|
||||
parent.type !== "UnaryExpression" ||
|
||||
parent.operator !== "!") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (isInFlaggedContext(parent)) {
|
||||
context.report({
|
||||
node: parent,
|
||||
messageId: "unexpectedNegation",
|
||||
fix(fixer) {
|
||||
if (hasCommentsInside(parent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (needsParens(parent, node.argument)) {
|
||||
return fixer.replaceText(parent, `(${sourceCode.getText(node.argument)})`);
|
||||
}
|
||||
|
||||
let prefix = "";
|
||||
const tokenBefore = sourceCode.getTokenBefore(parent);
|
||||
const firstReplacementToken = sourceCode.getFirstToken(node.argument);
|
||||
|
||||
if (
|
||||
tokenBefore &&
|
||||
tokenBefore.range[1] === parent.range[0] &&
|
||||
!astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken)
|
||||
) {
|
||||
prefix = " ";
|
||||
}
|
||||
|
||||
return fixer.replaceText(parent, prefix + sourceCode.getText(node.argument));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInFlaggedContext(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedCall",
|
||||
fix(fixer) {
|
||||
const parent = node.parent;
|
||||
|
||||
if (node.arguments.length === 0) {
|
||||
if (parent.type === "UnaryExpression" && parent.operator === "!") {
|
||||
|
||||
/*
|
||||
* !Boolean() -> true
|
||||
*/
|
||||
|
||||
if (hasCommentsInside(parent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const replacement = "true";
|
||||
let prefix = "";
|
||||
const tokenBefore = sourceCode.getTokenBefore(parent);
|
||||
|
||||
if (
|
||||
tokenBefore &&
|
||||
tokenBefore.range[1] === parent.range[0] &&
|
||||
!astUtils.canTokensBeAdjacent(tokenBefore, replacement)
|
||||
) {
|
||||
prefix = " ";
|
||||
}
|
||||
|
||||
return fixer.replaceText(parent, prefix + replacement);
|
||||
}
|
||||
|
||||
/*
|
||||
* Boolean() -> false
|
||||
*/
|
||||
|
||||
if (hasCommentsInside(node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fixer.replaceText(node, "false");
|
||||
}
|
||||
|
||||
if (node.arguments.length === 1) {
|
||||
const argument = node.arguments[0];
|
||||
|
||||
if (argument.type === "SpreadElement" || hasCommentsInside(node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Boolean(expression) -> expression
|
||||
*/
|
||||
|
||||
if (needsParens(node, argument)) {
|
||||
return fixer.replaceText(node, `(${sourceCode.getText(argument)})`);
|
||||
}
|
||||
|
||||
return fixer.replaceText(node, sourceCode.getText(argument));
|
||||
}
|
||||
|
||||
// two or more arguments
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow unnecessary labels
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow unnecessary labels",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-extra-label"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
fixable: "code",
|
||||
|
||||
messages: {
|
||||
unexpected: "This label '{{name}}' is unnecessary."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
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,
|
||||
messageId: "unexpected",
|
||||
data: labelNode,
|
||||
fix(fixer) {
|
||||
const breakOrContinueToken = sourceCode.getFirstToken(node);
|
||||
|
||||
if (sourceCode.commentsExistBetween(breakOrContinueToken, labelNode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fixer.removeRange([breakOrContinueToken.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
|
||||
};
|
||||
}
|
||||
};
|
||||
+1322
@@ -0,0 +1,1322 @@
|
||||
/**
|
||||
* @fileoverview Disallow parenthesising higher precedence subexpressions.
|
||||
* @author Michael Ficarra
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const { isParenthesized: isParenthesizedRaw } = require("@eslint-community/eslint-utils");
|
||||
const astUtils = require("./utils/ast-utils.js");
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Disallow unnecessary parentheses",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-extra-parens"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
schema: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["functions"]
|
||||
}
|
||||
],
|
||||
minItems: 0,
|
||||
maxItems: 1
|
||||
},
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["all"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
conditionalAssign: { type: "boolean" },
|
||||
ternaryOperandBinaryExpressions: { type: "boolean" },
|
||||
nestedBinaryExpressions: { type: "boolean" },
|
||||
returnAssign: { type: "boolean" },
|
||||
ignoreJSX: { enum: ["none", "all", "single-line", "multi-line"] },
|
||||
enforceForArrowConditionals: { type: "boolean" },
|
||||
enforceForSequenceExpressions: { type: "boolean" },
|
||||
enforceForNewInMemberExpressions: { type: "boolean" },
|
||||
enforceForFunctionPrototypeMethods: { type: "boolean" },
|
||||
allowParensAfterCommentPattern: { type: "string" }
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
minItems: 0,
|
||||
maxItems: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
messages: {
|
||||
unexpected: "Unnecessary parentheses around expression."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
const tokensToIgnore = new WeakSet();
|
||||
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 EXCEPT_COND_TERNARY = ALL_NODES && context.options[1] && context.options[1].ternaryOperandBinaryExpressions === 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 IGNORE_ARROW_CONDITIONALS = ALL_NODES && context.options[1] &&
|
||||
context.options[1].enforceForArrowConditionals === false;
|
||||
const IGNORE_SEQUENCE_EXPRESSIONS = ALL_NODES && context.options[1] &&
|
||||
context.options[1].enforceForSequenceExpressions === false;
|
||||
const IGNORE_NEW_IN_MEMBER_EXPR = ALL_NODES && context.options[1] &&
|
||||
context.options[1].enforceForNewInMemberExpressions === false;
|
||||
const IGNORE_FUNCTION_PROTOTYPE_METHODS = ALL_NODES && context.options[1] &&
|
||||
context.options[1].enforceForFunctionPrototypeMethods === false;
|
||||
const ALLOW_PARENS_AFTER_COMMENT_PATTERN = ALL_NODES && context.options[1] && context.options[1].allowParensAfterCommentPattern;
|
||||
|
||||
const PRECEDENCE_OF_ASSIGNMENT_EXPR = precedence({ type: "AssignmentExpression" });
|
||||
const PRECEDENCE_OF_UPDATE_EXPR = precedence({ type: "UpdateExpression" });
|
||||
|
||||
let reportsBuffer;
|
||||
|
||||
/**
|
||||
* Determines whether the given node is a `call` or `apply` method call, invoked directly on a `FunctionExpression` node.
|
||||
* Example: function(){}.call()
|
||||
* @param {ASTNode} node The node to be checked.
|
||||
* @returns {boolean} True if the node is an immediate `call` or `apply` method call.
|
||||
* @private
|
||||
*/
|
||||
function isImmediateFunctionPrototypeMethodCall(node) {
|
||||
const callNode = astUtils.skipChainExpression(node);
|
||||
|
||||
if (callNode.type !== "CallExpression") {
|
||||
return false;
|
||||
}
|
||||
const callee = astUtils.skipChainExpression(callNode.callee);
|
||||
|
||||
return (
|
||||
callee.type === "MemberExpression" &&
|
||||
callee.object.type === "FunctionExpression" &&
|
||||
["call", "apply"].includes(astUtils.getStaticPropertyName(callee))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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" || node.type === "JSXFragment") {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === "SequenceExpression" && IGNORE_SEQUENCE_EXPRESSIONS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isImmediateFunctionPrototypeMethodCall(node) && IGNORE_FUNCTION_PROTOTYPE_METHODS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ALL_NODES || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a node is surrounded by parentheses.
|
||||
* @param {ASTNode} node The node to be checked.
|
||||
* @returns {boolean} True if the node is parenthesised.
|
||||
* @private
|
||||
*/
|
||||
function isParenthesised(node) {
|
||||
return isParenthesizedRaw(1, node, sourceCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return isParenthesizedRaw(2, node, sourceCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 that is expected to be parenthesised is surrounded by
|
||||
* (potentially) invalid extra parentheses with considering precedence level of the node.
|
||||
* If the preference level of the node is not higher or equal to precedence lower limit, it also checks
|
||||
* whether the node is surrounded by parentheses twice or not.
|
||||
* @param {ASTNode} node The node to be checked.
|
||||
* @param {number} precedenceLowerLimit The lower limit of precedence.
|
||||
* @returns {boolean} True if the node is has an unexpected extra pair of parentheses.
|
||||
* @private
|
||||
*/
|
||||
function hasExcessParensWithPrecedence(node, precedenceLowerLimit) {
|
||||
if (ruleApplies(node) && isParenthesised(node)) {
|
||||
if (
|
||||
precedence(node) >= precedenceLowerLimit ||
|
||||
isParenthesisedTwice(node)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
for (let currentNode = node; currentNode; currentNode = currentNode.parent) {
|
||||
if (
|
||||
currentNode.type === "ReturnStatement" ||
|
||||
(currentNode.type === "ArrowFunctionExpression" && currentNode.body.type !== "BlockStatement")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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 ||
|
||||
(
|
||||
|
||||
// The expression should end with its own parens, e.g., new new foo() is not a new expression with parens
|
||||
astUtils.isOpeningParenToken(penultimateToken) &&
|
||||
astUtils.isClosingParenToken(lastToken) &&
|
||||
newExpression.callee.range[1] < newExpression.range[1]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
if (node.type === "ConditionalExpression" &&
|
||||
(node.consequent.type === "AssignmentExpression" || node.alternate.type === "AssignmentExpression")) {
|
||||
return true;
|
||||
}
|
||||
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);
|
||||
}
|
||||
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(leftParenToken, { includeComments: true });
|
||||
const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParenToken, { includeComments: true });
|
||||
|
||||
return tokenBeforeLeftParen &&
|
||||
tokenBeforeLeftParen.range[1] === leftParenToken.range[0] &&
|
||||
leftParenToken.range[1] === tokenAfterLeftParen.range[0] &&
|
||||
!astUtils.canTokensBeAdjacent(tokenBeforeLeftParen, tokenAfterLeftParen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a node should be followed 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 after the node
|
||||
* @private
|
||||
*/
|
||||
function requiresTrailingSpace(node) {
|
||||
const nextTwoTokens = sourceCode.getTokensAfter(node, { count: 2 });
|
||||
const rightParenToken = nextTwoTokens[0];
|
||||
const tokenAfterRightParen = nextTwoTokens[1];
|
||||
const tokenBeforeRightParen = sourceCode.getLastToken(node);
|
||||
|
||||
return rightParenToken && tokenAfterRightParen &&
|
||||
!sourceCode.isSpaceBetweenTokens(rightParenToken, tokenAfterRightParen) &&
|
||||
!astUtils.canTokensBeAdjacent(tokenBeforeRightParen, tokenAfterRightParen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given expression node is an IIFE
|
||||
* @param {ASTNode} node The node to check
|
||||
* @returns {boolean} `true` if the given node is an IIFE
|
||||
*/
|
||||
function isIIFE(node) {
|
||||
const maybeCallNode = astUtils.skipChainExpression(node);
|
||||
|
||||
return maybeCallNode.type === "CallExpression" && maybeCallNode.callee.type === "FunctionExpression";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given node can be the assignment target in destructuring or the LHS of an assignment.
|
||||
* This is to avoid an autofix that could change behavior because parsers mistakenly allow invalid syntax,
|
||||
* such as `(a = b) = c` and `[(a = b) = c] = []`. Ideally, this function shouldn't be necessary.
|
||||
* @param {ASTNode} [node] The node to check
|
||||
* @returns {boolean} `true` if the given node can be a valid assignment target
|
||||
*/
|
||||
function canBeAssignmentTarget(node) {
|
||||
return node && (node.type === "Identifier" || node.type === "MemberExpression");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node is fixable.
|
||||
* A node is fixable if removing a single pair of surrounding parentheses does not turn it
|
||||
* into a directive after fixing other nodes.
|
||||
* Almost all nodes are fixable, except if all of the following conditions are met:
|
||||
* The node is a string Literal
|
||||
* It has a single pair of parentheses
|
||||
* It is the only child of an ExpressionStatement
|
||||
* @param {ASTNode} node The node to evaluate.
|
||||
* @returns {boolean} Whether or not the node is fixable.
|
||||
* @private
|
||||
*/
|
||||
function isFixable(node) {
|
||||
|
||||
// if it's not a string literal it can be autofixed
|
||||
if (node.type !== "Literal" || typeof node.value !== "string") {
|
||||
return true;
|
||||
}
|
||||
if (isParenthesisedTwice(node)) {
|
||||
return true;
|
||||
}
|
||||
return !astUtils.isTopLevelExpressionStatement(node.parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (!isParenthesisedTwice(node)) {
|
||||
if (tokensToIgnore.has(sourceCode.getFirstToken(node))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isIIFE(node) && !isParenthesised(node.callee)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ALLOW_PARENS_AFTER_COMMENT_PATTERN) {
|
||||
const commentsBeforeLeftParenToken = sourceCode.getCommentsBefore(leftParenToken);
|
||||
const totalCommentsBeforeLeftParenTokenCount = commentsBeforeLeftParenToken.length;
|
||||
const ignorePattern = new RegExp(ALLOW_PARENS_AFTER_COMMENT_PATTERN, "u");
|
||||
|
||||
if (
|
||||
totalCommentsBeforeLeftParenTokenCount > 0 &&
|
||||
ignorePattern.test(commentsBeforeLeftParenToken[totalCommentsBeforeLeftParenTokenCount - 1].value)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes reporting
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function finishReport() {
|
||||
context.report({
|
||||
node,
|
||||
loc: leftParenToken.loc,
|
||||
messageId: "unexpected",
|
||||
fix: isFixable(node)
|
||||
? fixer => {
|
||||
const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0]);
|
||||
|
||||
return fixer.replaceTextRange([
|
||||
leftParenToken.range[0],
|
||||
rightParenToken.range[1]
|
||||
], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource + (requiresTrailingSpace(node) ? " " : ""));
|
||||
}
|
||||
: null
|
||||
});
|
||||
}
|
||||
|
||||
if (reportsBuffer) {
|
||||
reportsBuffer.reports.push({ node, finishReport });
|
||||
return;
|
||||
}
|
||||
|
||||
finishReport();
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a argument of the node.
|
||||
* @param {ASTNode} node node to evaluate
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkArgumentWithPrecedence(node) {
|
||||
if (hasExcessParensWithPrecedence(node.argument, precedence(node))) {
|
||||
report(node.argument);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a member expression contains a call expression
|
||||
* @param {ASTNode} node MemberExpression node to evaluate
|
||||
* @returns {boolean} true if found, false if not
|
||||
*/
|
||||
function doesMemberExpressionContainCallExpression(node) {
|
||||
let currentNode = node.object;
|
||||
let currentNodeType = node.object.type;
|
||||
|
||||
while (currentNodeType === "MemberExpression") {
|
||||
currentNode = currentNode.object;
|
||||
currentNodeType = currentNode.type;
|
||||
}
|
||||
|
||||
return currentNodeType === "CallExpression";
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a new call
|
||||
* @param {ASTNode} node node to evaluate
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkCallNew(node) {
|
||||
const callee = node.callee;
|
||||
|
||||
if (hasExcessParensWithPrecedence(callee, precedence(node))) {
|
||||
if (
|
||||
hasDoubleExcessParens(callee) ||
|
||||
!(
|
||||
isIIFE(node) ||
|
||||
|
||||
// (new A)(); new (new A)();
|
||||
(
|
||||
callee.type === "NewExpression" &&
|
||||
!isNewExpressionWithParens(callee) &&
|
||||
!(
|
||||
node.type === "NewExpression" &&
|
||||
!isNewExpressionWithParens(node)
|
||||
)
|
||||
) ||
|
||||
|
||||
// new (a().b)(); new (a.b().c);
|
||||
(
|
||||
node.type === "NewExpression" &&
|
||||
callee.type === "MemberExpression" &&
|
||||
doesMemberExpressionContainCallExpression(callee)
|
||||
) ||
|
||||
|
||||
// (a?.b)(); (a?.())();
|
||||
(
|
||||
!node.optional &&
|
||||
callee.type === "ChainExpression"
|
||||
)
|
||||
)
|
||||
) {
|
||||
report(node.callee);
|
||||
}
|
||||
}
|
||||
node.arguments
|
||||
.filter(arg => hasExcessParensWithPrecedence(arg, PRECEDENCE_OF_ASSIGNMENT_EXPR))
|
||||
.forEach(report);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
const shouldSkipRight = NESTED_BINARY && (node.right.type === "BinaryExpression" || node.right.type === "LogicalExpression");
|
||||
|
||||
if (!shouldSkipLeft && hasExcessParens(node.left)) {
|
||||
if (
|
||||
!(["AwaitExpression", "UnaryExpression"].includes(node.left.type) && isExponentiation) &&
|
||||
!astUtils.isMixedLogicalAndCoalesceExpressions(node.left, node) &&
|
||||
(leftPrecedence > prec || (leftPrecedence === prec && !isExponentiation)) ||
|
||||
isParenthesisedTwice(node.left)
|
||||
) {
|
||||
report(node.left);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldSkipRight && hasExcessParens(node.right)) {
|
||||
if (
|
||||
!astUtils.isMixedLogicalAndCoalesceExpressions(node.right, node) &&
|
||||
(rightPrecedence > prec || (rightPrecedence === prec && isExponentiation)) ||
|
||||
isParenthesisedTwice(node.right)
|
||||
) {
|
||||
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) {
|
||||
if (hasExcessParensWithPrecedence(node.argument, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
|
||||
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);
|
||||
const thirdToken = secondToken ? sourceCode.getTokenAfter(secondToken) : null;
|
||||
const tokenAfterClosingParens = secondToken ? sourceCode.getTokenAfter(secondToken, astUtils.isNotClosingParenToken) : null;
|
||||
|
||||
if (
|
||||
astUtils.isOpeningParenToken(firstToken) &&
|
||||
(
|
||||
astUtils.isOpeningBraceToken(secondToken) ||
|
||||
secondToken.type === "Keyword" && (
|
||||
secondToken.value === "function" ||
|
||||
secondToken.value === "class" ||
|
||||
secondToken.value === "let" &&
|
||||
tokenAfterClosingParens &&
|
||||
(
|
||||
astUtils.isOpeningBracketToken(tokenAfterClosingParens) ||
|
||||
tokenAfterClosingParens.type === "Identifier"
|
||||
)
|
||||
) ||
|
||||
secondToken && secondToken.type === "Identifier" && secondToken.value === "async" && thirdToken && thirdToken.type === "Keyword" && thirdToken.value === "function"
|
||||
)
|
||||
) {
|
||||
tokensToIgnore.add(secondToken);
|
||||
}
|
||||
|
||||
const hasExtraParens = node.parent.type === "ExportDefaultDeclaration"
|
||||
? hasExcessParensWithPrecedence(node, PRECEDENCE_OF_ASSIGNMENT_EXPR)
|
||||
: hasExcessParens(node);
|
||||
|
||||
if (hasExtraParens) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path from the given node to the specified ancestor.
|
||||
* @param {ASTNode} node First node in the path.
|
||||
* @param {ASTNode} ancestor Last node in the path.
|
||||
* @returns {ASTNode[]} Path, including both nodes.
|
||||
* @throws {Error} If the given node does not have the specified ancestor.
|
||||
*/
|
||||
function pathToAncestor(node, ancestor) {
|
||||
const path = [node];
|
||||
let currentNode = node;
|
||||
|
||||
while (currentNode !== ancestor) {
|
||||
|
||||
currentNode = currentNode.parent;
|
||||
|
||||
/* c8 ignore start */
|
||||
if (currentNode === null) {
|
||||
throw new Error("Nodes are not in the ancestor-descendant relationship.");
|
||||
}/* c8 ignore stop */
|
||||
|
||||
path.push(currentNode);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path from the given node to the specified descendant.
|
||||
* @param {ASTNode} node First node in the path.
|
||||
* @param {ASTNode} descendant Last node in the path.
|
||||
* @returns {ASTNode[]} Path, including both nodes.
|
||||
* @throws {Error} If the given node does not have the specified descendant.
|
||||
*/
|
||||
function pathToDescendant(node, descendant) {
|
||||
return pathToAncestor(descendant, node).reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the syntax of the given ancestor of an 'in' expression inside a for-loop initializer
|
||||
* is preventing the 'in' keyword from being interpreted as a part of an ill-formed for-in loop.
|
||||
* @param {ASTNode} node Ancestor of an 'in' expression.
|
||||
* @param {ASTNode} child Child of the node, ancestor of the same 'in' expression or the 'in' expression itself.
|
||||
* @returns {boolean} True if the keyword 'in' would be interpreted as the 'in' operator, without any parenthesis.
|
||||
*/
|
||||
function isSafelyEnclosingInExpression(node, child) {
|
||||
switch (node.type) {
|
||||
case "ArrayExpression":
|
||||
case "ArrayPattern":
|
||||
case "BlockStatement":
|
||||
case "ObjectExpression":
|
||||
case "ObjectPattern":
|
||||
case "TemplateLiteral":
|
||||
return true;
|
||||
case "ArrowFunctionExpression":
|
||||
case "FunctionExpression":
|
||||
return node.params.includes(child);
|
||||
case "CallExpression":
|
||||
case "NewExpression":
|
||||
return node.arguments.includes(child);
|
||||
case "MemberExpression":
|
||||
return node.computed && node.property === child;
|
||||
case "ConditionalExpression":
|
||||
return node.consequent === child;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new reports buffering. Warnings will be stored in a buffer instead of being reported immediately.
|
||||
* An additional logic that requires multiple nodes (e.g. a whole subtree) may dismiss some of the stored warnings.
|
||||
* @returns {void}
|
||||
*/
|
||||
function startNewReportsBuffering() {
|
||||
reportsBuffer = {
|
||||
upper: reportsBuffer,
|
||||
inExpressionNodes: [],
|
||||
reports: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the current reports buffering.
|
||||
* @returns {void}
|
||||
*/
|
||||
function endCurrentReportsBuffering() {
|
||||
const { upper, inExpressionNodes, reports } = reportsBuffer;
|
||||
|
||||
if (upper) {
|
||||
upper.inExpressionNodes.push(...inExpressionNodes);
|
||||
upper.reports.push(...reports);
|
||||
} else {
|
||||
|
||||
// flush remaining reports
|
||||
reports.forEach(({ finishReport }) => finishReport());
|
||||
}
|
||||
|
||||
reportsBuffer = upper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given node is in the current reports buffer.
|
||||
* @param {ASTNode} node Node to check.
|
||||
* @returns {boolean} True if the node is in the current buffer, false otherwise.
|
||||
*/
|
||||
function isInCurrentReportsBuffer(node) {
|
||||
return reportsBuffer.reports.some(r => r.node === node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given node from the current reports buffer.
|
||||
* @param {ASTNode} node Node to remove.
|
||||
* @returns {void}
|
||||
*/
|
||||
function removeFromCurrentReportsBuffer(node) {
|
||||
reportsBuffer.reports = reportsBuffer.reports.filter(r => r.node !== node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a node is a MemberExpression at NewExpression's callee.
|
||||
* @param {ASTNode} node node to check.
|
||||
* @returns {boolean} True if the node is a MemberExpression at NewExpression's callee. false otherwise.
|
||||
*/
|
||||
function isMemberExpInNewCallee(node) {
|
||||
if (node.type === "MemberExpression") {
|
||||
return node.parent.type === "NewExpression" && node.parent.callee === node
|
||||
? true
|
||||
: node.parent.object === node && isMemberExpInNewCallee(node.parent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the left-hand side of an assignment is an identifier, the operator is one of
|
||||
* `=`, `&&=`, `||=` or `??=` and the right-hand side is an anonymous class or function.
|
||||
*
|
||||
* As per https://tc39.es/ecma262/#sec-assignment-operators-runtime-semantics-evaluation, an
|
||||
* assignment involving one of the operators `=`, `&&=`, `||=` or `??=` where the right-hand
|
||||
* side is an anonymous class or function and the left-hand side is an *unparenthesized*
|
||||
* identifier has different semantics than other assignments.
|
||||
* Specifically, when an expression like `foo = function () {}` is evaluated, `foo.name`
|
||||
* will be set to the string "foo", i.e. the identifier name. The same thing does not happen
|
||||
* when evaluating `(foo) = function () {}`.
|
||||
* Since the parenthesizing of the identifier in the left-hand side is significant in this
|
||||
* special case, the parentheses, if present, should not be flagged as unnecessary.
|
||||
* @param {ASTNode} node an AssignmentExpression node.
|
||||
* @returns {boolean} `true` if the left-hand side of the assignment is an identifier, the
|
||||
* operator is one of `=`, `&&=`, `||=` or `??=` and the right-hand side is an anonymous
|
||||
* class or function; otherwise, `false`.
|
||||
*/
|
||||
function isAnonymousFunctionAssignmentException({ left, operator, right }) {
|
||||
if (left.type === "Identifier" && ["=", "&&=", "||=", "??="].includes(operator)) {
|
||||
const rhsType = right.type;
|
||||
|
||||
if (rhsType === "ArrowFunctionExpression") {
|
||||
return true;
|
||||
}
|
||||
if ((rhsType === "FunctionExpression" || rhsType === "ClassExpression") && !right.id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
ArrayExpression(node) {
|
||||
node.elements
|
||||
.filter(e => e && hasExcessParensWithPrecedence(e, PRECEDENCE_OF_ASSIGNMENT_EXPR))
|
||||
.forEach(report);
|
||||
},
|
||||
|
||||
ArrayPattern(node) {
|
||||
node.elements
|
||||
.filter(e => canBeAssignmentTarget(e) && hasExcessParens(e))
|
||||
.forEach(report);
|
||||
},
|
||||
|
||||
ArrowFunctionExpression(node) {
|
||||
if (isReturnAssignException(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.body.type === "ConditionalExpression" &&
|
||||
IGNORE_ARROW_CONDITIONALS
|
||||
) {
|
||||
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 (hasExcessParensWithPrecedence(node.body, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
|
||||
report(node.body);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
AssignmentExpression(node) {
|
||||
if (canBeAssignmentTarget(node.left) && hasExcessParens(node.left) &&
|
||||
(!isAnonymousFunctionAssignmentException(node) || isParenthesisedTwice(node.left))) {
|
||||
report(node.left);
|
||||
}
|
||||
|
||||
if (!isReturnAssignException(node) && hasExcessParensWithPrecedence(node.right, precedence(node))) {
|
||||
report(node.right);
|
||||
}
|
||||
},
|
||||
|
||||
BinaryExpression(node) {
|
||||
if (reportsBuffer && node.operator === "in") {
|
||||
reportsBuffer.inExpressionNodes.push(node);
|
||||
}
|
||||
|
||||
checkBinaryLogical(node);
|
||||
},
|
||||
|
||||
CallExpression: checkCallNew,
|
||||
|
||||
ConditionalExpression(node) {
|
||||
if (isReturnAssignException(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const availableTypes = new Set(["BinaryExpression", "LogicalExpression"]);
|
||||
|
||||
if (
|
||||
!(EXCEPT_COND_TERNARY && availableTypes.has(node.test.type)) &&
|
||||
!isCondAssignException(node) &&
|
||||
hasExcessParensWithPrecedence(node.test, precedence({ type: "LogicalExpression", operator: "||" }))
|
||||
) {
|
||||
report(node.test);
|
||||
}
|
||||
|
||||
if (
|
||||
!(EXCEPT_COND_TERNARY && availableTypes.has(node.consequent.type)) &&
|
||||
hasExcessParensWithPrecedence(node.consequent, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
|
||||
report(node.consequent);
|
||||
}
|
||||
|
||||
if (
|
||||
!(EXCEPT_COND_TERNARY && availableTypes.has(node.alternate.type)) &&
|
||||
hasExcessParensWithPrecedence(node.alternate, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
|
||||
report(node.alternate);
|
||||
}
|
||||
},
|
||||
|
||||
DoWhileStatement(node) {
|
||||
if (hasExcessParens(node.test) && !isCondAssignException(node)) {
|
||||
report(node.test);
|
||||
}
|
||||
},
|
||||
|
||||
ExportDefaultDeclaration: node => checkExpressionOrExportStatement(node.declaration),
|
||||
ExpressionStatement: node => checkExpressionOrExportStatement(node.expression),
|
||||
|
||||
ForInStatement(node) {
|
||||
if (node.left.type !== "VariableDeclaration") {
|
||||
const firstLeftToken = sourceCode.getFirstToken(node.left, astUtils.isNotOpeningParenToken);
|
||||
|
||||
if (
|
||||
firstLeftToken.value === "let" &&
|
||||
astUtils.isOpeningBracketToken(
|
||||
sourceCode.getTokenAfter(firstLeftToken, astUtils.isNotClosingParenToken)
|
||||
)
|
||||
) {
|
||||
|
||||
// ForInStatement#left expression cannot start with `let[`.
|
||||
tokensToIgnore.add(firstLeftToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasExcessParens(node.left)) {
|
||||
report(node.left);
|
||||
}
|
||||
|
||||
if (hasExcessParens(node.right)) {
|
||||
report(node.right);
|
||||
}
|
||||
},
|
||||
|
||||
ForOfStatement(node) {
|
||||
if (node.left.type !== "VariableDeclaration") {
|
||||
const firstLeftToken = sourceCode.getFirstToken(node.left, astUtils.isNotOpeningParenToken);
|
||||
|
||||
if (firstLeftToken.value === "let") {
|
||||
|
||||
// ForOfStatement#left expression cannot start with `let`.
|
||||
tokensToIgnore.add(firstLeftToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasExcessParens(node.left)) {
|
||||
report(node.left);
|
||||
}
|
||||
|
||||
if (hasExcessParensWithPrecedence(node.right, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
|
||||
report(node.right);
|
||||
}
|
||||
},
|
||||
|
||||
ForStatement(node) {
|
||||
if (node.test && hasExcessParens(node.test) && !isCondAssignException(node)) {
|
||||
report(node.test);
|
||||
}
|
||||
|
||||
if (node.update && hasExcessParens(node.update)) {
|
||||
report(node.update);
|
||||
}
|
||||
|
||||
if (node.init) {
|
||||
|
||||
if (node.init.type !== "VariableDeclaration") {
|
||||
const firstToken = sourceCode.getFirstToken(node.init, astUtils.isNotOpeningParenToken);
|
||||
|
||||
if (
|
||||
firstToken.value === "let" &&
|
||||
astUtils.isOpeningBracketToken(
|
||||
sourceCode.getTokenAfter(firstToken, astUtils.isNotClosingParenToken)
|
||||
)
|
||||
) {
|
||||
|
||||
// ForStatement#init expression cannot start with `let[`.
|
||||
tokensToIgnore.add(firstToken);
|
||||
}
|
||||
}
|
||||
|
||||
startNewReportsBuffering();
|
||||
|
||||
if (hasExcessParens(node.init)) {
|
||||
report(node.init);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"ForStatement > *.init:exit"(node) {
|
||||
|
||||
/*
|
||||
* Removing parentheses around `in` expressions might change semantics and cause errors.
|
||||
*
|
||||
* For example, this valid for loop:
|
||||
* for (let a = (b in c); ;);
|
||||
* after removing parentheses would be treated as an invalid for-in loop:
|
||||
* for (let a = b in c; ;);
|
||||
*/
|
||||
|
||||
if (reportsBuffer.reports.length) {
|
||||
reportsBuffer.inExpressionNodes.forEach(inExpressionNode => {
|
||||
const path = pathToDescendant(node, inExpressionNode);
|
||||
let nodeToExclude;
|
||||
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const pathNode = path[i];
|
||||
|
||||
if (i < path.length - 1) {
|
||||
const nextPathNode = path[i + 1];
|
||||
|
||||
if (isSafelyEnclosingInExpression(pathNode, nextPathNode)) {
|
||||
|
||||
// The 'in' expression in safely enclosed by the syntax of its ancestor nodes (e.g. by '{}' or '[]').
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isParenthesised(pathNode)) {
|
||||
if (isInCurrentReportsBuffer(pathNode)) {
|
||||
|
||||
// This node was supposed to be reported, but parentheses might be necessary.
|
||||
|
||||
if (isParenthesisedTwice(pathNode)) {
|
||||
|
||||
/*
|
||||
* This node is parenthesised twice, it certainly has at least one pair of `extra` parentheses.
|
||||
* If the --fix option is on, the current fixing iteration will remove only one pair of parentheses.
|
||||
* The remaining pair is safely enclosing the 'in' expression.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
// Exclude the outermost node only.
|
||||
if (!nodeToExclude) {
|
||||
nodeToExclude = pathNode;
|
||||
}
|
||||
|
||||
// Don't break the loop here, there might be some safe nodes or parentheses that will stay inside.
|
||||
|
||||
} else {
|
||||
|
||||
// This node will stay parenthesised, the 'in' expression in safely enclosed by '()'.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude the node from the list (i.e. treat parentheses as necessary)
|
||||
removeFromCurrentReportsBuffer(nodeToExclude);
|
||||
});
|
||||
}
|
||||
|
||||
endCurrentReportsBuffering();
|
||||
},
|
||||
|
||||
IfStatement(node) {
|
||||
if (hasExcessParens(node.test) && !isCondAssignException(node)) {
|
||||
report(node.test);
|
||||
}
|
||||
},
|
||||
|
||||
ImportExpression(node) {
|
||||
const { source } = node;
|
||||
|
||||
if (source.type === "SequenceExpression") {
|
||||
if (hasDoubleExcessParens(source)) {
|
||||
report(source);
|
||||
}
|
||||
} else if (hasExcessParens(source)) {
|
||||
report(source);
|
||||
}
|
||||
},
|
||||
|
||||
LogicalExpression: checkBinaryLogical,
|
||||
|
||||
MemberExpression(node) {
|
||||
const shouldAllowWrapOnce = isMemberExpInNewCallee(node) &&
|
||||
doesMemberExpressionContainCallExpression(node);
|
||||
const nodeObjHasExcessParens = shouldAllowWrapOnce
|
||||
? hasDoubleExcessParens(node.object)
|
||||
: hasExcessParens(node.object) &&
|
||||
!(
|
||||
isImmediateFunctionPrototypeMethodCall(node.parent) &&
|
||||
node.parent.callee === node &&
|
||||
IGNORE_FUNCTION_PROTOTYPE_METHODS
|
||||
);
|
||||
|
||||
if (
|
||||
nodeObjHasExcessParens &&
|
||||
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 (nodeObjHasExcessParens &&
|
||||
node.object.type === "CallExpression"
|
||||
) {
|
||||
report(node.object);
|
||||
}
|
||||
|
||||
if (nodeObjHasExcessParens &&
|
||||
!IGNORE_NEW_IN_MEMBER_EXPR &&
|
||||
node.object.type === "NewExpression" &&
|
||||
isNewExpressionWithParens(node.object)) {
|
||||
report(node.object);
|
||||
}
|
||||
|
||||
if (nodeObjHasExcessParens &&
|
||||
node.optional &&
|
||||
node.object.type === "ChainExpression"
|
||||
) {
|
||||
report(node.object);
|
||||
}
|
||||
|
||||
if (node.computed && hasExcessParens(node.property)) {
|
||||
report(node.property);
|
||||
}
|
||||
},
|
||||
|
||||
"MethodDefinition[computed=true]"(node) {
|
||||
if (hasExcessParensWithPrecedence(node.key, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
|
||||
report(node.key);
|
||||
}
|
||||
},
|
||||
|
||||
NewExpression: checkCallNew,
|
||||
|
||||
ObjectExpression(node) {
|
||||
node.properties
|
||||
.filter(property => property.value && hasExcessParensWithPrecedence(property.value, PRECEDENCE_OF_ASSIGNMENT_EXPR))
|
||||
.forEach(property => report(property.value));
|
||||
},
|
||||
|
||||
ObjectPattern(node) {
|
||||
node.properties
|
||||
.filter(property => {
|
||||
const value = property.value;
|
||||
|
||||
return canBeAssignmentTarget(value) && hasExcessParens(value);
|
||||
}).forEach(property => report(property.value));
|
||||
},
|
||||
|
||||
Property(node) {
|
||||
if (node.computed) {
|
||||
const { key } = node;
|
||||
|
||||
if (key && hasExcessParensWithPrecedence(key, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
|
||||
report(key);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
PropertyDefinition(node) {
|
||||
if (node.computed && hasExcessParensWithPrecedence(node.key, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
|
||||
report(node.key);
|
||||
}
|
||||
|
||||
if (node.value && hasExcessParensWithPrecedence(node.value, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
|
||||
report(node.value);
|
||||
}
|
||||
},
|
||||
|
||||
RestElement(node) {
|
||||
const argument = node.argument;
|
||||
|
||||
if (canBeAssignmentTarget(argument) && hasExcessParens(argument)) {
|
||||
report(argument);
|
||||
}
|
||||
},
|
||||
|
||||
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) {
|
||||
const precedenceOfNode = precedence(node);
|
||||
|
||||
node.expressions
|
||||
.filter(e => hasExcessParensWithPrecedence(e, precedenceOfNode))
|
||||
.forEach(report);
|
||||
},
|
||||
|
||||
SwitchCase(node) {
|
||||
if (node.test && hasExcessParens(node.test)) {
|
||||
report(node.test);
|
||||
}
|
||||
},
|
||||
|
||||
SwitchStatement(node) {
|
||||
if (hasExcessParens(node.discriminant)) {
|
||||
report(node.discriminant);
|
||||
}
|
||||
},
|
||||
|
||||
ThrowStatement(node) {
|
||||
const throwToken = sourceCode.getFirstToken(node);
|
||||
|
||||
if (hasExcessParensNoLineTerminator(throwToken, node.argument)) {
|
||||
report(node.argument);
|
||||
}
|
||||
},
|
||||
|
||||
UnaryExpression: checkArgumentWithPrecedence,
|
||||
UpdateExpression(node) {
|
||||
if (node.prefix) {
|
||||
checkArgumentWithPrecedence(node);
|
||||
} else {
|
||||
const { argument } = node;
|
||||
const operatorToken = sourceCode.getLastToken(node);
|
||||
|
||||
if (argument.loc.end.line === operatorToken.loc.start.line) {
|
||||
checkArgumentWithPrecedence(node);
|
||||
} else {
|
||||
if (hasDoubleExcessParens(argument)) {
|
||||
report(argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
AwaitExpression: checkArgumentWithPrecedence,
|
||||
|
||||
VariableDeclarator(node) {
|
||||
if (
|
||||
node.init && hasExcessParensWithPrecedence(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 (hasExcessParens(node.test) && !isCondAssignException(node)) {
|
||||
report(node.test);
|
||||
}
|
||||
},
|
||||
|
||||
WithStatement(node) {
|
||||
if (hasExcessParens(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,
|
||||
|
||||
TemplateLiteral(node) {
|
||||
node.expressions
|
||||
.filter(e => e && hasExcessParens(e))
|
||||
.forEach(report);
|
||||
},
|
||||
|
||||
AssignmentPattern(node) {
|
||||
const { left, right } = node;
|
||||
|
||||
if (canBeAssignmentTarget(left) && hasExcessParens(left)) {
|
||||
report(left);
|
||||
}
|
||||
|
||||
if (right && hasExcessParensWithPrecedence(right, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
|
||||
report(right);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of unnecessary semicolons
|
||||
* @author Nicholas C. Zakas
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const FixTracker = require("./utils/fix-tracker");
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow unnecessary semicolons",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-extra-semi"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpected: "Unnecessary semicolon."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Checks if a node or token is fixable.
|
||||
* A node is fixable if it can be removed without turning a subsequent statement into a directive after fixing other nodes.
|
||||
* @param {Token} nodeOrToken The node or token to check.
|
||||
* @returns {boolean} Whether or not the node is fixable.
|
||||
*/
|
||||
function isFixable(nodeOrToken) {
|
||||
const nextToken = sourceCode.getTokenAfter(nodeOrToken);
|
||||
|
||||
if (!nextToken || nextToken.type !== "String") {
|
||||
return true;
|
||||
}
|
||||
const stringNode = sourceCode.getNodeByRangeIndex(nextToken.range[0]);
|
||||
|
||||
return !astUtils.isTopLevelExpressionStatement(stringNode.parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
messageId: "unexpected",
|
||||
fix: isFixable(nodeOrToken)
|
||||
? fixer =>
|
||||
|
||||
/*
|
||||
* Expand the replacement range to include the surrounding
|
||||
* tokens to avoid conflicting with semi.
|
||||
* https://github.com/eslint/eslint/issues/7928
|
||||
*/
|
||||
new FixTracker(fixer, context.sourceCode)
|
||||
.retainSurroundingTokens(nodeOrToken)
|
||||
.remove(nodeOrToken)
|
||||
: null
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.includes(parent.type)) {
|
||||
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, PropertyDefinition, StaticBlock"(node) {
|
||||
checkForPartOfClassBody(sourceCode.getTokenAfter(node));
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag fall-through cases in switch statements.
|
||||
* @author Matt DuVall <http://mattduvall.com/>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const { directivesPattern } = require("../shared/directives");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/iu;
|
||||
|
||||
/**
|
||||
* Checks all segments in a set and returns true if any are reachable.
|
||||
* @param {Set<CodePathSegment>} segments The segments to check.
|
||||
* @returns {boolean} True if any segment is reachable; false otherwise.
|
||||
*/
|
||||
function isAnySegmentReachable(segments) {
|
||||
|
||||
for (const segment of segments) {
|
||||
if (segment.reachable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given comment string is really a fallthrough comment and not an ESLint directive.
|
||||
* @param {string} comment The comment string to check.
|
||||
* @param {RegExp} fallthroughCommentPattern The regular expression used for checking for fallthrough comments.
|
||||
* @returns {boolean} `true` if the comment string is truly a fallthrough comment.
|
||||
*/
|
||||
function isFallThroughComment(comment, fallthroughCommentPattern) {
|
||||
return fallthroughCommentPattern.test(comment) && !directivesPattern.test(comment.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given case has a fallthrough comment.
|
||||
* @param {ASTNode} caseWhichFallsThrough SwitchCase node which falls through.
|
||||
* @param {ASTNode} subsequentCase The case after caseWhichFallsThrough.
|
||||
* @param {RuleContext} context A rule context which stores comments.
|
||||
* @param {RegExp} fallthroughCommentPattern A pattern to match comment to.
|
||||
* @returns {boolean} `true` if the case has a valid fallthrough comment.
|
||||
*/
|
||||
function hasFallthroughComment(caseWhichFallsThrough, subsequentCase, context, fallthroughCommentPattern) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
if (caseWhichFallsThrough.consequent.length === 1 && caseWhichFallsThrough.consequent[0].type === "BlockStatement") {
|
||||
const trailingCloseBrace = sourceCode.getLastToken(caseWhichFallsThrough.consequent[0]);
|
||||
const commentInBlock = sourceCode.getCommentsBefore(trailingCloseBrace).pop();
|
||||
|
||||
if (commentInBlock && isFallThroughComment(commentInBlock.value, fallthroughCommentPattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const comment = sourceCode.getCommentsBefore(subsequentCase).pop();
|
||||
|
||||
return Boolean(comment && isFallThroughComment(comment.value, fallthroughCommentPattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow fallthrough of `case` statements",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-fallthrough"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
commentPattern: {
|
||||
type: "string",
|
||||
default: ""
|
||||
},
|
||||
allowEmptyCase: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
case: "Expected a 'break' statement before 'case'.",
|
||||
default: "Expected a 'break' statement before 'default'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const codePathSegments = [];
|
||||
let currentCodePathSegments = new Set();
|
||||
const sourceCode = context.sourceCode;
|
||||
const allowEmptyCase = options.allowEmptyCase || false;
|
||||
|
||||
/*
|
||||
* 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, "u");
|
||||
} else {
|
||||
fallthroughCommentPattern = DEFAULT_FALLTHROUGH_COMMENT;
|
||||
}
|
||||
return {
|
||||
|
||||
onCodePathStart() {
|
||||
codePathSegments.push(currentCodePathSegments);
|
||||
currentCodePathSegments = new Set();
|
||||
},
|
||||
|
||||
onCodePathEnd() {
|
||||
currentCodePathSegments = codePathSegments.pop();
|
||||
},
|
||||
|
||||
onUnreachableCodePathSegmentStart(segment) {
|
||||
currentCodePathSegments.add(segment);
|
||||
},
|
||||
|
||||
onUnreachableCodePathSegmentEnd(segment) {
|
||||
currentCodePathSegments.delete(segment);
|
||||
},
|
||||
|
||||
onCodePathSegmentStart(segment) {
|
||||
currentCodePathSegments.add(segment);
|
||||
},
|
||||
|
||||
onCodePathSegmentEnd(segment) {
|
||||
currentCodePathSegments.delete(segment);
|
||||
},
|
||||
|
||||
|
||||
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(fallthroughCase, node, context, fallthroughCommentPattern))) {
|
||||
context.report({
|
||||
messageId: 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 (isAnySegmentReachable(currentCodePathSegments) &&
|
||||
(node.consequent.length > 0 || (!allowEmptyCase && hasBlankLinesBetween(node, nextToken))) &&
|
||||
node.parent.cases[node.parent.cases.length - 1] !== node) {
|
||||
fallthroughCase = node;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of a leading/trailing decimal point in a numeric literal
|
||||
* @author James Allardice
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow leading or trailing decimal points in numeric literals",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-floating-decimal"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
fixable: "code",
|
||||
messages: {
|
||||
leading: "A leading decimal point can be confused with a dot.",
|
||||
trailing: "A trailing decimal point can be confused with a dot."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
Literal(node) {
|
||||
|
||||
if (typeof node.value === "number") {
|
||||
if (node.raw.startsWith(".")) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "leading",
|
||||
fix(fixer) {
|
||||
const tokenBefore = sourceCode.getTokenBefore(node);
|
||||
const needsSpaceBefore = tokenBefore &&
|
||||
tokenBefore.range[1] === node.range[0] &&
|
||||
!astUtils.canTokensBeAdjacent(tokenBefore, `0${node.raw}`);
|
||||
|
||||
return fixer.insertTextBefore(node, needsSpaceBefore ? " 0" : "0");
|
||||
}
|
||||
});
|
||||
}
|
||||
if (node.raw.indexOf(".") === node.raw.length - 1) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "trailing",
|
||||
fix: fixer => fixer.insertTextAfter(node, "0")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of function declaration identifiers as variables.
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow reassigning `function` declarations",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-func-assign"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
isAFunction: "'{{name}}' is a function."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Reports a reference if is non initializer and writable.
|
||||
* @param {References} references Collection of reference to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkReference(references) {
|
||||
astUtils.getModifyingReferences(references).forEach(reference => {
|
||||
context.report({
|
||||
node: reference.identifier,
|
||||
messageId: "isAFunction",
|
||||
data: {
|
||||
name: reference.identifier.name
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and reports references that are non initializer and writable.
|
||||
* @param {Variable} variable A variable to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkVariable(variable) {
|
||||
if (variable.defs[0].type === "FunctionName") {
|
||||
checkReference(variable.references);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks parameters of a given function node.
|
||||
* @param {ASTNode} node A function node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkForFunction(node) {
|
||||
sourceCode.getDeclaredVariables(node).forEach(checkVariable);
|
||||
}
|
||||
|
||||
return {
|
||||
FunctionDeclaration: checkForFunction,
|
||||
FunctionExpression: checkForFunction
|
||||
};
|
||||
}
|
||||
};
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow assignments to native objects or read-only global variables
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow assignments to native objects or read-only global variables",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-global-assign"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
exceptions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
globalShouldNotBeModified: "Read-only global '{{name}}' should not be modified."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0];
|
||||
const sourceCode = context.sourceCode;
|
||||
const exceptions = (config && config.exceptions) || [];
|
||||
|
||||
/**
|
||||
* Reports write references.
|
||||
* @param {Reference} reference A reference to check.
|
||||
* @param {int} index The index of the reference in the references.
|
||||
* @param {Reference[]} references The array that the reference belongs to.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkReference(reference, index, references) {
|
||||
const identifier = reference.identifier;
|
||||
|
||||
if (reference.init === false &&
|
||||
reference.isWrite() &&
|
||||
|
||||
/*
|
||||
* Destructuring assignments can have multiple default value,
|
||||
* so possibly there are multiple writeable references for the same identifier.
|
||||
*/
|
||||
(index === 0 || references[index - 1].identifier !== identifier)
|
||||
) {
|
||||
context.report({
|
||||
node: identifier,
|
||||
messageId: "globalShouldNotBeModified",
|
||||
data: {
|
||||
name: identifier.name
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports write references if a given variable is read-only builtin.
|
||||
* @param {Variable} variable A variable to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkVariable(variable) {
|
||||
if (variable.writeable === false && !exceptions.includes(variable.name)) {
|
||||
variable.references.forEach(checkReference);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
const globalScope = sourceCode.getScope(node);
|
||||
|
||||
globalScope.variables.forEach(checkVariable);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* @fileoverview A rule to disallow the type conversions with shorter notations.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/u;
|
||||
const ALLOWABLE_OPERATORS = ["~", "!!", "+", "*"];
|
||||
|
||||
/**
|
||||
* Parses and normalizes an option object.
|
||||
* @param {Object} options An option object to parse.
|
||||
* @returns {Object} The parsed and normalized option object.
|
||||
*/
|
||||
function parseOptions(options) {
|
||||
return {
|
||||
boolean: "boolean" in options ? options.boolean : true,
|
||||
number: "number" in options ? options.number : true,
|
||||
string: "string" in options ? options.string : true,
|
||||
disallowTemplateShorthand: "disallowTemplateShorthand" in options ? options.disallowTemplateShorthand : false,
|
||||
allow: options.allow || []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a node is a double logical negating.
|
||||
* @param {ASTNode} node An UnaryExpression node to check.
|
||||
* @returns {boolean} Whether or not the node is a double logical negating.
|
||||
*/
|
||||
function isDoubleLogicalNegating(node) {
|
||||
return (
|
||||
node.operator === "!" &&
|
||||
node.argument.type === "UnaryExpression" &&
|
||||
node.argument.operator === "!"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a node is a binary negating of `.indexOf()` method calling.
|
||||
* @param {ASTNode} node An UnaryExpression node to check.
|
||||
* @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling.
|
||||
*/
|
||||
function isBinaryNegatingOfIndexOf(node) {
|
||||
if (node.operator !== "~") {
|
||||
return false;
|
||||
}
|
||||
const callNode = astUtils.skipChainExpression(node.argument);
|
||||
|
||||
return (
|
||||
callNode.type === "CallExpression" &&
|
||||
astUtils.isSpecificMemberAccess(callNode.callee, null, INDEX_OF_PATTERN)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a node is a multiplying by one.
|
||||
* @param {BinaryExpression} node A BinaryExpression node to check.
|
||||
* @returns {boolean} Whether or not the node is a multiplying by one.
|
||||
*/
|
||||
function isMultiplyByOne(node) {
|
||||
return node.operator === "*" && (
|
||||
node.left.type === "Literal" && node.left.value === 1 ||
|
||||
node.right.type === "Literal" && node.right.value === 1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given node logically represents multiplication by a fraction of `1`.
|
||||
* For example, `a * 1` in `a * 1 / b` is technically multiplication by `1`, but the
|
||||
* whole expression can be logically interpreted as `a * (1 / b)` rather than `(a * 1) / b`.
|
||||
* @param {BinaryExpression} node A BinaryExpression node to check.
|
||||
* @param {SourceCode} sourceCode The source code object.
|
||||
* @returns {boolean} Whether or not the node is a multiplying by a fraction of `1`.
|
||||
*/
|
||||
function isMultiplyByFractionOfOne(node, sourceCode) {
|
||||
return node.type === "BinaryExpression" &&
|
||||
node.operator === "*" &&
|
||||
(node.right.type === "Literal" && node.right.value === 1) &&
|
||||
node.parent.type === "BinaryExpression" &&
|
||||
node.parent.operator === "/" &&
|
||||
node.parent.left === node &&
|
||||
!astUtils.isParenthesised(sourceCode, node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the result of a node is numeric or not
|
||||
* @param {ASTNode} node The node to test
|
||||
* @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call
|
||||
*/
|
||||
function isNumeric(node) {
|
||||
return (
|
||||
node.type === "Literal" && typeof node.value === "number" ||
|
||||
node.type === "CallExpression" && (
|
||||
node.callee.name === "Number" ||
|
||||
node.callee.name === "parseInt" ||
|
||||
node.callee.name === "parseFloat"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first non-numeric operand in a BinaryExpression. Designed to be
|
||||
* used from bottom to up since it walks up the BinaryExpression trees using
|
||||
* node.parent to find the result.
|
||||
* @param {BinaryExpression} node The BinaryExpression node to be walked up on
|
||||
* @returns {ASTNode|null} The first non-numeric item in the BinaryExpression tree or null
|
||||
*/
|
||||
function getNonNumericOperand(node) {
|
||||
const left = node.left,
|
||||
right = node.right;
|
||||
|
||||
if (right.type !== "BinaryExpression" && !isNumeric(right)) {
|
||||
return right;
|
||||
}
|
||||
|
||||
if (left.type !== "BinaryExpression" && !isNumeric(left)) {
|
||||
return left;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an expression evaluates to a string.
|
||||
* @param {ASTNode} node node that represents the expression to check.
|
||||
* @returns {boolean} Whether or not the expression evaluates to a string.
|
||||
*/
|
||||
function isStringType(node) {
|
||||
return astUtils.isStringLiteral(node) ||
|
||||
(
|
||||
node.type === "CallExpression" &&
|
||||
node.callee.type === "Identifier" &&
|
||||
node.callee.name === "String"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a node is an empty string literal or not.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} Whether or not the passed in node is an
|
||||
* empty string literal or not.
|
||||
*/
|
||||
function isEmptyString(node) {
|
||||
return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a node is a concatenating with an empty string.
|
||||
* @param {ASTNode} node A BinaryExpression node to check.
|
||||
* @returns {boolean} Whether or not the node is a concatenating with an empty string.
|
||||
*/
|
||||
function isConcatWithEmptyString(node) {
|
||||
return node.operator === "+" && (
|
||||
(isEmptyString(node.left) && !isStringType(node.right)) ||
|
||||
(isEmptyString(node.right) && !isStringType(node.left))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a node is appended with an empty string.
|
||||
* @param {ASTNode} node An AssignmentExpression node to check.
|
||||
* @returns {boolean} Whether or not the node is appended with an empty string.
|
||||
*/
|
||||
function isAppendEmptyString(node) {
|
||||
return node.operator === "+=" && isEmptyString(node.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operand that is not an empty string from a flagged BinaryExpression.
|
||||
* @param {ASTNode} node The flagged BinaryExpression node to check.
|
||||
* @returns {ASTNode} The operand that is not an empty string from a flagged BinaryExpression.
|
||||
*/
|
||||
function getNonEmptyOperand(node) {
|
||||
return isEmptyString(node.left) ? node.right : node.left;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow shorthand type conversions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-implicit-coercion"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
boolean: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
},
|
||||
number: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
},
|
||||
string: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
},
|
||||
disallowTemplateShorthand: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
allow: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: ALLOWABLE_OPERATORS
|
||||
},
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
useRecommendation: "use `{{recommendation}}` instead."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = parseOptions(context.options[0] || {});
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Reports an error and autofixes the node
|
||||
* @param {ASTNode} node An ast node to report the error on.
|
||||
* @param {string} recommendation The recommended code for the issue
|
||||
* @param {bool} shouldFix Whether this report should fix the node
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(node, recommendation, shouldFix) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useRecommendation",
|
||||
data: {
|
||||
recommendation
|
||||
},
|
||||
fix(fixer) {
|
||||
if (!shouldFix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenBefore = sourceCode.getTokenBefore(node);
|
||||
|
||||
if (
|
||||
tokenBefore &&
|
||||
tokenBefore.range[1] === node.range[0] &&
|
||||
!astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
|
||||
) {
|
||||
return fixer.replaceText(node, ` ${recommendation}`);
|
||||
}
|
||||
return fixer.replaceText(node, recommendation);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
UnaryExpression(node) {
|
||||
let operatorAllowed;
|
||||
|
||||
// !!foo
|
||||
operatorAllowed = options.allow.includes("!!");
|
||||
if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
|
||||
const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
|
||||
|
||||
report(node, recommendation, true);
|
||||
}
|
||||
|
||||
// ~foo.indexOf(bar)
|
||||
operatorAllowed = options.allow.includes("~");
|
||||
if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
|
||||
|
||||
// `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
|
||||
const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
|
||||
const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
|
||||
|
||||
report(node, recommendation, false);
|
||||
}
|
||||
|
||||
// +foo
|
||||
operatorAllowed = options.allow.includes("+");
|
||||
if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
|
||||
const recommendation = `Number(${sourceCode.getText(node.argument)})`;
|
||||
|
||||
report(node, recommendation, true);
|
||||
}
|
||||
},
|
||||
|
||||
// Use `:exit` to prevent double reporting
|
||||
"BinaryExpression:exit"(node) {
|
||||
let operatorAllowed;
|
||||
|
||||
// 1 * foo
|
||||
operatorAllowed = options.allow.includes("*");
|
||||
const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && !isMultiplyByFractionOfOne(node, sourceCode) &&
|
||||
getNonNumericOperand(node);
|
||||
|
||||
if (nonNumericOperand) {
|
||||
const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
|
||||
|
||||
report(node, recommendation, true);
|
||||
}
|
||||
|
||||
// "" + foo
|
||||
operatorAllowed = options.allow.includes("+");
|
||||
if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
|
||||
const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
|
||||
|
||||
report(node, recommendation, true);
|
||||
}
|
||||
},
|
||||
|
||||
AssignmentExpression(node) {
|
||||
|
||||
// foo += ""
|
||||
const operatorAllowed = options.allow.includes("+");
|
||||
|
||||
if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
|
||||
const code = sourceCode.getText(getNonEmptyOperand(node));
|
||||
const recommendation = `${code} = String(${code})`;
|
||||
|
||||
report(node, recommendation, true);
|
||||
}
|
||||
},
|
||||
|
||||
TemplateLiteral(node) {
|
||||
if (!options.disallowTemplateShorthand) {
|
||||
return;
|
||||
}
|
||||
|
||||
// tag`${foo}`
|
||||
if (node.parent.type === "TaggedTemplateExpression") {
|
||||
return;
|
||||
}
|
||||
|
||||
// `` or `${foo}${bar}`
|
||||
if (node.expressions.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// `prefix${foo}`
|
||||
if (node.quasis[0].value.cooked !== "") {
|
||||
return;
|
||||
}
|
||||
|
||||
// `${foo}postfix`
|
||||
if (node.quasis[1].value.cooked !== "") {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the expression is already a string, then this isn't a coercion
|
||||
if (isStringType(node.expressions[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const code = sourceCode.getText(node.expressions[0]);
|
||||
const recommendation = `String(${code})`;
|
||||
|
||||
report(node, recommendation, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* @fileoverview Rule to check for implicit global variables, functions and classes.
|
||||
* @author Joshua Peek
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow declarations in the global scope",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-implicit-globals"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
lexicalBindings: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
globalNonLexicalBinding: "Unexpected {{kind}} declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable.",
|
||||
globalLexicalBinding: "Unexpected {{kind}} declaration in the global scope, wrap in a block or in an IIFE.",
|
||||
globalVariableLeak: "Global variable leak, declare the variable if it is intended to be local.",
|
||||
assignmentToReadonlyGlobal: "Unexpected assignment to read-only global variable.",
|
||||
redeclarationOfReadonlyGlobal: "Unexpected redeclaration of read-only global variable."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const checkLexicalBindings = context.options[0] && context.options[0].lexicalBindings === true;
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Reports the node.
|
||||
* @param {ASTNode} node Node to report.
|
||||
* @param {string} messageId Id of the message to report.
|
||||
* @param {string|undefined} kind Declaration kind, can be 'var', 'const', 'let', function or class.
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(node, messageId, kind) {
|
||||
context.report({
|
||||
node,
|
||||
messageId,
|
||||
data: {
|
||||
kind
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
|
||||
scope.variables.forEach(variable => {
|
||||
|
||||
// Only ESLint global variables have the `writable` key.
|
||||
const isReadonlyEslintGlobalVariable = variable.writeable === false;
|
||||
const isWritableEslintGlobalVariable = variable.writeable === true;
|
||||
|
||||
if (isWritableEslintGlobalVariable) {
|
||||
|
||||
// Everything is allowed with writable ESLint global variables.
|
||||
return;
|
||||
}
|
||||
|
||||
// Variables exported by "exported" block comments
|
||||
if (variable.eslintExported) {
|
||||
return;
|
||||
}
|
||||
|
||||
variable.defs.forEach(def => {
|
||||
const defNode = def.node;
|
||||
|
||||
if (def.type === "FunctionName" || (def.type === "Variable" && def.parent.kind === "var")) {
|
||||
if (isReadonlyEslintGlobalVariable) {
|
||||
report(defNode, "redeclarationOfReadonlyGlobal");
|
||||
} else {
|
||||
report(
|
||||
defNode,
|
||||
"globalNonLexicalBinding",
|
||||
def.type === "FunctionName" ? "function" : `'${def.parent.kind}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (checkLexicalBindings) {
|
||||
if (def.type === "ClassName" ||
|
||||
(def.type === "Variable" && (def.parent.kind === "let" || def.parent.kind === "const"))) {
|
||||
if (isReadonlyEslintGlobalVariable) {
|
||||
report(defNode, "redeclarationOfReadonlyGlobal");
|
||||
} else {
|
||||
report(
|
||||
defNode,
|
||||
"globalLexicalBinding",
|
||||
def.type === "ClassName" ? "class" : `'${def.parent.kind}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Undeclared assigned variables.
|
||||
scope.implicit.variables.forEach(variable => {
|
||||
const scopeVariable = scope.set.get(variable.name);
|
||||
let messageId;
|
||||
|
||||
if (scopeVariable) {
|
||||
|
||||
// ESLint global variable
|
||||
if (scopeVariable.writeable) {
|
||||
return;
|
||||
}
|
||||
messageId = "assignmentToReadonlyGlobal";
|
||||
|
||||
} else {
|
||||
|
||||
// Reference to an unknown variable, possible global leak.
|
||||
messageId = "globalVariableLeak";
|
||||
}
|
||||
|
||||
// def.node is an AssignmentExpression, ForInStatement or ForOfStatement.
|
||||
variable.defs.forEach(def => {
|
||||
report(def.node, messageId);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of implied eval via setTimeout and setInterval
|
||||
* @author James Allardice
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
const { getStaticValue } = require("@eslint-community/eslint-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow the use of `eval()`-like methods",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-implied-eval"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
impliedEval: "Implied eval. Consider passing a function instead of a string."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const GLOBAL_CANDIDATES = Object.freeze(["global", "window", "globalThis"]);
|
||||
const EVAL_LIKE_FUNC_PATTERN = /^(?:set(?:Interval|Timeout)|execScript)$/u;
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Checks whether a node is evaluated as a string or not.
|
||||
* @param {ASTNode} node A node to check.
|
||||
* @returns {boolean} True if the node is evaluated as a string.
|
||||
*/
|
||||
function isEvaluatedString(node) {
|
||||
if (
|
||||
(node.type === "Literal" && typeof node.value === "string") ||
|
||||
node.type === "TemplateLiteral"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (node.type === "BinaryExpression" && node.operator === "+") {
|
||||
return isEvaluatedString(node.left) || isEvaluatedString(node.right);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports if the `CallExpression` node has evaluated argument.
|
||||
* @param {ASTNode} node A CallExpression to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportImpliedEvalCallExpression(node) {
|
||||
const [firstArgument] = node.arguments;
|
||||
|
||||
if (firstArgument) {
|
||||
|
||||
const staticValue = getStaticValue(firstArgument, sourceCode.getScope(node));
|
||||
const isStaticString = staticValue && typeof staticValue.value === "string";
|
||||
const isString = isStaticString || isEvaluatedString(firstArgument);
|
||||
|
||||
if (isString) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "impliedEval"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports calls of `implied eval` via the global references.
|
||||
* @param {Variable} globalVar A global variable to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportImpliedEvalViaGlobal(globalVar) {
|
||||
const { references, name } = globalVar;
|
||||
|
||||
references.forEach(ref => {
|
||||
const identifier = ref.identifier;
|
||||
let node = identifier.parent;
|
||||
|
||||
while (astUtils.isSpecificMemberAccess(node, null, name)) {
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
if (astUtils.isSpecificMemberAccess(node, null, EVAL_LIKE_FUNC_PATTERN)) {
|
||||
const calleeNode = node.parent.type === "ChainExpression" ? node.parent : node;
|
||||
const parent = calleeNode.parent;
|
||||
|
||||
if (parent.type === "CallExpression" && parent.callee === calleeNode) {
|
||||
reportImpliedEvalCallExpression(parent);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (astUtils.isSpecificId(node.callee, EVAL_LIKE_FUNC_PATTERN)) {
|
||||
reportImpliedEvalCallExpression(node);
|
||||
}
|
||||
},
|
||||
"Program:exit"(node) {
|
||||
const globalScope = sourceCode.getScope(node);
|
||||
|
||||
GLOBAL_CANDIDATES
|
||||
.map(candidate => astUtils.getVariableByName(globalScope, candidate))
|
||||
.filter(globalVar => !!globalVar && globalVar.defs.length === 0)
|
||||
.forEach(reportImpliedEvalViaGlobal);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag updates of imported bindings.
|
||||
* @author Toru Nagashima <https://github.com/mysticatea>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const { findVariable } = require("@eslint-community/eslint-utils");
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
const WellKnownMutationFunctions = {
|
||||
Object: /^(?:assign|definePropert(?:y|ies)|freeze|setPrototypeOf)$/u,
|
||||
Reflect: /^(?:(?:define|delete)Property|set(?:PrototypeOf)?)$/u
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a given node is LHS of an assignment node.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} `true` if the node is LHS.
|
||||
*/
|
||||
function isAssignmentLeft(node) {
|
||||
const { parent } = node;
|
||||
|
||||
return (
|
||||
(
|
||||
parent.type === "AssignmentExpression" &&
|
||||
parent.left === node
|
||||
) ||
|
||||
|
||||
// Destructuring assignments
|
||||
parent.type === "ArrayPattern" ||
|
||||
(
|
||||
parent.type === "Property" &&
|
||||
parent.value === node &&
|
||||
parent.parent.type === "ObjectPattern"
|
||||
) ||
|
||||
parent.type === "RestElement" ||
|
||||
(
|
||||
parent.type === "AssignmentPattern" &&
|
||||
parent.left === node
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given node is the operand of mutation unary operator.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} `true` if the node is the operand of mutation unary operator.
|
||||
*/
|
||||
function isOperandOfMutationUnaryOperator(node) {
|
||||
const argumentNode = node.parent.type === "ChainExpression"
|
||||
? node.parent
|
||||
: node;
|
||||
const { parent } = argumentNode;
|
||||
|
||||
return (
|
||||
(
|
||||
parent.type === "UpdateExpression" &&
|
||||
parent.argument === argumentNode
|
||||
) ||
|
||||
(
|
||||
parent.type === "UnaryExpression" &&
|
||||
parent.operator === "delete" &&
|
||||
parent.argument === argumentNode
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given node is the iteration variable of `for-in`/`for-of` syntax.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} `true` if the node is the iteration variable.
|
||||
*/
|
||||
function isIterationVariable(node) {
|
||||
const { parent } = node;
|
||||
|
||||
return (
|
||||
(
|
||||
parent.type === "ForInStatement" &&
|
||||
parent.left === node
|
||||
) ||
|
||||
(
|
||||
parent.type === "ForOfStatement" &&
|
||||
parent.left === node
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given node is at the first argument of a well-known mutation function.
|
||||
* - `Object.assign`
|
||||
* - `Object.defineProperty`
|
||||
* - `Object.defineProperties`
|
||||
* - `Object.freeze`
|
||||
* - `Object.setPrototypeOf`
|
||||
* - `Reflect.defineProperty`
|
||||
* - `Reflect.deleteProperty`
|
||||
* - `Reflect.set`
|
||||
* - `Reflect.setPrototypeOf`
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @param {Scope} scope A `escope.Scope` object to find variable (whichever).
|
||||
* @returns {boolean} `true` if the node is at the first argument of a well-known mutation function.
|
||||
*/
|
||||
function isArgumentOfWellKnownMutationFunction(node, scope) {
|
||||
const { parent } = node;
|
||||
|
||||
if (parent.type !== "CallExpression" || parent.arguments[0] !== node) {
|
||||
return false;
|
||||
}
|
||||
const callee = astUtils.skipChainExpression(parent.callee);
|
||||
|
||||
if (
|
||||
!astUtils.isSpecificMemberAccess(callee, "Object", WellKnownMutationFunctions.Object) &&
|
||||
!astUtils.isSpecificMemberAccess(callee, "Reflect", WellKnownMutationFunctions.Reflect)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const variable = findVariable(scope, callee.object);
|
||||
|
||||
return variable !== null && variable.scope.type === "global";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the identifier node is placed at to update members.
|
||||
* @param {ASTNode} id The Identifier node to check.
|
||||
* @param {Scope} scope A `escope.Scope` object to find variable (whichever).
|
||||
* @returns {boolean} `true` if the member of `id` was updated.
|
||||
*/
|
||||
function isMemberWrite(id, scope) {
|
||||
const { parent } = id;
|
||||
|
||||
return (
|
||||
(
|
||||
parent.type === "MemberExpression" &&
|
||||
parent.object === id &&
|
||||
(
|
||||
isAssignmentLeft(parent) ||
|
||||
isOperandOfMutationUnaryOperator(parent) ||
|
||||
isIterationVariable(parent)
|
||||
)
|
||||
) ||
|
||||
isArgumentOfWellKnownMutationFunction(id, scope)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mutation node.
|
||||
* @param {ASTNode} id The Identifier node to get.
|
||||
* @returns {ASTNode} The mutation node.
|
||||
*/
|
||||
function getWriteNode(id) {
|
||||
let node = id.parent;
|
||||
|
||||
while (
|
||||
node &&
|
||||
node.type !== "AssignmentExpression" &&
|
||||
node.type !== "UpdateExpression" &&
|
||||
node.type !== "UnaryExpression" &&
|
||||
node.type !== "CallExpression" &&
|
||||
node.type !== "ForInStatement" &&
|
||||
node.type !== "ForOfStatement"
|
||||
) {
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
return node || id;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow assigning to imported bindings",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-import-assign"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
readonly: "'{{name}}' is read-only.",
|
||||
readonlyMember: "The members of '{{name}}' are read-only."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
|
||||
for (const variable of sourceCode.getDeclaredVariables(node)) {
|
||||
const shouldCheckMembers = variable.defs.some(
|
||||
d => d.node.type === "ImportNamespaceSpecifier"
|
||||
);
|
||||
let prevIdNode = null;
|
||||
|
||||
for (const reference of variable.references) {
|
||||
const idNode = reference.identifier;
|
||||
|
||||
/*
|
||||
* AssignmentPattern (e.g. `[a = 0] = b`) makes two write
|
||||
* references for the same identifier. This should skip
|
||||
* the one of the two in order to prevent redundant reports.
|
||||
*/
|
||||
if (idNode === prevIdNode) {
|
||||
continue;
|
||||
}
|
||||
prevIdNode = idNode;
|
||||
|
||||
if (reference.isWrite()) {
|
||||
context.report({
|
||||
node: getWriteNode(idNode),
|
||||
messageId: "readonly",
|
||||
data: { name: idNode.name }
|
||||
});
|
||||
} else if (shouldCheckMembers && isMemberWrite(idNode, scope)) {
|
||||
context.report({
|
||||
node: getWriteNode(idNode),
|
||||
messageId: "readonlyMember",
|
||||
data: { name: idNode.name }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @fileoverview Enforces or disallows inline comments.
|
||||
* @author Greg Cochard
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow inline comments after code",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-inline-comments"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
ignorePattern: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedInlineComment: "Unexpected comment inline with code."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const options = context.options[0];
|
||||
let customIgnoreRegExp;
|
||||
|
||||
if (options && options.ignorePattern) {
|
||||
customIgnoreRegExp = new RegExp(options.ignorePattern, "u");
|
||||
}
|
||||
|
||||
/**
|
||||
* Will check that comments are not on lines starting with or ending with code
|
||||
* @param {ASTNode} node The comment node to check
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function testCodeAroundComment(node) {
|
||||
|
||||
const startLine = String(sourceCode.lines[node.loc.start.line - 1]),
|
||||
endLine = String(sourceCode.lines[node.loc.end.line - 1]),
|
||||
preamble = startLine.slice(0, node.loc.start.column).trim(),
|
||||
postamble = endLine.slice(node.loc.end.column).trim(),
|
||||
isPreambleEmpty = !preamble,
|
||||
isPostambleEmpty = !postamble;
|
||||
|
||||
// Nothing on both sides
|
||||
if (isPreambleEmpty && isPostambleEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Matches the ignore pattern
|
||||
if (customIgnoreRegExp && customIgnoreRegExp.test(node.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// JSX Exception
|
||||
if (
|
||||
(isPreambleEmpty || preamble === "{") &&
|
||||
(isPostambleEmpty || postamble === "}")
|
||||
) {
|
||||
const enclosingNode = sourceCode.getNodeByRangeIndex(node.range[0]);
|
||||
|
||||
if (enclosingNode && enclosingNode.type === "JSXEmptyExpression") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't report ESLint directive comments
|
||||
if (astUtils.isDirectiveComment(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedInlineComment"
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program() {
|
||||
sourceCode.getAllComments()
|
||||
.filter(token => token.type !== "Shebang")
|
||||
.forEach(testCodeAroundComment);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce declarations in program or function body root.
|
||||
* @author Brandon Mills
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const validParent = new Set(["Program", "StaticBlock", "ExportNamedDeclaration", "ExportDefaultDeclaration"]);
|
||||
const validBlockStatementParent = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
|
||||
|
||||
/**
|
||||
* Finds the nearest enclosing context where this rule allows declarations and returns its description.
|
||||
* @param {ASTNode} node Node to search from.
|
||||
* @returns {string} Description. One of "program", "function body", "class static block body".
|
||||
*/
|
||||
function getAllowedBodyDescription(node) {
|
||||
let { parent } = node;
|
||||
|
||||
while (parent) {
|
||||
|
||||
if (parent.type === "StaticBlock") {
|
||||
return "class static block body";
|
||||
}
|
||||
|
||||
if (astUtils.isFunction(parent)) {
|
||||
return "function body";
|
||||
}
|
||||
|
||||
({ parent } = parent);
|
||||
}
|
||||
|
||||
return "program";
|
||||
}
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow variable or `function` declarations in nested blocks",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-inner-declarations"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["functions", "both"]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
moveDeclToRoot: "Move {{type}} declaration to {{body}} root."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/**
|
||||
* Ensure that a given node is at a program or function body's root.
|
||||
* @param {ASTNode} node Declaration node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function check(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
if (
|
||||
parent.type === "BlockStatement" && validBlockStatementParent.has(parent.parent.type)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (validParent.has(parent.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "moveDeclToRoot",
|
||||
data: {
|
||||
type: (node.type === "FunctionDeclaration" ? "function" : "variable"),
|
||||
body: getAllowedBodyDescription(node)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
|
||||
FunctionDeclaration: check,
|
||||
VariableDeclaration(node) {
|
||||
if (context.options[0] === "both" && node.kind === "var") {
|
||||
check(node);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* @fileoverview Validate strings passed to the RegExp constructor
|
||||
* @author Michael Ficarra
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const RegExpValidator = require("@eslint-community/regexpp").RegExpValidator;
|
||||
const validator = new RegExpValidator();
|
||||
const validFlags = /[dgimsuvy]/gu;
|
||||
const undefined1 = void 0;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow invalid regular expression strings in `RegExp` constructors",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-invalid-regexp"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowConstructorFlags: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
regexMessage: "{{message}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const options = context.options[0];
|
||||
let allowedFlags = null;
|
||||
|
||||
if (options && options.allowConstructorFlags) {
|
||||
const temp = options.allowConstructorFlags.join("").replace(validFlags, "");
|
||||
|
||||
if (temp) {
|
||||
allowedFlags = new RegExp(`[${temp}]`, "giu");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports error with the provided message.
|
||||
* @param {ASTNode} node The node holding the invalid RegExp
|
||||
* @param {string} message The message to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(node, message) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "regexMessage",
|
||||
data: { message }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if node is a string
|
||||
* @param {ASTNode} node node to evaluate
|
||||
* @returns {boolean} True if its a string
|
||||
* @private
|
||||
*/
|
||||
function isString(node) {
|
||||
return node && node.type === "Literal" && typeof node.value === "string";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets flags of a regular expression created by the given `RegExp()` or `new RegExp()` call
|
||||
* Examples:
|
||||
* new RegExp(".") // => ""
|
||||
* new RegExp(".", "gu") // => "gu"
|
||||
* new RegExp(".", flags) // => null
|
||||
* @param {ASTNode} node `CallExpression` or `NewExpression` node
|
||||
* @returns {string|null} flags if they can be determined, `null` otherwise
|
||||
* @private
|
||||
*/
|
||||
function getFlags(node) {
|
||||
if (node.arguments.length < 2) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (isString(node.arguments[1])) {
|
||||
return node.arguments[1].value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check syntax error in a given pattern.
|
||||
* @param {string} pattern The RegExp pattern to validate.
|
||||
* @param {Object} flags The RegExp flags to validate.
|
||||
* @param {boolean} [flags.unicode] The Unicode flag.
|
||||
* @param {boolean} [flags.unicodeSets] The UnicodeSets flag.
|
||||
* @returns {string|null} The syntax error.
|
||||
*/
|
||||
function validateRegExpPattern(pattern, flags) {
|
||||
try {
|
||||
validator.validatePattern(pattern, undefined1, undefined1, flags);
|
||||
return null;
|
||||
} catch (err) {
|
||||
return err.message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check syntax error in a given flags.
|
||||
* @param {string|null} flags The RegExp flags to validate.
|
||||
* @returns {string|null} The syntax error.
|
||||
*/
|
||||
function validateRegExpFlags(flags) {
|
||||
if (!flags) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
validator.validateFlags(flags);
|
||||
} catch {
|
||||
return `Invalid flags supplied to RegExp constructor '${flags}'`;
|
||||
}
|
||||
|
||||
/*
|
||||
* `regexpp` checks the combination of `u` and `v` flags when parsing `Pattern` according to `ecma262`,
|
||||
* but this rule may check only the flag when the pattern is unidentifiable, so check it here.
|
||||
* https://tc39.es/ecma262/multipage/text-processing.html#sec-parsepattern
|
||||
*/
|
||||
if (flags.includes("u") && flags.includes("v")) {
|
||||
return "Regex 'u' and 'v' flags cannot be used together";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
"CallExpression, NewExpression"(node) {
|
||||
if (node.callee.type !== "Identifier" || node.callee.name !== "RegExp") {
|
||||
return;
|
||||
}
|
||||
|
||||
let flags = getFlags(node);
|
||||
|
||||
if (flags && allowedFlags) {
|
||||
flags = flags.replace(allowedFlags, "");
|
||||
}
|
||||
|
||||
let message = validateRegExpFlags(flags);
|
||||
|
||||
if (message) {
|
||||
report(node, message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isString(node.arguments[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = node.arguments[0].value;
|
||||
|
||||
message = (
|
||||
|
||||
// If flags are unknown, report the regex only if its pattern is invalid both with and without the "u" flag
|
||||
flags === null
|
||||
? (
|
||||
validateRegExpPattern(pattern, { unicode: true, unicodeSets: false }) &&
|
||||
validateRegExpPattern(pattern, { unicode: false, unicodeSets: true }) &&
|
||||
validateRegExpPattern(pattern, { unicode: false, unicodeSets: false })
|
||||
)
|
||||
: validateRegExpPattern(pattern, { unicode: flags.includes("u"), unicodeSets: flags.includes("v") })
|
||||
);
|
||||
|
||||
if (message) {
|
||||
report(node, message);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @fileoverview A rule to disallow `this` keywords in contexts where the value of `this` is `undefined`.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines if the given code path is a code path with lexical `this` binding.
|
||||
* That is, if `this` within the code path refers to `this` of surrounding code path.
|
||||
* @param {CodePath} codePath Code path.
|
||||
* @param {ASTNode} node Node that started the code path.
|
||||
* @returns {boolean} `true` if it is a code path with lexical `this` binding.
|
||||
*/
|
||||
function isCodePathWithLexicalThis(codePath, node) {
|
||||
return codePath.origin === "function" && node.type === "ArrowFunctionExpression";
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow use of `this` in contexts where the value of `this` is `undefined`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-invalid-this"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
capIsConstructor: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedThis: "Unexpected 'this'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const capIsConstructor = options.capIsConstructor !== false;
|
||||
const stack = [],
|
||||
sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Gets the current checking context.
|
||||
*
|
||||
* The return value has a flag that whether or not `this` keyword is valid.
|
||||
* The flag is initialized when got at the first time.
|
||||
* @returns {{valid: boolean}}
|
||||
* an object which has a flag that whether or not `this` keyword is valid.
|
||||
*/
|
||||
stack.getCurrent = function() {
|
||||
const current = this[this.length - 1];
|
||||
|
||||
if (!current.init) {
|
||||
current.init = true;
|
||||
current.valid = !astUtils.isDefaultThisBinding(
|
||||
current.node,
|
||||
sourceCode,
|
||||
{ capIsConstructor }
|
||||
);
|
||||
}
|
||||
return current;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
onCodePathStart(codePath, node) {
|
||||
if (isCodePathWithLexicalThis(codePath, node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (codePath.origin === "program") {
|
||||
const scope = sourceCode.getScope(node);
|
||||
const features = context.languageOptions.parserOptions.ecmaFeatures || {};
|
||||
|
||||
// `this` at the top level of scripts always refers to the global object
|
||||
stack.push({
|
||||
init: true,
|
||||
node,
|
||||
valid: !(
|
||||
node.sourceType === "module" ||
|
||||
(features.globalReturn && scope.childScopes[0].isStrict)
|
||||
)
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* `init: false` means that `valid` isn't determined yet.
|
||||
* Most functions don't use `this`, and the calculation for `valid`
|
||||
* is relatively costly, so we'll calculate it lazily when the first
|
||||
* `this` within the function is traversed. A special case are non-strict
|
||||
* functions, because `this` refers to the global object and therefore is
|
||||
* always valid, so we can set `init: true` right away.
|
||||
*/
|
||||
stack.push({
|
||||
init: !sourceCode.getScope(node).isStrict,
|
||||
node,
|
||||
valid: true
|
||||
});
|
||||
},
|
||||
|
||||
onCodePathEnd(codePath, node) {
|
||||
if (isCodePathWithLexicalThis(codePath, node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
stack.pop();
|
||||
},
|
||||
|
||||
// Reports if `this` of the current context is invalid.
|
||||
ThisExpression(node) {
|
||||
const current = stack.getCurrent();
|
||||
|
||||
if (current && !current.valid) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedThis"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow whitespace that is not a tab or space, whitespace inside strings and comments are allowed
|
||||
* @author Jonathan Kingston
|
||||
* @author Christophe Porteneuve
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const ALL_IRREGULARS = /[\f\v\u0085\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000\u2028\u2029]/u;
|
||||
const IRREGULAR_WHITESPACE = /[\f\v\u0085\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000]+/mgu;
|
||||
const IRREGULAR_LINE_TERMINATORS = /[\u2028\u2029]/mgu;
|
||||
const LINE_BREAK = astUtils.createGlobalLinebreakMatcher();
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow irregular whitespace",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-irregular-whitespace"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
skipComments: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
skipStrings: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
},
|
||||
skipTemplates: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
skipRegExps: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
skipJSXText: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
noIrregularWhitespace: "Irregular whitespace not allowed."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
// Module store of errors that we have found
|
||||
let errors = [];
|
||||
|
||||
// Lookup the `skipComments` option, which defaults to `false`.
|
||||
const options = context.options[0] || {};
|
||||
const skipComments = !!options.skipComments;
|
||||
const skipStrings = options.skipStrings !== false;
|
||||
const skipRegExps = !!options.skipRegExps;
|
||||
const skipTemplates = !!options.skipTemplates;
|
||||
const skipJSXText = !!options.skipJSXText;
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
const commentNodes = sourceCode.getAllComments();
|
||||
|
||||
/**
|
||||
* Removes errors that occur inside the given node
|
||||
* @param {ASTNode} node to check for matching errors.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function removeWhitespaceError(node) {
|
||||
const locStart = node.loc.start;
|
||||
const locEnd = node.loc.end;
|
||||
|
||||
errors = errors.filter(({ loc: { start: errorLocStart } }) => (
|
||||
errorLocStart.line < locStart.line ||
|
||||
errorLocStart.line === locStart.line && errorLocStart.column < locStart.column ||
|
||||
errorLocStart.line === locEnd.line && errorLocStart.column >= locEnd.column ||
|
||||
errorLocStart.line > locEnd.line
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
|
||||
* @param {ASTNode} node to check for matching errors.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function removeInvalidNodeErrorsInLiteral(node) {
|
||||
const shouldCheckStrings = skipStrings && (typeof node.value === "string");
|
||||
const shouldCheckRegExps = skipRegExps && Boolean(node.regex);
|
||||
|
||||
if (shouldCheckStrings || shouldCheckRegExps) {
|
||||
|
||||
// If we have irregular characters remove them from the errors list
|
||||
if (ALL_IRREGULARS.test(node.raw)) {
|
||||
removeWhitespaceError(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks template string literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
|
||||
* @param {ASTNode} node to check for matching errors.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function removeInvalidNodeErrorsInTemplateLiteral(node) {
|
||||
if (typeof node.value.raw === "string") {
|
||||
if (ALL_IRREGULARS.test(node.value.raw)) {
|
||||
removeWhitespaceError(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks comment nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
|
||||
* @param {ASTNode} node to check for matching errors.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function removeInvalidNodeErrorsInComment(node) {
|
||||
if (ALL_IRREGULARS.test(node.value)) {
|
||||
removeWhitespaceError(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks JSX nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
|
||||
* @param {ASTNode} node to check for matching errors.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function removeInvalidNodeErrorsInJSXText(node) {
|
||||
if (ALL_IRREGULARS.test(node.raw)) {
|
||||
removeWhitespaceError(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the program source for irregular whitespace
|
||||
* @param {ASTNode} node The program node
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkForIrregularWhitespace(node) {
|
||||
const sourceLines = sourceCode.lines;
|
||||
|
||||
sourceLines.forEach((sourceLine, lineIndex) => {
|
||||
const lineNumber = lineIndex + 1;
|
||||
let match;
|
||||
|
||||
while ((match = IRREGULAR_WHITESPACE.exec(sourceLine)) !== null) {
|
||||
errors.push({
|
||||
node,
|
||||
messageId: "noIrregularWhitespace",
|
||||
loc: {
|
||||
start: {
|
||||
line: lineNumber,
|
||||
column: match.index
|
||||
},
|
||||
end: {
|
||||
line: lineNumber,
|
||||
column: match.index + match[0].length
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the program source for irregular line terminators
|
||||
* @param {ASTNode} node The program node
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkForIrregularLineTerminators(node) {
|
||||
const source = sourceCode.getText(),
|
||||
sourceLines = sourceCode.lines,
|
||||
linebreaks = source.match(LINE_BREAK);
|
||||
let lastLineIndex = -1,
|
||||
match;
|
||||
|
||||
while ((match = IRREGULAR_LINE_TERMINATORS.exec(source)) !== null) {
|
||||
const lineIndex = linebreaks.indexOf(match[0], lastLineIndex + 1) || 0;
|
||||
|
||||
errors.push({
|
||||
node,
|
||||
messageId: "noIrregularWhitespace",
|
||||
loc: {
|
||||
start: {
|
||||
line: lineIndex + 1,
|
||||
column: sourceLines[lineIndex].length
|
||||
},
|
||||
end: {
|
||||
line: lineIndex + 2,
|
||||
column: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
lastLineIndex = lineIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A no-op function to act as placeholder for comment accumulation when the `skipComments` option is `false`.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function noop() {}
|
||||
|
||||
const nodes = {};
|
||||
|
||||
if (ALL_IRREGULARS.test(sourceCode.getText())) {
|
||||
nodes.Program = function(node) {
|
||||
|
||||
/*
|
||||
* As we can easily fire warnings for all white space issues with
|
||||
* all the source its simpler to fire them here.
|
||||
* This means we can check all the application code without having
|
||||
* to worry about issues caused in the parser tokens.
|
||||
* When writing this code also evaluating per node was missing out
|
||||
* connecting tokens in some cases.
|
||||
* We can later filter the errors when they are found to be not an
|
||||
* issue in nodes we don't care about.
|
||||
*/
|
||||
checkForIrregularWhitespace(node);
|
||||
checkForIrregularLineTerminators(node);
|
||||
};
|
||||
|
||||
nodes.Literal = removeInvalidNodeErrorsInLiteral;
|
||||
nodes.TemplateElement = skipTemplates ? removeInvalidNodeErrorsInTemplateLiteral : noop;
|
||||
nodes.JSXText = skipJSXText ? removeInvalidNodeErrorsInJSXText : noop;
|
||||
nodes["Program:exit"] = function() {
|
||||
if (skipComments) {
|
||||
|
||||
// First strip errors occurring in comment nodes.
|
||||
commentNodes.forEach(removeInvalidNodeErrorsInComment);
|
||||
}
|
||||
|
||||
// If we have any errors remaining report on them
|
||||
errors.forEach(error => context.report(error));
|
||||
};
|
||||
} else {
|
||||
nodes.Program = noop;
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag usage of __iterator__ property
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const { getStaticPropertyName } = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow the use of the `__iterator__` property",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-iterator"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
noIterator: "Reserved name '__iterator__'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
|
||||
MemberExpression(node) {
|
||||
|
||||
if (getStaticPropertyName(node) === "__iterator__") {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noIterator"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag labels that are the same as an identifier
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow labels that share a name with a variable",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-label-var"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
identifierClashWithLabel: "Found identifier with same name as label."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if the identifier is present inside current scope
|
||||
* @param {Object} scope current scope
|
||||
* @param {string} name To evaluate
|
||||
* @returns {boolean} True if its present
|
||||
* @private
|
||||
*/
|
||||
function findIdentifier(scope, name) {
|
||||
return astUtils.getVariableByName(scope, name) !== null;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
|
||||
LabeledStatement(node) {
|
||||
|
||||
// Fetch the innermost scope.
|
||||
const scope = sourceCode.getScope(node);
|
||||
|
||||
/*
|
||||
* Recursively find the identifier walking up the scope, starting
|
||||
* with the innermost scope.
|
||||
*/
|
||||
if (findIdentifier(scope, node.label.name)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "identifierClashWithLabel"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @fileoverview Disallow Labeled Statements
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow labeled statements",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-labels"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowLoop: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
allowSwitch: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedLabel: "Unexpected labeled statement.",
|
||||
unexpectedLabelInBreak: "Unexpected label in break statement.",
|
||||
unexpectedLabelInContinue: "Unexpected label in continue statement."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0];
|
||||
const allowLoop = options && options.allowLoop;
|
||||
const allowSwitch = options && options.allowSwitch;
|
||||
let scopeInfo = null;
|
||||
|
||||
/**
|
||||
* Gets the kind of a given node.
|
||||
* @param {ASTNode} node A node to get.
|
||||
* @returns {string} The kind of the node.
|
||||
*/
|
||||
function getBodyKind(node) {
|
||||
if (astUtils.isLoop(node)) {
|
||||
return "loop";
|
||||
}
|
||||
if (node.type === "SwitchStatement") {
|
||||
return "switch";
|
||||
}
|
||||
return "other";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the label of a given kind is allowed or not.
|
||||
* @param {string} kind A kind to check.
|
||||
* @returns {boolean} `true` if the kind is allowed.
|
||||
*/
|
||||
function isAllowed(kind) {
|
||||
switch (kind) {
|
||||
case "loop": return allowLoop;
|
||||
case "switch": return allowSwitch;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given name is a label of a loop or not.
|
||||
* @param {string} label A name of a label to check.
|
||||
* @returns {boolean} `true` if the name is a label of a loop.
|
||||
*/
|
||||
function getKind(label) {
|
||||
let info = scopeInfo;
|
||||
|
||||
while (info) {
|
||||
if (info.label === label) {
|
||||
return info.kind;
|
||||
}
|
||||
info = info.upper;
|
||||
}
|
||||
|
||||
/* c8 ignore next */
|
||||
return "other";
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
LabeledStatement(node) {
|
||||
scopeInfo = {
|
||||
label: node.label.name,
|
||||
kind: getBodyKind(node.body),
|
||||
upper: scopeInfo
|
||||
};
|
||||
},
|
||||
|
||||
"LabeledStatement:exit"(node) {
|
||||
if (!isAllowed(scopeInfo.kind)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedLabel"
|
||||
});
|
||||
}
|
||||
|
||||
scopeInfo = scopeInfo.upper;
|
||||
},
|
||||
|
||||
BreakStatement(node) {
|
||||
if (node.label && !isAllowed(getKind(node.label.name))) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedLabelInBreak"
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
ContinueStatement(node) {
|
||||
if (node.label && !isAllowed(getKind(node.label.name))) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedLabelInContinue"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag blocks with no reason to exist
|
||||
* @author Brandon Mills
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow unnecessary nested blocks",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-lone-blocks"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
redundantBlock: "Block is redundant.",
|
||||
redundantNestedBlock: "Nested block is redundant."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
// A stack of lone blocks to be checked for block-level bindings
|
||||
const loneBlocks = [];
|
||||
let ruleDef;
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Reports a node as invalid.
|
||||
* @param {ASTNode} node The node to be reported.
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(node) {
|
||||
const messageId = node.parent.type === "BlockStatement" || node.parent.type === "StaticBlock"
|
||||
? "redundantNestedBlock"
|
||||
: "redundantBlock";
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for any occurrence of a BlockStatement in a place where lists of statements can appear
|
||||
* @param {ASTNode} node The node to check
|
||||
* @returns {boolean} True if the node is a lone block.
|
||||
*/
|
||||
function isLoneBlock(node) {
|
||||
return node.parent.type === "BlockStatement" ||
|
||||
node.parent.type === "StaticBlock" ||
|
||||
node.parent.type === "Program" ||
|
||||
|
||||
// Don't report blocks in switch cases if the block is the only statement of the case.
|
||||
node.parent.type === "SwitchCase" && !(node.parent.consequent[0] === node && node.parent.consequent.length === 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the enclosing block of the current node for block-level bindings,
|
||||
* and "marks it" as valid if any.
|
||||
* @param {ASTNode} node The current node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function markLoneBlock(node) {
|
||||
if (loneBlocks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const block = node.parent;
|
||||
|
||||
if (loneBlocks[loneBlocks.length - 1] === block) {
|
||||
loneBlocks.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// Default rule definition: report all lone blocks
|
||||
ruleDef = {
|
||||
BlockStatement(node) {
|
||||
if (isLoneBlock(node)) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ES6: report blocks without block-level bindings, or that's only child of another block
|
||||
if (context.languageOptions.ecmaVersion >= 2015) {
|
||||
ruleDef = {
|
||||
BlockStatement(node) {
|
||||
if (isLoneBlock(node)) {
|
||||
loneBlocks.push(node);
|
||||
}
|
||||
},
|
||||
"BlockStatement:exit"(node) {
|
||||
if (loneBlocks.length > 0 && loneBlocks[loneBlocks.length - 1] === node) {
|
||||
loneBlocks.pop();
|
||||
report(node);
|
||||
} else if (
|
||||
(
|
||||
node.parent.type === "BlockStatement" ||
|
||||
node.parent.type === "StaticBlock"
|
||||
) &&
|
||||
node.parent.body.length === 1
|
||||
) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ruleDef.VariableDeclaration = function(node) {
|
||||
if (node.kind === "let" || node.kind === "const") {
|
||||
markLoneBlock(node);
|
||||
}
|
||||
};
|
||||
|
||||
ruleDef.FunctionDeclaration = function(node) {
|
||||
if (sourceCode.getScope(node).isStrict) {
|
||||
markLoneBlock(node);
|
||||
}
|
||||
};
|
||||
|
||||
ruleDef.ClassDeclaration = markLoneBlock;
|
||||
}
|
||||
|
||||
return ruleDef;
|
||||
}
|
||||
};
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow if as the only statement in an else block
|
||||
* @author Brandon Mills
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `if` statements as the only statement in `else` blocks",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-lonely-if"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
fixable: "code",
|
||||
|
||||
messages: {
|
||||
unexpectedLonelyIf: "Unexpected if as the only statement in an else block."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
IfStatement(node) {
|
||||
const parent = node.parent,
|
||||
grandparent = parent.parent;
|
||||
|
||||
if (parent && parent.type === "BlockStatement" &&
|
||||
parent.body.length === 1 && grandparent &&
|
||||
grandparent.type === "IfStatement" &&
|
||||
parent === grandparent.alternate) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedLonelyIf",
|
||||
fix(fixer) {
|
||||
const openingElseCurly = sourceCode.getFirstToken(parent);
|
||||
const closingElseCurly = sourceCode.getLastToken(parent);
|
||||
const elseKeyword = sourceCode.getTokenBefore(openingElseCurly);
|
||||
const tokenAfterElseBlock = sourceCode.getTokenAfter(closingElseCurly);
|
||||
const lastIfToken = sourceCode.getLastToken(node.consequent);
|
||||
const sourceText = sourceCode.getText();
|
||||
|
||||
if (sourceText.slice(openingElseCurly.range[1],
|
||||
node.range[0]).trim() || sourceText.slice(node.range[1], closingElseCurly.range[0]).trim()) {
|
||||
|
||||
// Don't fix if there are any non-whitespace characters interfering (e.g. comments)
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
node.consequent.type !== "BlockStatement" && lastIfToken.value !== ";" && tokenAfterElseBlock &&
|
||||
(
|
||||
node.consequent.loc.end.line === tokenAfterElseBlock.loc.start.line ||
|
||||
/^[([/+`-]/u.test(tokenAfterElseBlock.value) ||
|
||||
lastIfToken.value === "++" ||
|
||||
lastIfToken.value === "--"
|
||||
)
|
||||
) {
|
||||
|
||||
/*
|
||||
* If the `if` statement has no block, and is not followed by a semicolon, make sure that fixing
|
||||
* the issue would not change semantics due to ASI. If this would happen, don't do a fix.
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
|
||||
return fixer.replaceTextRange(
|
||||
[openingElseCurly.range[0], closingElseCurly.range[1]],
|
||||
(elseKeyword.range[1] === openingElseCurly.range[0] ? " " : "") + sourceCode.getText(node)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag creation of function inside a loop
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gets the containing loop node of a specified node.
|
||||
*
|
||||
* We don't need to check nested functions, so this ignores those.
|
||||
* `Scope.through` contains references of nested functions.
|
||||
* @param {ASTNode} node An AST node to get.
|
||||
* @returns {ASTNode|null} The containing loop node of the specified node, or
|
||||
* `null`.
|
||||
*/
|
||||
function getContainingLoopNode(node) {
|
||||
for (let currentNode = node; currentNode.parent; currentNode = currentNode.parent) {
|
||||
const parent = currentNode.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
case "WhileStatement":
|
||||
case "DoWhileStatement":
|
||||
return parent;
|
||||
|
||||
case "ForStatement":
|
||||
|
||||
// `init` is outside of the loop.
|
||||
if (parent.init !== currentNode) {
|
||||
return parent;
|
||||
}
|
||||
break;
|
||||
|
||||
case "ForInStatement":
|
||||
case "ForOfStatement":
|
||||
|
||||
// `right` is outside of the loop.
|
||||
if (parent.right !== currentNode) {
|
||||
return parent;
|
||||
}
|
||||
break;
|
||||
|
||||
case "ArrowFunctionExpression":
|
||||
case "FunctionExpression":
|
||||
case "FunctionDeclaration":
|
||||
|
||||
// We don't need to check nested functions.
|
||||
return null;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the containing loop node of a given node.
|
||||
* If the loop was nested, this returns the most outer loop.
|
||||
* @param {ASTNode} node A node to get. This is a loop node.
|
||||
* @param {ASTNode|null} excludedNode A node that the result node should not
|
||||
* include.
|
||||
* @returns {ASTNode} The most outer loop node.
|
||||
*/
|
||||
function getTopLoopNode(node, excludedNode) {
|
||||
const border = excludedNode ? excludedNode.range[1] : 0;
|
||||
let retv = node;
|
||||
let containingLoopNode = node;
|
||||
|
||||
while (containingLoopNode && containingLoopNode.range[0] >= border) {
|
||||
retv = containingLoopNode;
|
||||
containingLoopNode = getContainingLoopNode(containingLoopNode);
|
||||
}
|
||||
|
||||
return retv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given reference which refers to an upper scope's variable is
|
||||
* safe or not.
|
||||
* @param {ASTNode} loopNode A containing loop node.
|
||||
* @param {eslint-scope.Reference} reference A reference to check.
|
||||
* @returns {boolean} `true` if the reference is safe or not.
|
||||
*/
|
||||
function isSafe(loopNode, reference) {
|
||||
const variable = reference.resolved;
|
||||
const definition = variable && variable.defs[0];
|
||||
const declaration = definition && definition.parent;
|
||||
const kind = (declaration && declaration.type === "VariableDeclaration")
|
||||
? declaration.kind
|
||||
: "";
|
||||
|
||||
// Variables which are declared by `const` is safe.
|
||||
if (kind === "const") {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Variables which are declared by `let` in the loop is safe.
|
||||
* It's a different instance from the next loop step's.
|
||||
*/
|
||||
if (kind === "let" &&
|
||||
declaration.range[0] > loopNode.range[0] &&
|
||||
declaration.range[1] < loopNode.range[1]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* WriteReferences which exist after this border are unsafe because those
|
||||
* can modify the variable.
|
||||
*/
|
||||
const border = getTopLoopNode(
|
||||
loopNode,
|
||||
(kind === "let") ? declaration : null
|
||||
).range[0];
|
||||
|
||||
/**
|
||||
* Checks whether a given reference is safe or not.
|
||||
* The reference is every reference of the upper scope's variable we are
|
||||
* looking now.
|
||||
*
|
||||
* It's safe if the reference matches one of the following condition.
|
||||
* - is readonly.
|
||||
* - doesn't exist inside a local function and after the border.
|
||||
* @param {eslint-scope.Reference} upperRef A reference to check.
|
||||
* @returns {boolean} `true` if the reference is safe.
|
||||
*/
|
||||
function isSafeReference(upperRef) {
|
||||
const id = upperRef.identifier;
|
||||
|
||||
return (
|
||||
!upperRef.isWrite() ||
|
||||
variable.scope.variableScope === upperRef.from.variableScope &&
|
||||
id.range[0] < border
|
||||
);
|
||||
}
|
||||
|
||||
return Boolean(variable) && variable.references.every(isSafeReference);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow function declarations that contain unsafe references inside loop statements",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-loop-func"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unsafeRefs: "Function declared in a loop contains unsafe references to variable(s) {{ varNames }}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Reports functions which match the following condition:
|
||||
*
|
||||
* - has a loop node in ancestors.
|
||||
* - has any references which refers to an unsafe variable.
|
||||
* @param {ASTNode} node The AST node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkForLoops(node) {
|
||||
const loopNode = getContainingLoopNode(node);
|
||||
|
||||
if (!loopNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const references = sourceCode.getScope(node).through;
|
||||
const unsafeRefs = references.filter(r => r.resolved && !isSafe(loopNode, r)).map(r => r.identifier.name);
|
||||
|
||||
if (unsafeRefs.length > 0) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unsafeRefs",
|
||||
data: { varNames: `'${unsafeRefs.join("', '")}'` }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ArrowFunctionExpression: checkForLoops,
|
||||
FunctionExpression: checkForLoops,
|
||||
FunctionDeclaration: checkForLoops
|
||||
};
|
||||
}
|
||||
};
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag numbers that will lose significant figure precision at runtime
|
||||
* @author Jacob Moore
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow literal numbers that lose precision",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-loss-of-precision"
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
noLossOfPrecision: "This number literal will lose precision at runtime."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/**
|
||||
* Returns whether the node is number literal
|
||||
* @param {Node} node the node literal being evaluated
|
||||
* @returns {boolean} true if the node is a number literal
|
||||
*/
|
||||
function isNumber(node) {
|
||||
return typeof node.value === "number";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the source code of the given number literal. Removes `_` numeric separators from the result.
|
||||
* @param {Node} node the number `Literal` node
|
||||
* @returns {string} raw source code of the literal, without numeric separators
|
||||
*/
|
||||
function getRaw(node) {
|
||||
return node.raw.replace(/_/gu, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the number is base ten
|
||||
* @param {ASTNode} node the node being evaluated
|
||||
* @returns {boolean} true if the node is in base ten
|
||||
*/
|
||||
function isBaseTen(node) {
|
||||
const prefixes = ["0x", "0X", "0b", "0B", "0o", "0O"];
|
||||
|
||||
return prefixes.every(prefix => !node.raw.startsWith(prefix)) &&
|
||||
!/^0[0-7]+$/u.test(node.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the user-intended non-base ten number equals the actual number after is has been converted to the Number type
|
||||
* @param {Node} node the node being evaluated
|
||||
* @returns {boolean} true if they do not match
|
||||
*/
|
||||
function notBaseTenLosesPrecision(node) {
|
||||
const rawString = getRaw(node).toUpperCase();
|
||||
let base = 0;
|
||||
|
||||
if (rawString.startsWith("0B")) {
|
||||
base = 2;
|
||||
} else if (rawString.startsWith("0X")) {
|
||||
base = 16;
|
||||
} else {
|
||||
base = 8;
|
||||
}
|
||||
|
||||
return !rawString.endsWith(node.value.toString(base).toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a decimal point to the numeric string at index 1
|
||||
* @param {string} stringNumber the numeric string without any decimal point
|
||||
* @returns {string} the numeric string with a decimal point in the proper place
|
||||
*/
|
||||
function addDecimalPointToNumber(stringNumber) {
|
||||
return `${stringNumber[0]}.${stringNumber.slice(1)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number stripped of leading zeros
|
||||
* @param {string} numberAsString the string representation of the number
|
||||
* @returns {string} the stripped string
|
||||
*/
|
||||
function removeLeadingZeros(numberAsString) {
|
||||
for (let i = 0; i < numberAsString.length; i++) {
|
||||
if (numberAsString[i] !== "0") {
|
||||
return numberAsString.slice(i);
|
||||
}
|
||||
}
|
||||
return numberAsString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number stripped of trailing zeros
|
||||
* @param {string} numberAsString the string representation of the number
|
||||
* @returns {string} the stripped string
|
||||
*/
|
||||
function removeTrailingZeros(numberAsString) {
|
||||
for (let i = numberAsString.length - 1; i >= 0; i--) {
|
||||
if (numberAsString[i] !== "0") {
|
||||
return numberAsString.slice(0, i + 1);
|
||||
}
|
||||
}
|
||||
return numberAsString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an integer to an object containing the integer's coefficient and order of magnitude
|
||||
* @param {string} stringInteger the string representation of the integer being converted
|
||||
* @returns {Object} the object containing the integer's coefficient and order of magnitude
|
||||
*/
|
||||
function normalizeInteger(stringInteger) {
|
||||
const significantDigits = removeTrailingZeros(removeLeadingZeros(stringInteger));
|
||||
|
||||
return {
|
||||
magnitude: stringInteger.startsWith("0") ? stringInteger.length - 2 : stringInteger.length - 1,
|
||||
coefficient: addDecimalPointToNumber(significantDigits)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Converts a float to an object containing the floats's coefficient and order of magnitude
|
||||
* @param {string} stringFloat the string representation of the float being converted
|
||||
* @returns {Object} the object containing the integer's coefficient and order of magnitude
|
||||
*/
|
||||
function normalizeFloat(stringFloat) {
|
||||
const trimmedFloat = removeLeadingZeros(stringFloat);
|
||||
|
||||
if (trimmedFloat.startsWith(".")) {
|
||||
const decimalDigits = trimmedFloat.slice(1);
|
||||
const significantDigits = removeLeadingZeros(decimalDigits);
|
||||
|
||||
return {
|
||||
magnitude: significantDigits.length - decimalDigits.length - 1,
|
||||
coefficient: addDecimalPointToNumber(significantDigits)
|
||||
};
|
||||
|
||||
}
|
||||
return {
|
||||
magnitude: trimmedFloat.indexOf(".") - 1,
|
||||
coefficient: addDecimalPointToNumber(trimmedFloat.replace(".", ""))
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a base ten number to proper scientific notation
|
||||
* @param {string} stringNumber the string representation of the base ten number to be converted
|
||||
* @returns {string} the number converted to scientific notation
|
||||
*/
|
||||
function convertNumberToScientificNotation(stringNumber) {
|
||||
const splitNumber = stringNumber.replace("E", "e").split("e");
|
||||
const originalCoefficient = splitNumber[0];
|
||||
const normalizedNumber = stringNumber.includes(".") ? normalizeFloat(originalCoefficient)
|
||||
: normalizeInteger(originalCoefficient);
|
||||
const normalizedCoefficient = normalizedNumber.coefficient;
|
||||
const magnitude = splitNumber.length > 1 ? (parseInt(splitNumber[1], 10) + normalizedNumber.magnitude)
|
||||
: normalizedNumber.magnitude;
|
||||
|
||||
return `${normalizedCoefficient}e${magnitude}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the user-intended base ten number equals the actual number after is has been converted to the Number type
|
||||
* @param {Node} node the node being evaluated
|
||||
* @returns {boolean} true if they do not match
|
||||
*/
|
||||
function baseTenLosesPrecision(node) {
|
||||
const normalizedRawNumber = convertNumberToScientificNotation(getRaw(node));
|
||||
const requestedPrecision = normalizedRawNumber.split("e")[0].replace(".", "").length;
|
||||
|
||||
if (requestedPrecision > 100) {
|
||||
return true;
|
||||
}
|
||||
const storedNumber = node.value.toPrecision(requestedPrecision);
|
||||
const normalizedStoredNumber = convertNumberToScientificNotation(storedNumber);
|
||||
|
||||
return normalizedRawNumber !== normalizedStoredNumber;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks that the user-intended number equals the actual number after is has been converted to the Number type
|
||||
* @param {Node} node the node being evaluated
|
||||
* @returns {boolean} true if they do not match
|
||||
*/
|
||||
function losesPrecision(node) {
|
||||
return isBaseTen(node) ? baseTenLosesPrecision(node) : notBaseTenLosesPrecision(node);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
Literal(node) {
|
||||
if (node.value && isNumber(node) && losesPrecision(node)) {
|
||||
context.report({
|
||||
messageId: "noLossOfPrecision",
|
||||
node
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag statements that use magic numbers (adapted from https://github.com/danielstjules/buddy.js)
|
||||
* @author Vincent Lemeunier
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
// Maximum array length by the ECMAScript Specification.
|
||||
const MAX_ARRAY_LENGTH = 2 ** 32 - 1;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Convert the value to bigint if it's a string. Otherwise return the value as-is.
|
||||
* @param {bigint|number|string} x The value to normalize.
|
||||
* @returns {bigint|number} The normalized value.
|
||||
*/
|
||||
function normalizeIgnoreValue(x) {
|
||||
if (typeof x === "string") {
|
||||
return BigInt(x.slice(0, -1));
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow magic numbers",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-magic-numbers"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
detectObjects: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
enforceConst: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
ignore: {
|
||||
type: "array",
|
||||
items: {
|
||||
anyOf: [
|
||||
{ type: "number" },
|
||||
{ type: "string", pattern: "^[+-]?(?:0|[1-9][0-9]*)n$" }
|
||||
]
|
||||
},
|
||||
uniqueItems: true
|
||||
},
|
||||
ignoreArrayIndexes: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
ignoreDefaultValues: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
ignoreClassFieldInitialValues: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
useConst: "Number constants declarations must use 'const'.",
|
||||
noMagic: "No magic number: {{raw}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0] || {},
|
||||
detectObjects = !!config.detectObjects,
|
||||
enforceConst = !!config.enforceConst,
|
||||
ignore = new Set((config.ignore || []).map(normalizeIgnoreValue)),
|
||||
ignoreArrayIndexes = !!config.ignoreArrayIndexes,
|
||||
ignoreDefaultValues = !!config.ignoreDefaultValues,
|
||||
ignoreClassFieldInitialValues = !!config.ignoreClassFieldInitialValues;
|
||||
|
||||
const okTypes = detectObjects ? [] : ["ObjectExpression", "Property", "AssignmentExpression"];
|
||||
|
||||
/**
|
||||
* Returns whether the rule is configured to ignore the given value
|
||||
* @param {bigint|number} value The value to check
|
||||
* @returns {boolean} true if the value is ignored
|
||||
*/
|
||||
function isIgnoredValue(value) {
|
||||
return ignore.has(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the number is a default value assignment.
|
||||
* @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
|
||||
* @returns {boolean} true if the number is a default value
|
||||
*/
|
||||
function isDefaultValue(fullNumberNode) {
|
||||
const parent = fullNumberNode.parent;
|
||||
|
||||
return parent.type === "AssignmentPattern" && parent.right === fullNumberNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the number is the initial value of a class field.
|
||||
* @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
|
||||
* @returns {boolean} true if the number is the initial value of a class field.
|
||||
*/
|
||||
function isClassFieldInitialValue(fullNumberNode) {
|
||||
const parent = fullNumberNode.parent;
|
||||
|
||||
return parent.type === "PropertyDefinition" && parent.value === fullNumberNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given node is used as a radix within parseInt() or Number.parseInt()
|
||||
* @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
|
||||
* @returns {boolean} true if the node is radix
|
||||
*/
|
||||
function isParseIntRadix(fullNumberNode) {
|
||||
const parent = fullNumberNode.parent;
|
||||
|
||||
return parent.type === "CallExpression" && fullNumberNode === parent.arguments[1] &&
|
||||
(
|
||||
astUtils.isSpecificId(parent.callee, "parseInt") ||
|
||||
astUtils.isSpecificMemberAccess(parent.callee, "Number", "parseInt")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given node is a direct child of a JSX node.
|
||||
* In particular, it aims to detect numbers used as prop values in JSX tags.
|
||||
* Example: <input maxLength={10} />
|
||||
* @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
|
||||
* @returns {boolean} true if the node is a JSX number
|
||||
*/
|
||||
function isJSXNumber(fullNumberNode) {
|
||||
return fullNumberNode.parent.type.indexOf("JSX") === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given node is used as an array index.
|
||||
* Value must coerce to a valid array index name: "0", "1", "2" ... "4294967294".
|
||||
*
|
||||
* All other values, like "-1", "2.5", or "4294967295", are just "normal" object properties,
|
||||
* which can be created and accessed on an array in addition to the array index properties,
|
||||
* but they don't affect array's length and are not considered by methods such as .map(), .forEach() etc.
|
||||
*
|
||||
* The maximum array length by the specification is 2 ** 32 - 1 = 4294967295,
|
||||
* thus the maximum valid index is 2 ** 32 - 2 = 4294967294.
|
||||
*
|
||||
* All notations are allowed, as long as the value coerces to one of "0", "1", "2" ... "4294967294".
|
||||
*
|
||||
* Valid examples:
|
||||
* a[0], a[1], a[1.2e1], a[0xAB], a[0n], a[1n]
|
||||
* a[-0] (same as a[0] because -0 coerces to "0")
|
||||
* a[-0n] (-0n evaluates to 0n)
|
||||
*
|
||||
* Invalid examples:
|
||||
* a[-1], a[-0xAB], a[-1n], a[2.5], a[1.23e1], a[12e-1]
|
||||
* a[4294967295] (above the max index, it's an access to a regular property a["4294967295"])
|
||||
* a[999999999999999999999] (even if it wasn't above the max index, it would be a["1e+21"])
|
||||
* a[1e310] (same as a["Infinity"])
|
||||
* @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
|
||||
* @param {bigint|number} value Value expressed by the fullNumberNode
|
||||
* @returns {boolean} true if the node is a valid array index
|
||||
*/
|
||||
function isArrayIndex(fullNumberNode, value) {
|
||||
const parent = fullNumberNode.parent;
|
||||
|
||||
return parent.type === "MemberExpression" && parent.property === fullNumberNode &&
|
||||
(Number.isInteger(value) || typeof value === "bigint") &&
|
||||
value >= 0 && value < MAX_ARRAY_LENGTH;
|
||||
}
|
||||
|
||||
return {
|
||||
Literal(node) {
|
||||
if (!astUtils.isNumericLiteral(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fullNumberNode;
|
||||
let value;
|
||||
let raw;
|
||||
|
||||
// Treat unary minus as a part of the number
|
||||
if (node.parent.type === "UnaryExpression" && node.parent.operator === "-") {
|
||||
fullNumberNode = node.parent;
|
||||
value = -node.value;
|
||||
raw = `-${node.raw}`;
|
||||
} else {
|
||||
fullNumberNode = node;
|
||||
value = node.value;
|
||||
raw = node.raw;
|
||||
}
|
||||
|
||||
const parent = fullNumberNode.parent;
|
||||
|
||||
// Always allow radix arguments and JSX props
|
||||
if (
|
||||
isIgnoredValue(value) ||
|
||||
(ignoreDefaultValues && isDefaultValue(fullNumberNode)) ||
|
||||
(ignoreClassFieldInitialValues && isClassFieldInitialValue(fullNumberNode)) ||
|
||||
isParseIntRadix(fullNumberNode) ||
|
||||
isJSXNumber(fullNumberNode) ||
|
||||
(ignoreArrayIndexes && isArrayIndex(fullNumberNode, value))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parent.type === "VariableDeclarator") {
|
||||
if (enforceConst && parent.parent.kind !== "const") {
|
||||
context.report({
|
||||
node: fullNumberNode,
|
||||
messageId: "useConst"
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
!okTypes.includes(parent.type) ||
|
||||
(parent.type === "AssignmentExpression" && parent.left.type === "Identifier")
|
||||
) {
|
||||
context.report({
|
||||
node: fullNumberNode,
|
||||
messageId: "noMagic",
|
||||
data: {
|
||||
raw
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+300
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* @author Toru Nagashima <https://github.com/mysticatea>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("@eslint-community/eslint-utils");
|
||||
const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp");
|
||||
const { isCombiningCharacter, isEmojiModifier, isRegionalIndicatorSymbol, isSurrogatePair } = require("./utils/unicode");
|
||||
const astUtils = require("./utils/ast-utils.js");
|
||||
const { isValidWithUnicodeFlag } = require("./utils/regular-expressions");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @typedef {import('@eslint-community/regexpp').AST.Character} Character
|
||||
* @typedef {import('@eslint-community/regexpp').AST.CharacterClassElement} CharacterClassElement
|
||||
*/
|
||||
|
||||
/**
|
||||
* Iterate character sequences of a given nodes.
|
||||
*
|
||||
* CharacterClassRange syntax can steal a part of character sequence,
|
||||
* so this function reverts CharacterClassRange syntax and restore the sequence.
|
||||
* @param {CharacterClassElement[]} nodes The node list to iterate character sequences.
|
||||
* @returns {IterableIterator<Character[]>} The list of character sequences.
|
||||
*/
|
||||
function *iterateCharacterSequence(nodes) {
|
||||
|
||||
/** @type {Character[]} */
|
||||
let seq = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
switch (node.type) {
|
||||
case "Character":
|
||||
seq.push(node);
|
||||
break;
|
||||
|
||||
case "CharacterClassRange":
|
||||
seq.push(node.min);
|
||||
yield seq;
|
||||
seq = [node.max];
|
||||
break;
|
||||
|
||||
case "CharacterSet":
|
||||
case "CharacterClass": // [[]] nesting character class
|
||||
case "ClassStringDisjunction": // \q{...}
|
||||
case "ExpressionCharacterClass": // [A--B]
|
||||
if (seq.length > 0) {
|
||||
yield seq;
|
||||
seq = [];
|
||||
}
|
||||
break;
|
||||
|
||||
// no default
|
||||
}
|
||||
}
|
||||
|
||||
if (seq.length > 0) {
|
||||
yield seq;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether the given character node is a Unicode code point escape or not.
|
||||
* @param {Character} char the character node to check.
|
||||
* @returns {boolean} `true` if the character node is a Unicode code point escape.
|
||||
*/
|
||||
function isUnicodeCodePointEscape(char) {
|
||||
return /^\\u\{[\da-f]+\}$/iu.test(char.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Each function returns `true` if it detects that kind of problem.
|
||||
* @type {Record<string, (chars: Character[]) => boolean>}
|
||||
*/
|
||||
const hasCharacterSequence = {
|
||||
surrogatePairWithoutUFlag(chars) {
|
||||
return chars.some((c, i) => {
|
||||
if (i === 0) {
|
||||
return false;
|
||||
}
|
||||
const c1 = chars[i - 1];
|
||||
|
||||
return (
|
||||
isSurrogatePair(c1.value, c.value) &&
|
||||
!isUnicodeCodePointEscape(c1) &&
|
||||
!isUnicodeCodePointEscape(c)
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
surrogatePair(chars) {
|
||||
return chars.some((c, i) => {
|
||||
if (i === 0) {
|
||||
return false;
|
||||
}
|
||||
const c1 = chars[i - 1];
|
||||
|
||||
return (
|
||||
isSurrogatePair(c1.value, c.value) &&
|
||||
(
|
||||
isUnicodeCodePointEscape(c1) ||
|
||||
isUnicodeCodePointEscape(c)
|
||||
)
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
combiningClass(chars) {
|
||||
return chars.some((c, i) => (
|
||||
i !== 0 &&
|
||||
isCombiningCharacter(c.value) &&
|
||||
!isCombiningCharacter(chars[i - 1].value)
|
||||
));
|
||||
},
|
||||
|
||||
emojiModifier(chars) {
|
||||
return chars.some((c, i) => (
|
||||
i !== 0 &&
|
||||
isEmojiModifier(c.value) &&
|
||||
!isEmojiModifier(chars[i - 1].value)
|
||||
));
|
||||
},
|
||||
|
||||
regionalIndicatorSymbol(chars) {
|
||||
return chars.some((c, i) => (
|
||||
i !== 0 &&
|
||||
isRegionalIndicatorSymbol(c.value) &&
|
||||
isRegionalIndicatorSymbol(chars[i - 1].value)
|
||||
));
|
||||
},
|
||||
|
||||
zwj(chars) {
|
||||
const lastIndex = chars.length - 1;
|
||||
|
||||
return chars.some((c, i) => (
|
||||
i !== 0 &&
|
||||
i !== lastIndex &&
|
||||
c.value === 0x200d &&
|
||||
chars[i - 1].value !== 0x200d &&
|
||||
chars[i + 1].value !== 0x200d
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
const kinds = Object.keys(hasCharacterSequence);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow characters which are made with multiple code points in character class syntax",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-misleading-character-class"
|
||||
},
|
||||
|
||||
hasSuggestions: true,
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
surrogatePairWithoutUFlag: "Unexpected surrogate pair in character class. Use 'u' flag.",
|
||||
surrogatePair: "Unexpected surrogate pair in character class.",
|
||||
combiningClass: "Unexpected combined character in character class.",
|
||||
emojiModifier: "Unexpected modified Emoji in character class.",
|
||||
regionalIndicatorSymbol: "Unexpected national flag in character class.",
|
||||
zwj: "Unexpected joined character sequence in character class.",
|
||||
suggestUnicodeFlag: "Add unicode 'u' flag to regex."
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const parser = new RegExpParser();
|
||||
|
||||
/**
|
||||
* Verify a given regular expression.
|
||||
* @param {Node} node The node to report.
|
||||
* @param {string} pattern The regular expression pattern to verify.
|
||||
* @param {string} flags The flags of the regular expression.
|
||||
* @param {Function} unicodeFixer Fixer for missing "u" flag.
|
||||
* @returns {void}
|
||||
*/
|
||||
function verify(node, pattern, flags, unicodeFixer) {
|
||||
let patternNode;
|
||||
|
||||
try {
|
||||
patternNode = parser.parsePattern(
|
||||
pattern,
|
||||
0,
|
||||
pattern.length,
|
||||
{
|
||||
unicode: flags.includes("u"),
|
||||
unicodeSets: flags.includes("v")
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
|
||||
// Ignore regular expressions with syntax errors
|
||||
return;
|
||||
}
|
||||
|
||||
const foundKinds = new Set();
|
||||
|
||||
visitRegExpAST(patternNode, {
|
||||
onCharacterClassEnter(ccNode) {
|
||||
for (const chars of iterateCharacterSequence(ccNode.elements)) {
|
||||
for (const kind of kinds) {
|
||||
if (hasCharacterSequence[kind](chars)) {
|
||||
foundKinds.add(kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const kind of foundKinds) {
|
||||
let suggest;
|
||||
|
||||
if (kind === "surrogatePairWithoutUFlag") {
|
||||
suggest = [{
|
||||
messageId: "suggestUnicodeFlag",
|
||||
fix: unicodeFixer
|
||||
}];
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: kind,
|
||||
suggest
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"Literal[regex]"(node) {
|
||||
verify(node, node.regex.pattern, node.regex.flags, fixer => {
|
||||
if (!isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, node.regex.pattern)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fixer.insertTextAfter(node, "u");
|
||||
});
|
||||
},
|
||||
"Program"(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
const tracker = new ReferenceTracker(scope);
|
||||
|
||||
/*
|
||||
* Iterate calls of RegExp.
|
||||
* E.g., `new RegExp()`, `RegExp()`, `new window.RegExp()`,
|
||||
* `const {RegExp: a} = window; new a()`, etc...
|
||||
*/
|
||||
for (const { node: refNode } of tracker.iterateGlobalReferences({
|
||||
RegExp: { [CALL]: true, [CONSTRUCT]: true }
|
||||
})) {
|
||||
const [patternNode, flagsNode] = refNode.arguments;
|
||||
const pattern = getStringIfConstant(patternNode, scope);
|
||||
const flags = getStringIfConstant(flagsNode, scope);
|
||||
|
||||
if (typeof pattern === "string") {
|
||||
verify(refNode, pattern, flags || "", fixer => {
|
||||
|
||||
if (!isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (refNode.arguments.length === 1) {
|
||||
const penultimateToken = sourceCode.getLastToken(refNode, { skip: 1 }); // skip closing parenthesis
|
||||
|
||||
return fixer.insertTextAfter(
|
||||
penultimateToken,
|
||||
astUtils.isCommaToken(penultimateToken)
|
||||
? ' "u",'
|
||||
: ', "u"'
|
||||
);
|
||||
}
|
||||
|
||||
if ((flagsNode.type === "Literal" && typeof flagsNode.value === "string") || flagsNode.type === "TemplateLiteral") {
|
||||
const range = [flagsNode.range[0], flagsNode.range[1] - 1];
|
||||
|
||||
return fixer.insertTextAfterRange(range, "u");
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow mixed binary operators.
|
||||
* @author Toru Nagashima
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils.js");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const ARITHMETIC_OPERATORS = ["+", "-", "*", "/", "%", "**"];
|
||||
const BITWISE_OPERATORS = ["&", "|", "^", "~", "<<", ">>", ">>>"];
|
||||
const COMPARISON_OPERATORS = ["==", "!=", "===", "!==", ">", ">=", "<", "<="];
|
||||
const LOGICAL_OPERATORS = ["&&", "||"];
|
||||
const RELATIONAL_OPERATORS = ["in", "instanceof"];
|
||||
const TERNARY_OPERATOR = ["?:"];
|
||||
const COALESCE_OPERATOR = ["??"];
|
||||
const ALL_OPERATORS = [].concat(
|
||||
ARITHMETIC_OPERATORS,
|
||||
BITWISE_OPERATORS,
|
||||
COMPARISON_OPERATORS,
|
||||
LOGICAL_OPERATORS,
|
||||
RELATIONAL_OPERATORS,
|
||||
TERNARY_OPERATOR,
|
||||
COALESCE_OPERATOR
|
||||
);
|
||||
const DEFAULT_GROUPS = [
|
||||
ARITHMETIC_OPERATORS,
|
||||
BITWISE_OPERATORS,
|
||||
COMPARISON_OPERATORS,
|
||||
LOGICAL_OPERATORS,
|
||||
RELATIONAL_OPERATORS
|
||||
];
|
||||
const TARGET_NODE_TYPE = /^(?:Binary|Logical|Conditional)Expression$/u;
|
||||
|
||||
/**
|
||||
* Normalizes options.
|
||||
* @param {Object|undefined} options A options object to normalize.
|
||||
* @returns {Object} Normalized option object.
|
||||
*/
|
||||
function normalizeOptions(options = {}) {
|
||||
const hasGroups = options.groups && options.groups.length > 0;
|
||||
const groups = hasGroups ? options.groups : DEFAULT_GROUPS;
|
||||
const allowSamePrecedence = options.allowSamePrecedence !== false;
|
||||
|
||||
return {
|
||||
groups,
|
||||
allowSamePrecedence
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether any group which includes both given operator exists or not.
|
||||
* @param {Array<string[]>} groups A list of groups to check.
|
||||
* @param {string} left An operator.
|
||||
* @param {string} right Another operator.
|
||||
* @returns {boolean} `true` if such group existed.
|
||||
*/
|
||||
function includesBothInAGroup(groups, left, right) {
|
||||
return groups.some(group => group.includes(left) && group.includes(right));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given node is a conditional expression and returns the test node else the left node.
|
||||
* @param {ASTNode} node A node which can be a BinaryExpression or a LogicalExpression node.
|
||||
* This parent node can be BinaryExpression, LogicalExpression
|
||||
* , or a ConditionalExpression node
|
||||
* @returns {ASTNode} node the appropriate node(left or test).
|
||||
*/
|
||||
function getChildNode(node) {
|
||||
return node.type === "ConditionalExpression" ? node.test : node.left;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow mixed binary operators",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-mixed-operators"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
groups: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "array",
|
||||
items: { enum: ALL_OPERATORS },
|
||||
minItems: 2,
|
||||
uniqueItems: true
|
||||
},
|
||||
uniqueItems: true
|
||||
},
|
||||
allowSamePrecedence: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedMixedOperator: "Unexpected mix of '{{leftOperator}}' and '{{rightOperator}}'. Use parentheses to clarify the intended order of operations."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const options = normalizeOptions(context.options[0]);
|
||||
|
||||
/**
|
||||
* Checks whether a given node should be ignored by options or not.
|
||||
* @param {ASTNode} node A node to check. This is a BinaryExpression
|
||||
* node or a LogicalExpression node. This parent node is one of
|
||||
* them, too.
|
||||
* @returns {boolean} `true` if the node should be ignored.
|
||||
*/
|
||||
function shouldIgnore(node) {
|
||||
const a = node;
|
||||
const b = node.parent;
|
||||
|
||||
return (
|
||||
!includesBothInAGroup(options.groups, a.operator, b.type === "ConditionalExpression" ? "?:" : b.operator) ||
|
||||
(
|
||||
options.allowSamePrecedence &&
|
||||
astUtils.getPrecedence(a) === astUtils.getPrecedence(b)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the operator of a given node is mixed with parent
|
||||
* node's operator or not.
|
||||
* @param {ASTNode} node A node to check. This is a BinaryExpression
|
||||
* node or a LogicalExpression node. This parent node is one of
|
||||
* them, too.
|
||||
* @returns {boolean} `true` if the node was mixed.
|
||||
*/
|
||||
function isMixedWithParent(node) {
|
||||
|
||||
return (
|
||||
node.operator !== node.parent.operator &&
|
||||
!astUtils.isParenthesised(sourceCode, node)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the operator token of a given node.
|
||||
* @param {ASTNode} node A node to check. This is a BinaryExpression
|
||||
* node or a LogicalExpression node.
|
||||
* @returns {Token} The operator token of the node.
|
||||
*/
|
||||
function getOperatorToken(node) {
|
||||
return sourceCode.getTokenAfter(getChildNode(node), astUtils.isNotClosingParenToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports both the operator of a given node and the operator of the
|
||||
* parent node.
|
||||
* @param {ASTNode} node A node to check. This is a BinaryExpression
|
||||
* node or a LogicalExpression node. This parent node is one of
|
||||
* them, too.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportBothOperators(node) {
|
||||
const parent = node.parent;
|
||||
const left = (getChildNode(parent) === node) ? node : parent;
|
||||
const right = (getChildNode(parent) !== node) ? node : parent;
|
||||
const data = {
|
||||
leftOperator: left.operator || "?:",
|
||||
rightOperator: right.operator || "?:"
|
||||
};
|
||||
|
||||
context.report({
|
||||
node: left,
|
||||
loc: getOperatorToken(left).loc,
|
||||
messageId: "unexpectedMixedOperator",
|
||||
data
|
||||
});
|
||||
context.report({
|
||||
node: right,
|
||||
loc: getOperatorToken(right).loc,
|
||||
messageId: "unexpectedMixedOperator",
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks between the operator of this node and the operator of the
|
||||
* parent node.
|
||||
* @param {ASTNode} node A node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function check(node) {
|
||||
if (
|
||||
TARGET_NODE_TYPE.test(node.parent.type) &&
|
||||
isMixedWithParent(node) &&
|
||||
!shouldIgnore(node)
|
||||
) {
|
||||
reportBothOperators(node);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
BinaryExpression: check,
|
||||
LogicalExpression: check
|
||||
};
|
||||
}
|
||||
};
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce grouped require statements for Node.JS
|
||||
* @author Raphael Pigulla
|
||||
* @deprecated in ESLint v7.0.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: [],
|
||||
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `require` calls to be mixed with regular variable declarations",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-mixed-requires"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "boolean"
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
grouping: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowCall: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
noMixRequire: "Do not mix 'require' and other declarations.",
|
||||
noMixCoreModuleFileComputed: "Do not mix core, module, file and computed requires."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const options = context.options[0];
|
||||
let grouping = false,
|
||||
allowCall = false;
|
||||
|
||||
if (typeof options === "object") {
|
||||
grouping = options.grouping;
|
||||
allowCall = options.allowCall;
|
||||
} else {
|
||||
grouping = !!options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of built-in modules.
|
||||
* @returns {string[]} An array of built-in Node.js modules.
|
||||
*/
|
||||
function getBuiltinModules() {
|
||||
|
||||
/*
|
||||
* This list is generated using:
|
||||
* `require("repl")._builtinLibs.concat('repl').sort()`
|
||||
* This particular list is as per nodejs v0.12.2 and iojs v0.7.1
|
||||
*/
|
||||
return [
|
||||
"assert", "buffer", "child_process", "cluster", "crypto",
|
||||
"dgram", "dns", "domain", "events", "fs", "http", "https",
|
||||
"net", "os", "path", "punycode", "querystring", "readline",
|
||||
"repl", "smalloc", "stream", "string_decoder", "tls", "tty",
|
||||
"url", "util", "v8", "vm", "zlib"
|
||||
];
|
||||
}
|
||||
|
||||
const BUILTIN_MODULES = getBuiltinModules();
|
||||
|
||||
const DECL_REQUIRE = "require",
|
||||
DECL_UNINITIALIZED = "uninitialized",
|
||||
DECL_OTHER = "other";
|
||||
|
||||
const REQ_CORE = "core",
|
||||
REQ_FILE = "file",
|
||||
REQ_MODULE = "module",
|
||||
REQ_COMPUTED = "computed";
|
||||
|
||||
/**
|
||||
* Determines the type of a declaration statement.
|
||||
* @param {ASTNode} initExpression The init node of the VariableDeclarator.
|
||||
* @returns {string} The type of declaration represented by the expression.
|
||||
*/
|
||||
function getDeclarationType(initExpression) {
|
||||
if (!initExpression) {
|
||||
|
||||
// "var x;"
|
||||
return DECL_UNINITIALIZED;
|
||||
}
|
||||
|
||||
if (initExpression.type === "CallExpression" &&
|
||||
initExpression.callee.type === "Identifier" &&
|
||||
initExpression.callee.name === "require"
|
||||
) {
|
||||
|
||||
// "var x = require('util');"
|
||||
return DECL_REQUIRE;
|
||||
}
|
||||
if (allowCall &&
|
||||
initExpression.type === "CallExpression" &&
|
||||
initExpression.callee.type === "CallExpression"
|
||||
) {
|
||||
|
||||
// "var x = require('diagnose')('sub-module');"
|
||||
return getDeclarationType(initExpression.callee);
|
||||
}
|
||||
if (initExpression.type === "MemberExpression") {
|
||||
|
||||
// "var x = require('glob').Glob;"
|
||||
return getDeclarationType(initExpression.object);
|
||||
}
|
||||
|
||||
// "var x = 42;"
|
||||
return DECL_OTHER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the type of module that is loaded via require.
|
||||
* @param {ASTNode} initExpression The init node of the VariableDeclarator.
|
||||
* @returns {string} The module type.
|
||||
*/
|
||||
function inferModuleType(initExpression) {
|
||||
if (initExpression.type === "MemberExpression") {
|
||||
|
||||
// "var x = require('glob').Glob;"
|
||||
return inferModuleType(initExpression.object);
|
||||
}
|
||||
if (initExpression.arguments.length === 0) {
|
||||
|
||||
// "var x = require();"
|
||||
return REQ_COMPUTED;
|
||||
}
|
||||
|
||||
const arg = initExpression.arguments[0];
|
||||
|
||||
if (arg.type !== "Literal" || typeof arg.value !== "string") {
|
||||
|
||||
// "var x = require(42);"
|
||||
return REQ_COMPUTED;
|
||||
}
|
||||
|
||||
if (BUILTIN_MODULES.includes(arg.value)) {
|
||||
|
||||
// "var fs = require('fs');"
|
||||
return REQ_CORE;
|
||||
}
|
||||
if (/^\.{0,2}\//u.test(arg.value)) {
|
||||
|
||||
// "var utils = require('./utils');"
|
||||
return REQ_FILE;
|
||||
}
|
||||
|
||||
// "var async = require('async');"
|
||||
return REQ_MODULE;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the list of variable declarations is mixed, i.e. whether it
|
||||
* contains both require and other declarations.
|
||||
* @param {ASTNode} declarations The list of VariableDeclarators.
|
||||
* @returns {boolean} True if the declarations are mixed, false if not.
|
||||
*/
|
||||
function isMixed(declarations) {
|
||||
const contains = {};
|
||||
|
||||
declarations.forEach(declaration => {
|
||||
const type = getDeclarationType(declaration.init);
|
||||
|
||||
contains[type] = true;
|
||||
});
|
||||
|
||||
return !!(
|
||||
contains[DECL_REQUIRE] &&
|
||||
(contains[DECL_UNINITIALIZED] || contains[DECL_OTHER])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all require declarations in the given list are of the same
|
||||
* type.
|
||||
* @param {ASTNode} declarations The list of VariableDeclarators.
|
||||
* @returns {boolean} True if the declarations are grouped, false if not.
|
||||
*/
|
||||
function isGrouped(declarations) {
|
||||
const found = {};
|
||||
|
||||
declarations.forEach(declaration => {
|
||||
if (getDeclarationType(declaration.init) === DECL_REQUIRE) {
|
||||
found[inferModuleType(declaration.init)] = true;
|
||||
}
|
||||
});
|
||||
|
||||
return Object.keys(found).length <= 1;
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
|
||||
VariableDeclaration(node) {
|
||||
|
||||
if (isMixed(node.declarations)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noMixRequire"
|
||||
});
|
||||
} else if (grouping && !isGrouped(node.declarations)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noMixCoreModuleFileComputed"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+116
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @fileoverview Disallow mixed spaces and tabs for indentation
|
||||
* @author Jary Niebur
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Disallow mixed spaces and tabs for indentation",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-mixed-spaces-and-tabs"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["smart-tabs", true, false]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
mixedSpacesAndTabs: "Mixed spaces and tabs."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
let smartTabs;
|
||||
|
||||
switch (context.options[0]) {
|
||||
case true: // Support old syntax, maybe add deprecation warning here
|
||||
case "smart-tabs":
|
||||
smartTabs = true;
|
||||
break;
|
||||
default:
|
||||
smartTabs = false;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
|
||||
"Program:exit"(node) {
|
||||
const lines = sourceCode.lines,
|
||||
comments = sourceCode.getAllComments(),
|
||||
ignoredCommentLines = new Set();
|
||||
|
||||
// Add all lines except the first ones.
|
||||
comments.forEach(comment => {
|
||||
for (let i = comment.loc.start.line + 1; i <= comment.loc.end.line; i++) {
|
||||
ignoredCommentLines.add(i);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* At least one space followed by a tab
|
||||
* or the reverse before non-tab/-space
|
||||
* characters begin.
|
||||
*/
|
||||
let regex = /^(?=( +|\t+))\1(?:\t| )/u;
|
||||
|
||||
if (smartTabs) {
|
||||
|
||||
/*
|
||||
* At least one space followed by a tab
|
||||
* before non-tab/-space characters begin.
|
||||
*/
|
||||
regex = /^(?=(\t*))\1(?=( +))\2\t/u;
|
||||
}
|
||||
|
||||
lines.forEach((line, i) => {
|
||||
const match = regex.exec(line);
|
||||
|
||||
if (match) {
|
||||
const lineNumber = i + 1;
|
||||
const loc = {
|
||||
start: {
|
||||
line: lineNumber,
|
||||
column: match[0].length - 2
|
||||
},
|
||||
end: {
|
||||
line: lineNumber,
|
||||
column: match[0].length
|
||||
}
|
||||
};
|
||||
|
||||
if (!ignoredCommentLines.has(lineNumber)) {
|
||||
const containingNode = sourceCode.getNodeByRangeIndex(sourceCode.getIndexFromLoc(loc.start));
|
||||
|
||||
if (!(containingNode && ["Literal", "TemplateElement"].includes(containingNode.type))) {
|
||||
context.report({
|
||||
node,
|
||||
loc,
|
||||
messageId: "mixedSpacesAndTabs"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @fileoverview Rule to check use of chained assignment expressions
|
||||
* @author Stewart Rand
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow use of chained assignment expressions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-multi-assign"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
ignoreNonDeclaration: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
unexpectedChain: "Unexpected chained assignment."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
const options = context.options[0] || {
|
||||
ignoreNonDeclaration: false
|
||||
};
|
||||
const selectors = [
|
||||
"VariableDeclarator > AssignmentExpression.init",
|
||||
"PropertyDefinition > AssignmentExpression.value"
|
||||
];
|
||||
|
||||
if (!options.ignoreNonDeclaration) {
|
||||
selectors.push("AssignmentExpression > AssignmentExpression.right");
|
||||
}
|
||||
|
||||
return {
|
||||
[selectors](node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedChain"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* @fileoverview Disallow use of multiple spaces.
|
||||
* @author Nicholas C. Zakas
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Disallow multiple spaces",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-multi-spaces"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
exceptions: {
|
||||
type: "object",
|
||||
patternProperties: {
|
||||
"^([A-Z][a-z]*)+$": {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
ignoreEOLComments: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
multipleSpaces: "Multiple spaces found before '{{displayValue}}'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const options = context.options[0] || {};
|
||||
const ignoreEOLComments = options.ignoreEOLComments;
|
||||
const exceptions = Object.assign({ Property: true }, options.exceptions);
|
||||
const hasExceptions = Object.keys(exceptions).some(key => exceptions[key]);
|
||||
|
||||
/**
|
||||
* Formats value of given comment token for error message by truncating its length.
|
||||
* @param {Token} token comment token
|
||||
* @returns {string} formatted value
|
||||
* @private
|
||||
*/
|
||||
function formatReportedCommentValue(token) {
|
||||
const valueLines = token.value.split("\n");
|
||||
const value = valueLines[0];
|
||||
const formattedValue = `${value.slice(0, 12)}...`;
|
||||
|
||||
return valueLines.length === 1 && value.length <= 12 ? value : formattedValue;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program() {
|
||||
sourceCode.tokensAndComments.forEach((leftToken, leftIndex, tokensAndComments) => {
|
||||
if (leftIndex === tokensAndComments.length - 1) {
|
||||
return;
|
||||
}
|
||||
const rightToken = tokensAndComments[leftIndex + 1];
|
||||
|
||||
// Ignore tokens that don't have 2 spaces between them or are on different lines
|
||||
if (
|
||||
!sourceCode.text.slice(leftToken.range[1], rightToken.range[0]).includes(" ") ||
|
||||
leftToken.loc.end.line < rightToken.loc.start.line
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore comments that are the last token on their line if `ignoreEOLComments` is active.
|
||||
if (
|
||||
ignoreEOLComments &&
|
||||
astUtils.isCommentToken(rightToken) &&
|
||||
(
|
||||
leftIndex === tokensAndComments.length - 2 ||
|
||||
rightToken.loc.end.line < tokensAndComments[leftIndex + 2].loc.start.line
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore tokens that are in a node in the "exceptions" object
|
||||
if (hasExceptions) {
|
||||
const parentNode = sourceCode.getNodeByRangeIndex(rightToken.range[0] - 1);
|
||||
|
||||
if (parentNode && exceptions[parentNode.type]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let displayValue;
|
||||
|
||||
if (rightToken.type === "Block") {
|
||||
displayValue = `/*${formatReportedCommentValue(rightToken)}*/`;
|
||||
} else if (rightToken.type === "Line") {
|
||||
displayValue = `//${formatReportedCommentValue(rightToken)}`;
|
||||
} else {
|
||||
displayValue = rightToken.value;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: rightToken,
|
||||
loc: { start: leftToken.loc.end, end: rightToken.loc.start },
|
||||
messageId: "multipleSpaces",
|
||||
data: { displayValue },
|
||||
fix: fixer => fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], " ")
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag when using multiline strings
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow multiline strings",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-multi-str"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
multilineString: "Multiline support is limited to browsers supporting ES5 only."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/**
|
||||
* Determines if a given node is part of JSX syntax.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} True if the node is a JSX node, false if not.
|
||||
* @private
|
||||
*/
|
||||
function isJSXElement(node) {
|
||||
return node.type.indexOf("JSX") === 0;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
|
||||
Literal(node) {
|
||||
if (astUtils.LINEBREAK_MATCHER.test(node.raw) && !isJSXElement(node.parent)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "multilineString"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* @fileoverview Disallows multiple blank lines.
|
||||
* implementation adapted from the no-trailing-spaces rule.
|
||||
* @author Greg Cochard
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Disallow multiple empty lines",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-multiple-empty-lines"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
max: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
maxEOF: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
maxBOF: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
required: ["max"],
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
blankBeginningOfFile: "Too many blank lines at the beginning of file. Max of {{max}} allowed.",
|
||||
blankEndOfFile: "Too many blank lines at the end of file. Max of {{max}} allowed.",
|
||||
consecutiveBlank: "More than {{max}} blank {{pluralizedLines}} not allowed."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
// Use options.max or 2 as default
|
||||
let max = 2,
|
||||
maxEOF = max,
|
||||
maxBOF = max;
|
||||
|
||||
if (context.options.length) {
|
||||
max = context.options[0].max;
|
||||
maxEOF = typeof context.options[0].maxEOF !== "undefined" ? context.options[0].maxEOF : max;
|
||||
maxBOF = typeof context.options[0].maxBOF !== "undefined" ? context.options[0].maxBOF : max;
|
||||
}
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
// Swallow the final newline, as some editors add it automatically and we don't want it to cause an issue
|
||||
const allLines = sourceCode.lines[sourceCode.lines.length - 1] === "" ? sourceCode.lines.slice(0, -1) : sourceCode.lines;
|
||||
const templateLiteralLines = new Set();
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
TemplateLiteral(node) {
|
||||
node.quasis.forEach(literalPart => {
|
||||
|
||||
// Empty lines have a semantic meaning if they're inside template literals. Don't count these as empty lines.
|
||||
for (let ignoredLine = literalPart.loc.start.line; ignoredLine < literalPart.loc.end.line; ignoredLine++) {
|
||||
templateLiteralLines.add(ignoredLine);
|
||||
}
|
||||
});
|
||||
},
|
||||
"Program:exit"(node) {
|
||||
return allLines
|
||||
|
||||
// Given a list of lines, first get a list of line numbers that are non-empty.
|
||||
.reduce((nonEmptyLineNumbers, line, index) => {
|
||||
if (line.trim() || templateLiteralLines.has(index + 1)) {
|
||||
nonEmptyLineNumbers.push(index + 1);
|
||||
}
|
||||
return nonEmptyLineNumbers;
|
||||
}, [])
|
||||
|
||||
// Add a value at the end to allow trailing empty lines to be checked.
|
||||
.concat(allLines.length + 1)
|
||||
|
||||
// Given two line numbers of non-empty lines, report the lines between if the difference is too large.
|
||||
.reduce((lastLineNumber, lineNumber) => {
|
||||
let messageId, maxAllowed;
|
||||
|
||||
if (lastLineNumber === 0) {
|
||||
messageId = "blankBeginningOfFile";
|
||||
maxAllowed = maxBOF;
|
||||
} else if (lineNumber === allLines.length + 1) {
|
||||
messageId = "blankEndOfFile";
|
||||
maxAllowed = maxEOF;
|
||||
} else {
|
||||
messageId = "consecutiveBlank";
|
||||
maxAllowed = max;
|
||||
}
|
||||
|
||||
if (lineNumber - lastLineNumber - 1 > maxAllowed) {
|
||||
context.report({
|
||||
node,
|
||||
loc: {
|
||||
start: { line: lastLineNumber + maxAllowed + 1, column: 0 },
|
||||
end: { line: lineNumber, column: 0 }
|
||||
},
|
||||
messageId,
|
||||
data: {
|
||||
max: maxAllowed,
|
||||
pluralizedLines: maxAllowed === 1 ? "line" : "lines"
|
||||
},
|
||||
fix(fixer) {
|
||||
const rangeStart = sourceCode.getIndexFromLoc({ line: lastLineNumber + 1, column: 0 });
|
||||
|
||||
/*
|
||||
* The end of the removal range is usually the start index of the next line.
|
||||
* However, at the end of the file there is no next line, so the end of the
|
||||
* range is just the length of the text.
|
||||
*/
|
||||
const lineNumberAfterRemovedLines = lineNumber - maxAllowed;
|
||||
const rangeEnd = lineNumberAfterRemovedLines <= allLines.length
|
||||
? sourceCode.getIndexFromLoc({ line: lineNumberAfterRemovedLines, column: 0 })
|
||||
: sourceCode.text.length;
|
||||
|
||||
return fixer.removeRange([rangeStart, rangeEnd]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return lineNumber;
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow assignments to native objects or read-only global variables
|
||||
* @author Ilya Volodin
|
||||
* @deprecated in ESLint v3.3.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow assignments to native objects or read-only global variables",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-native-reassign"
|
||||
},
|
||||
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: ["no-global-assign"],
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
exceptions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
nativeReassign: "Read-only global '{{name}}' should not be modified."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0];
|
||||
const exceptions = (config && config.exceptions) || [];
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Reports write references.
|
||||
* @param {Reference} reference A reference to check.
|
||||
* @param {int} index The index of the reference in the references.
|
||||
* @param {Reference[]} references The array that the reference belongs to.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkReference(reference, index, references) {
|
||||
const identifier = reference.identifier;
|
||||
|
||||
if (reference.init === false &&
|
||||
reference.isWrite() &&
|
||||
|
||||
/*
|
||||
* Destructuring assignments can have multiple default value,
|
||||
* so possibly there are multiple writeable references for the same identifier.
|
||||
*/
|
||||
(index === 0 || references[index - 1].identifier !== identifier)
|
||||
) {
|
||||
context.report({
|
||||
node: identifier,
|
||||
messageId: "nativeReassign",
|
||||
data: identifier
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports write references if a given variable is read-only builtin.
|
||||
* @param {Variable} variable A variable to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkVariable(variable) {
|
||||
if (variable.writeable === false && !exceptions.includes(variable.name)) {
|
||||
variable.references.forEach(checkReference);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
const globalScope = sourceCode.getScope(node);
|
||||
|
||||
globalScope.variables.forEach(checkVariable);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow a negated condition
|
||||
* @author Alberto Rodríguez
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow negated conditions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-negated-condition"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpectedNegated: "Unexpected negated condition."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/**
|
||||
* Determines if a given node is an if-else without a condition on the else
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} True if the node has an else without an if.
|
||||
* @private
|
||||
*/
|
||||
function hasElseWithoutCondition(node) {
|
||||
return node.alternate && node.alternate.type !== "IfStatement";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given node is a negated unary expression
|
||||
* @param {Object} test The test object to check.
|
||||
* @returns {boolean} True if the node is a negated unary expression.
|
||||
* @private
|
||||
*/
|
||||
function isNegatedUnaryExpression(test) {
|
||||
return test.type === "UnaryExpression" && test.operator === "!";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given node is a negated binary expression
|
||||
* @param {Test} test The test to check.
|
||||
* @returns {boolean} True if the node is a negated binary expression.
|
||||
* @private
|
||||
*/
|
||||
function isNegatedBinaryExpression(test) {
|
||||
return test.type === "BinaryExpression" &&
|
||||
(test.operator === "!=" || test.operator === "!==");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given node has a negated if expression
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} True if the node has a negated if expression.
|
||||
* @private
|
||||
*/
|
||||
function isNegatedIf(node) {
|
||||
return isNegatedUnaryExpression(node.test) || isNegatedBinaryExpression(node.test);
|
||||
}
|
||||
|
||||
return {
|
||||
IfStatement(node) {
|
||||
if (!hasElseWithoutCondition(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNegatedIf(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedNegated"
|
||||
});
|
||||
}
|
||||
},
|
||||
ConditionalExpression(node) {
|
||||
if (isNegatedIf(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedNegated"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @fileoverview A rule to disallow negated left operands of the `in` operator
|
||||
* @author Michael Ficarra
|
||||
* @deprecated in ESLint v3.3.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow negating the left operand in `in` expressions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-negated-in-lhs"
|
||||
},
|
||||
|
||||
replacedBy: ["no-unsafe-negation"],
|
||||
|
||||
deprecated: true,
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
negatedLHS: "The 'in' expression's left operand is negated."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
|
||||
BinaryExpression(node) {
|
||||
if (node.operator === "in" && node.left.type === "UnaryExpression" && node.left.operator === "!") {
|
||||
context.report({ node, messageId: "negatedLHS" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag nested ternary expressions
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow nested ternary expressions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-nested-ternary"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
noNestedTernary: "Do not nest ternary expressions."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
ConditionalExpression(node) {
|
||||
if (node.alternate.type === "ConditionalExpression" ||
|
||||
node.consequent.type === "ConditionalExpression") {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noNestedTernary"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag when using new Function
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const callMethods = new Set(["apply", "bind", "call"]);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `new` operators with the `Function` object",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-new-func"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
noFunctionConstructor: "The Function constructor is eval."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
"Program:exit"(node) {
|
||||
const globalScope = sourceCode.getScope(node);
|
||||
const variable = globalScope.set.get("Function");
|
||||
|
||||
if (variable && variable.defs.length === 0) {
|
||||
variable.references.forEach(ref => {
|
||||
const idNode = ref.identifier;
|
||||
const { parent } = idNode;
|
||||
let evalNode;
|
||||
|
||||
if (parent) {
|
||||
if (idNode === parent.callee && (
|
||||
parent.type === "NewExpression" ||
|
||||
parent.type === "CallExpression"
|
||||
)) {
|
||||
evalNode = parent;
|
||||
} else if (
|
||||
parent.type === "MemberExpression" &&
|
||||
idNode === parent.object &&
|
||||
callMethods.has(astUtils.getStaticPropertyName(parent))
|
||||
) {
|
||||
const maybeCallee = parent.parent.type === "ChainExpression" ? parent.parent : parent;
|
||||
|
||||
if (maybeCallee.parent.type === "CallExpression" && maybeCallee.parent.callee === maybeCallee) {
|
||||
evalNode = maybeCallee.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (evalNode) {
|
||||
context.report({
|
||||
node: evalNode,
|
||||
messageId: "noFunctionConstructor"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow use of the new operator with global non-constructor functions
|
||||
* @author Sosuke Suzuki
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const nonConstructorGlobalFunctionNames = ["Symbol", "BigInt"];
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `new` operators with global non-constructor functions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-new-native-nonconstructor"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
noNewNonconstructor: "`{{name}}` cannot be called as a constructor."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
"Program:exit"(node) {
|
||||
const globalScope = sourceCode.getScope(node);
|
||||
|
||||
for (const nonConstructorName of nonConstructorGlobalFunctionNames) {
|
||||
const variable = globalScope.set.get(nonConstructorName);
|
||||
|
||||
if (variable && variable.defs.length === 0) {
|
||||
variable.references.forEach(ref => {
|
||||
const idNode = ref.identifier;
|
||||
const parent = idNode.parent;
|
||||
|
||||
if (parent && parent.type === "NewExpression" && parent.callee === idNode) {
|
||||
context.report({
|
||||
node: idNode,
|
||||
messageId: "noNewNonconstructor",
|
||||
data: { name: nonConstructorName }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @fileoverview A rule to disallow calls to the Object constructor
|
||||
* @author Matt DuVall <http://www.mattduvall.com/>
|
||||
* @deprecated in ESLint v8.50.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `Object` constructors",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-new-object"
|
||||
},
|
||||
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: [
|
||||
"no-object-constructor"
|
||||
],
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
preferLiteral: "The object literal notation {} is preferable."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
NewExpression(node) {
|
||||
const variable = astUtils.getVariableByName(
|
||||
sourceCode.getScope(node),
|
||||
node.callee.name
|
||||
);
|
||||
|
||||
if (variable && variable.identifiers.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.callee.name === "Object") {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "preferLiteral"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow use of new operator with the `require` function
|
||||
* @author Wil Moore III
|
||||
* @deprecated in ESLint v7.0.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: [],
|
||||
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `new` operators with calls to `require`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-new-require"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
noNewRequire: "Unexpected use of new with require."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
|
||||
NewExpression(node) {
|
||||
if (node.callee.type === "Identifier" && node.callee.name === "require") {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noNewRequire"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow use of the new operator with the `Symbol` object
|
||||
* @author Alberto Rodríguez
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `new` operators with the `Symbol` object",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-new-symbol"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
noNewSymbol: "`Symbol` cannot be called as a constructor."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
"Program:exit"(node) {
|
||||
const globalScope = sourceCode.getScope(node);
|
||||
const variable = globalScope.set.get("Symbol");
|
||||
|
||||
if (variable && variable.defs.length === 0) {
|
||||
variable.references.forEach(ref => {
|
||||
const idNode = ref.identifier;
|
||||
const parent = idNode.parent;
|
||||
|
||||
if (parent && parent.type === "NewExpression" && parent.callee === idNode) {
|
||||
context.report({
|
||||
node: idNode,
|
||||
messageId: "noNewSymbol"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag when using constructor for wrapper objects
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const { getVariableByName } = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `new` operators with the `String`, `Number`, and `Boolean` objects",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-new-wrappers"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
noConstructor: "Do not use {{fn}} as a constructor."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
|
||||
return {
|
||||
|
||||
NewExpression(node) {
|
||||
const wrapperObjects = ["String", "Number", "Boolean"];
|
||||
const { name } = node.callee;
|
||||
|
||||
if (wrapperObjects.includes(name)) {
|
||||
const variable = getVariableByName(sourceCode.getScope(node), name);
|
||||
|
||||
if (variable && variable.identifiers.length === 0) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noConstructor",
|
||||
data: { fn: name }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag statements with function invocation preceded by
|
||||
* "new" and not part of assignment
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `new` operators outside of assignments or comparisons",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-new"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
noNewStatement: "Do not use 'new' for side effects."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
"ExpressionStatement > NewExpression"(node) {
|
||||
context.report({
|
||||
node: node.parent,
|
||||
messageId: "noNewStatement"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+148
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow `\8` and `\9` escape sequences in string literals.
|
||||
* @author Milos Djermanovic
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const QUICK_TEST_REGEX = /\\[89]/u;
|
||||
|
||||
/**
|
||||
* Returns unicode escape sequence that represents the given character.
|
||||
* @param {string} character A single code unit.
|
||||
* @returns {string} "\uXXXX" sequence.
|
||||
*/
|
||||
function getUnicodeEscape(character) {
|
||||
return `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `\\8` and `\\9` escape sequences in string literals",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-nonoctal-decimal-escape"
|
||||
},
|
||||
|
||||
hasSuggestions: true,
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
decimalEscape: "Don't use '{{decimalEscape}}' escape sequence.",
|
||||
|
||||
// suggestions
|
||||
refactor: "Replace '{{original}}' with '{{replacement}}'. This maintains the current functionality.",
|
||||
escapeBackslash: "Replace '{{original}}' with '{{replacement}}' to include the actual backslash character."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Creates a new Suggestion object.
|
||||
* @param {string} messageId "refactor" or "escapeBackslash".
|
||||
* @param {int[]} range The range to replace.
|
||||
* @param {string} replacement New text for the range.
|
||||
* @returns {Object} Suggestion
|
||||
*/
|
||||
function createSuggestion(messageId, range, replacement) {
|
||||
return {
|
||||
messageId,
|
||||
data: {
|
||||
original: sourceCode.getText().slice(...range),
|
||||
replacement
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange(range, replacement);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
Literal(node) {
|
||||
if (typeof node.value !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!QUICK_TEST_REGEX.test(node.raw)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const regex = /(?:[^\\]|(?<previousEscape>\\.))*?(?<decimalEscape>\\[89])/suy;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(node.raw))) {
|
||||
const { previousEscape, decimalEscape } = match.groups;
|
||||
const decimalEscapeRangeEnd = node.range[0] + match.index + match[0].length;
|
||||
const decimalEscapeRangeStart = decimalEscapeRangeEnd - decimalEscape.length;
|
||||
const decimalEscapeRange = [decimalEscapeRangeStart, decimalEscapeRangeEnd];
|
||||
const suggest = [];
|
||||
|
||||
// When `regex` is matched, `previousEscape` can only capture characters adjacent to `decimalEscape`
|
||||
if (previousEscape === "\\0") {
|
||||
|
||||
/*
|
||||
* Now we have a NULL escape "\0" immediately followed by a decimal escape, e.g.: "\0\8".
|
||||
* Fixing this to "\08" would turn "\0" into a legacy octal escape. To avoid producing
|
||||
* an octal escape while fixing a decimal escape, we provide different suggestions.
|
||||
*/
|
||||
suggest.push(
|
||||
createSuggestion( // "\0\8" -> "\u00008"
|
||||
"refactor",
|
||||
[decimalEscapeRangeStart - previousEscape.length, decimalEscapeRangeEnd],
|
||||
`${getUnicodeEscape("\0")}${decimalEscape[1]}`
|
||||
),
|
||||
createSuggestion( // "\8" -> "\u0038"
|
||||
"refactor",
|
||||
decimalEscapeRange,
|
||||
getUnicodeEscape(decimalEscape[1])
|
||||
)
|
||||
);
|
||||
} else {
|
||||
suggest.push(
|
||||
createSuggestion( // "\8" -> "8"
|
||||
"refactor",
|
||||
decimalEscapeRange,
|
||||
decimalEscape[1]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
suggest.push(
|
||||
createSuggestion( // "\8" -> "\\8"
|
||||
"escapeBackslash",
|
||||
decimalEscapeRange,
|
||||
`\\${decimalEscape}`
|
||||
)
|
||||
);
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: {
|
||||
start: sourceCode.getLocFromIndex(decimalEscapeRangeStart),
|
||||
end: sourceCode.getLocFromIndex(decimalEscapeRangeEnd)
|
||||
},
|
||||
messageId: "decimalEscape",
|
||||
data: {
|
||||
decimalEscape
|
||||
},
|
||||
suggest
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of an object property of the global object (Math and JSON) as a function
|
||||
* @author James Allardice
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const { CALL, CONSTRUCT, ReferenceTracker } = require("@eslint-community/eslint-utils");
|
||||
const getPropertyName = require("./utils/ast-utils").getStaticPropertyName;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const nonCallableGlobals = ["Atomics", "JSON", "Math", "Reflect", "Intl"];
|
||||
|
||||
/**
|
||||
* Returns the name of the node to report
|
||||
* @param {ASTNode} node A node to report
|
||||
* @returns {string} name to report
|
||||
*/
|
||||
function getReportNodeName(node) {
|
||||
if (node.type === "ChainExpression") {
|
||||
return getReportNodeName(node.expression);
|
||||
}
|
||||
if (node.type === "MemberExpression") {
|
||||
return getPropertyName(node);
|
||||
}
|
||||
return node.name;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow calling global object properties as functions",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-obj-calls"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpectedCall: "'{{name}}' is not a function.",
|
||||
unexpectedRefCall: "'{{name}}' is reference to '{{ref}}', which is not a function."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
const tracker = new ReferenceTracker(scope);
|
||||
const traceMap = {};
|
||||
|
||||
for (const g of nonCallableGlobals) {
|
||||
traceMap[g] = {
|
||||
[CALL]: true,
|
||||
[CONSTRUCT]: true
|
||||
};
|
||||
}
|
||||
|
||||
for (const { node: refNode, path } of tracker.iterateGlobalReferences(traceMap)) {
|
||||
const name = getReportNodeName(refNode.callee);
|
||||
const ref = path[0];
|
||||
const messageId = name === ref ? "unexpectedCall" : "unexpectedRefCall";
|
||||
|
||||
context.report({ node: refNode, messageId, data: { name, ref } });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow calls to the `Object` constructor without an argument
|
||||
* @author Francesco Trotta
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
getVariableByName,
|
||||
isArrowToken,
|
||||
isStartOfExpressionStatement,
|
||||
needsPrecedingSemicolon
|
||||
} = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow calls to the `Object` constructor without an argument",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-object-constructor"
|
||||
},
|
||||
|
||||
hasSuggestions: true,
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
preferLiteral: "The object literal notation {} is preferable.",
|
||||
useLiteral: "Replace with '{{replacement}}'.",
|
||||
useLiteralAfterSemicolon: "Replace with '{{replacement}}', add preceding semicolon."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Determines whether or not an object literal that replaces a specified node needs to be enclosed in parentheses.
|
||||
* @param {ASTNode} node The node to be replaced.
|
||||
* @returns {boolean} Whether or not parentheses around the object literal are required.
|
||||
*/
|
||||
function needsParentheses(node) {
|
||||
if (isStartOfExpressionStatement(node)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const prevToken = sourceCode.getTokenBefore(node);
|
||||
|
||||
if (prevToken && isArrowToken(prevToken)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports on nodes where the `Object` constructor is called without arguments.
|
||||
* @param {ASTNode} node The node to evaluate.
|
||||
* @returns {void}
|
||||
*/
|
||||
function check(node) {
|
||||
if (node.callee.type !== "Identifier" || node.callee.name !== "Object" || node.arguments.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const variable = getVariableByName(sourceCode.getScope(node), "Object");
|
||||
|
||||
if (variable && variable.identifiers.length === 0) {
|
||||
let replacement;
|
||||
let fixText;
|
||||
let messageId = "useLiteral";
|
||||
|
||||
if (needsParentheses(node)) {
|
||||
replacement = "({})";
|
||||
if (needsPrecedingSemicolon(sourceCode, node)) {
|
||||
fixText = ";({})";
|
||||
messageId = "useLiteralAfterSemicolon";
|
||||
} else {
|
||||
fixText = "({})";
|
||||
}
|
||||
} else {
|
||||
replacement = fixText = "{}";
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "preferLiteral",
|
||||
suggest: [
|
||||
{
|
||||
messageId,
|
||||
data: { replacement },
|
||||
fix: fixer => fixer.replaceText(node, fixText)
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression: check,
|
||||
NewExpression: check
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag octal escape sequences in string literals.
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow octal escape sequences in string literals",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-octal-escape"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
octalEscapeSequence: "Don't use octal: '\\{{sequence}}'. Use '\\u....' instead."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
|
||||
Literal(node) {
|
||||
if (typeof node.value !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
// \0 represents a valid NULL character if it isn't followed by a digit.
|
||||
const match = node.raw.match(
|
||||
/^(?:[^\\]|\\.)*?\\([0-3][0-7]{1,2}|[4-7][0-7]|0(?=[89])|[1-7])/su
|
||||
);
|
||||
|
||||
if (match) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "octalEscapeSequence",
|
||||
data: { sequence: match[1] }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag when initializing octal literal
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow octal literals",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-octal"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
noOctal: "Octal literals should not be used."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
|
||||
Literal(node) {
|
||||
if (typeof node.value === "number" && /^0[0-9]/u.test(node.raw)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noOctal"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* @fileoverview Disallow reassignment of function parameters.
|
||||
* @author Nat Burns
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const stopNodePattern = /(?:Statement|Declaration|Function(?:Expression)?|Program)$/u;
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow reassigning `function` parameters",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-param-reassign"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
props: {
|
||||
enum: [false]
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
props: {
|
||||
enum: [true]
|
||||
},
|
||||
ignorePropertyModificationsFor: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
},
|
||||
uniqueItems: true
|
||||
},
|
||||
ignorePropertyModificationsForRegex: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
},
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
assignmentToFunctionParam: "Assignment to function parameter '{{name}}'.",
|
||||
assignmentToFunctionParamProp: "Assignment to property of function parameter '{{name}}'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const props = context.options[0] && context.options[0].props;
|
||||
const ignoredPropertyAssignmentsFor = context.options[0] && context.options[0].ignorePropertyModificationsFor || [];
|
||||
const ignoredPropertyAssignmentsForRegex = context.options[0] && context.options[0].ignorePropertyModificationsForRegex || [];
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Checks whether or not the reference modifies properties of its variable.
|
||||
* @param {Reference} reference A reference to check.
|
||||
* @returns {boolean} Whether or not the reference modifies properties of its variable.
|
||||
*/
|
||||
function isModifyingProp(reference) {
|
||||
let node = reference.identifier;
|
||||
let parent = node.parent;
|
||||
|
||||
while (parent && (!stopNodePattern.test(parent.type) ||
|
||||
parent.type === "ForInStatement" || parent.type === "ForOfStatement")) {
|
||||
switch (parent.type) {
|
||||
|
||||
// e.g. foo.a = 0;
|
||||
case "AssignmentExpression":
|
||||
return parent.left === node;
|
||||
|
||||
// e.g. ++foo.a;
|
||||
case "UpdateExpression":
|
||||
return true;
|
||||
|
||||
// e.g. delete foo.a;
|
||||
case "UnaryExpression":
|
||||
if (parent.operator === "delete") {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
// e.g. for (foo.a in b) {}
|
||||
case "ForInStatement":
|
||||
case "ForOfStatement":
|
||||
if (parent.left === node) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// this is a stop node for parent.right and parent.body
|
||||
return false;
|
||||
|
||||
// EXCLUDES: e.g. cache.get(foo.a).b = 0;
|
||||
case "CallExpression":
|
||||
if (parent.callee !== node) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
// EXCLUDES: e.g. cache[foo.a] = 0;
|
||||
case "MemberExpression":
|
||||
if (parent.property === node) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
// EXCLUDES: e.g. ({ [foo]: a }) = bar;
|
||||
case "Property":
|
||||
if (parent.key === node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// EXCLUDES: e.g. (foo ? a : b).c = bar;
|
||||
case "ConditionalExpression":
|
||||
if (parent.test === node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// no default
|
||||
}
|
||||
|
||||
node = parent;
|
||||
parent = node.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that an identifier name matches any of the ignored property assignments.
|
||||
* First we test strings in ignoredPropertyAssignmentsFor.
|
||||
* Then we instantiate and test RegExp objects from ignoredPropertyAssignmentsForRegex strings.
|
||||
* @param {string} identifierName A string that describes the name of an identifier to
|
||||
* ignore property assignments for.
|
||||
* @returns {boolean} Whether the string matches an ignored property assignment regular expression or not.
|
||||
*/
|
||||
function isIgnoredPropertyAssignment(identifierName) {
|
||||
return ignoredPropertyAssignmentsFor.includes(identifierName) ||
|
||||
ignoredPropertyAssignmentsForRegex.some(ignored => new RegExp(ignored, "u").test(identifierName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a reference if is non initializer and writable.
|
||||
* @param {Reference} reference A reference to check.
|
||||
* @param {int} index The index of the reference in the references.
|
||||
* @param {Reference[]} references The array that the reference belongs to.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkReference(reference, index, references) {
|
||||
const identifier = reference.identifier;
|
||||
|
||||
if (identifier &&
|
||||
!reference.init &&
|
||||
|
||||
/*
|
||||
* Destructuring assignments can have multiple default value,
|
||||
* so possibly there are multiple writeable references for the same identifier.
|
||||
*/
|
||||
(index === 0 || references[index - 1].identifier !== identifier)
|
||||
) {
|
||||
if (reference.isWrite()) {
|
||||
context.report({
|
||||
node: identifier,
|
||||
messageId: "assignmentToFunctionParam",
|
||||
data: { name: identifier.name }
|
||||
});
|
||||
} else if (props && isModifyingProp(reference) && !isIgnoredPropertyAssignment(identifier.name)) {
|
||||
context.report({
|
||||
node: identifier,
|
||||
messageId: "assignmentToFunctionParamProp",
|
||||
data: { name: identifier.name }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and reports references that are non initializer and writable.
|
||||
* @param {Variable} variable A variable to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkVariable(variable) {
|
||||
if (variable.defs[0].type === "Parameter") {
|
||||
variable.references.forEach(checkReference);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks parameters of a given function node.
|
||||
* @param {ASTNode} node A function node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkForFunction(node) {
|
||||
sourceCode.getDeclaredVariables(node).forEach(checkVariable);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
// `:exit` is needed for the `node.parent` property of identifier nodes.
|
||||
"FunctionDeclaration:exit": checkForFunction,
|
||||
"FunctionExpression:exit": checkForFunction,
|
||||
"ArrowFunctionExpression:exit": checkForFunction
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @fileoverview Disallow string concatenation when using __dirname and __filename
|
||||
* @author Nicholas C. Zakas
|
||||
* @deprecated in ESLint v7.0.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: [],
|
||||
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow string concatenation with `__dirname` and `__filename`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-path-concat"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
usePathFunctions: "Use path.join() or path.resolve() instead of + to create paths."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const MATCHER = /^__(?:dir|file)name$/u;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
|
||||
BinaryExpression(node) {
|
||||
|
||||
const left = node.left,
|
||||
right = node.right;
|
||||
|
||||
if (node.operator === "+" &&
|
||||
((left.type === "Identifier" && MATCHER.test(left.name)) ||
|
||||
(right.type === "Identifier" && MATCHER.test(right.name)))
|
||||
) {
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "usePathFunctions"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of unary increment and decrement operators.
|
||||
* @author Ian Christian Myers
|
||||
* @author Brody McKee (github.com/mrmckeb)
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines whether the given node is the update node of a `ForStatement`.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} `true` if the node is `ForStatement` update.
|
||||
*/
|
||||
function isForStatementUpdate(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
return parent.type === "ForStatement" && parent.update === node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given node is considered to be a for loop "afterthought" by the logic of this rule.
|
||||
* In particular, it returns `true` if the given node is either:
|
||||
* - The update node of a `ForStatement`: for (;; i++) {}
|
||||
* - An operand of a sequence expression that is the update node: for (;; foo(), i++) {}
|
||||
* - An operand of a sequence expression that is child of another sequence expression, etc.,
|
||||
* up to the sequence expression that is the update node: for (;; foo(), (bar(), (baz(), i++))) {}
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} `true` if the node is a for loop afterthought.
|
||||
*/
|
||||
function isForLoopAfterthought(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
if (parent.type === "SequenceExpression") {
|
||||
return isForLoopAfterthought(parent);
|
||||
}
|
||||
|
||||
return isForStatementUpdate(node);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow the unary operators `++` and `--`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-plusplus"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowForLoopAfterthoughts: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedUnaryOp: "Unary operator '{{operator}}' used."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const config = context.options[0];
|
||||
let allowForLoopAfterthoughts = false;
|
||||
|
||||
if (typeof config === "object") {
|
||||
allowForLoopAfterthoughts = config.allowForLoopAfterthoughts === true;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
UpdateExpression(node) {
|
||||
if (allowForLoopAfterthoughts && isForLoopAfterthought(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedUnaryOp",
|
||||
data: {
|
||||
operator: node.operator
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @fileoverview Disallow the use of process.env()
|
||||
* @author Vignesh Anand
|
||||
* @deprecated in ESLint v7.0.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: [],
|
||||
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow the use of `process.env`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-process-env"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpectedProcessEnv: "Unexpected use of process.env."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
|
||||
MemberExpression(node) {
|
||||
const objectName = node.object.name,
|
||||
propertyName = node.property.name;
|
||||
|
||||
if (objectName === "process" && !node.computed && propertyName && propertyName === "env") {
|
||||
context.report({ node, messageId: "unexpectedProcessEnv" });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @fileoverview Disallow the use of process.exit()
|
||||
* @author Nicholas C. Zakas
|
||||
* @deprecated in ESLint v7.0.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: [],
|
||||
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow the use of `process.exit()`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-process-exit"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
noProcessExit: "Don't use process.exit(); throw an error instead."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
"CallExpression > MemberExpression.callee[object.name = 'process'][property.name = 'exit']"(node) {
|
||||
context.report({ node: node.parent, messageId: "noProcessExit" });
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+263
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow returning values from Promise executor functions
|
||||
* @author Milos Djermanovic
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const { findVariable } = require("@eslint-community/eslint-utils");
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const functionTypesToCheck = new Set(["ArrowFunctionExpression", "FunctionExpression"]);
|
||||
|
||||
/**
|
||||
* Determines whether the given identifier node is a reference to a global variable.
|
||||
* @param {ASTNode} node `Identifier` node to check.
|
||||
* @param {Scope} scope Scope to which the node belongs.
|
||||
* @returns {boolean} True if the identifier is a reference to a global variable.
|
||||
*/
|
||||
function isGlobalReference(node, scope) {
|
||||
const variable = findVariable(scope, node);
|
||||
|
||||
return variable !== null && variable.scope.type === "global" && variable.defs.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds function's outer scope.
|
||||
* @param {Scope} scope Function's own scope.
|
||||
* @returns {Scope} Function's outer scope.
|
||||
*/
|
||||
function getOuterScope(scope) {
|
||||
const upper = scope.upper;
|
||||
|
||||
if (upper.type === "function-expression-name") {
|
||||
return upper.upper;
|
||||
}
|
||||
return upper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given function node is used as a Promise executor.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @param {Scope} scope Function's own scope.
|
||||
* @returns {boolean} `true` if the node is a Promise executor.
|
||||
*/
|
||||
function isPromiseExecutor(node, scope) {
|
||||
const parent = node.parent;
|
||||
|
||||
return parent.type === "NewExpression" &&
|
||||
parent.arguments[0] === node &&
|
||||
parent.callee.type === "Identifier" &&
|
||||
parent.callee.name === "Promise" &&
|
||||
isGlobalReference(parent.callee, getOuterScope(scope));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given node is a void expression.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} - `true` if the node is a void expression
|
||||
*/
|
||||
function expressionIsVoid(node) {
|
||||
return node.type === "UnaryExpression" && node.operator === "void";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes the linting error by prepending "void " to the given node
|
||||
* @param {Object} sourceCode context given by context.sourceCode
|
||||
* @param {ASTNode} node The node to fix.
|
||||
* @param {Object} fixer The fixer object provided by ESLint.
|
||||
* @returns {Array<Object>} - An array of fix objects to apply to the node.
|
||||
*/
|
||||
function voidPrependFixer(sourceCode, node, fixer) {
|
||||
|
||||
const requiresParens =
|
||||
|
||||
// prepending `void ` will fail if the node has a lower precedence than void
|
||||
astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression", operator: "void" }) &&
|
||||
|
||||
// check if there are parentheses around the node to avoid redundant parentheses
|
||||
!astUtils.isParenthesised(sourceCode, node);
|
||||
|
||||
// avoid parentheses issues
|
||||
const returnOrArrowToken = sourceCode.getTokenBefore(
|
||||
node,
|
||||
node.parent.type === "ArrowFunctionExpression"
|
||||
? astUtils.isArrowToken
|
||||
|
||||
// isReturnToken
|
||||
: token => token.type === "Keyword" && token.value === "return"
|
||||
);
|
||||
|
||||
const firstToken = sourceCode.getTokenAfter(returnOrArrowToken);
|
||||
|
||||
const prependSpace =
|
||||
|
||||
// is return token, as => allows void to be adjacent
|
||||
returnOrArrowToken.value === "return" &&
|
||||
|
||||
// If two tokens (return and "(") are adjacent
|
||||
returnOrArrowToken.range[1] === firstToken.range[0];
|
||||
|
||||
return [
|
||||
fixer.insertTextBefore(firstToken, `${prependSpace ? " " : ""}void ${requiresParens ? "(" : ""}`),
|
||||
fixer.insertTextAfter(node, requiresParens ? ")" : "")
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes the linting error by `wrapping {}` around the given node's body.
|
||||
* @param {Object} sourceCode context given by context.sourceCode
|
||||
* @param {ASTNode} node The node to fix.
|
||||
* @param {Object} fixer The fixer object provided by ESLint.
|
||||
* @returns {Array<Object>} - An array of fix objects to apply to the node.
|
||||
*/
|
||||
function curlyWrapFixer(sourceCode, node, fixer) {
|
||||
|
||||
// https://github.com/eslint/eslint/pull/17282#issuecomment-1592795923
|
||||
const arrowToken = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken);
|
||||
const firstToken = sourceCode.getTokenAfter(arrowToken);
|
||||
const lastToken = sourceCode.getLastToken(node);
|
||||
|
||||
return [
|
||||
fixer.insertTextBefore(firstToken, "{"),
|
||||
fixer.insertTextAfter(lastToken, "}")
|
||||
];
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow returning values from Promise executor functions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-promise-executor-return"
|
||||
},
|
||||
|
||||
hasSuggestions: true,
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowVoid: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
returnsValue: "Return values from promise executor functions cannot be read.",
|
||||
|
||||
// arrow and function suggestions
|
||||
prependVoid: "Prepend `void` to the expression.",
|
||||
|
||||
// only arrow suggestions
|
||||
wrapBraces: "Wrap the expression in `{}`."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
let funcInfo = null;
|
||||
const sourceCode = context.sourceCode;
|
||||
const {
|
||||
allowVoid = false
|
||||
} = context.options[0] || {};
|
||||
|
||||
return {
|
||||
|
||||
onCodePathStart(_, node) {
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
shouldCheck:
|
||||
functionTypesToCheck.has(node.type) &&
|
||||
isPromiseExecutor(node, sourceCode.getScope(node))
|
||||
};
|
||||
|
||||
if (// Is a Promise executor
|
||||
funcInfo.shouldCheck &&
|
||||
node.type === "ArrowFunctionExpression" &&
|
||||
node.expression &&
|
||||
|
||||
// Except void
|
||||
!(allowVoid && expressionIsVoid(node.body))
|
||||
) {
|
||||
const suggest = [];
|
||||
|
||||
// prevent useless refactors
|
||||
if (allowVoid) {
|
||||
suggest.push({
|
||||
messageId: "prependVoid",
|
||||
fix(fixer) {
|
||||
return voidPrependFixer(sourceCode, node.body, fixer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Do not suggest wrapping an unnamed FunctionExpression in braces as that would be invalid syntax.
|
||||
if (!(node.body.type === "FunctionExpression" && !node.body.id)) {
|
||||
suggest.push({
|
||||
messageId: "wrapBraces",
|
||||
fix(fixer) {
|
||||
return curlyWrapFixer(sourceCode, node, fixer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: node.body,
|
||||
messageId: "returnsValue",
|
||||
suggest
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onCodePathEnd() {
|
||||
funcInfo = funcInfo.upper;
|
||||
},
|
||||
|
||||
ReturnStatement(node) {
|
||||
if (!(funcInfo.shouldCheck && node.argument)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// node is `return <expression>`
|
||||
if (!allowVoid) {
|
||||
context.report({ node, messageId: "returnsValue" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (expressionIsVoid(node.argument)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// allowVoid && !expressionIsVoid
|
||||
context.report({
|
||||
node,
|
||||
messageId: "returnsValue",
|
||||
suggest: [{
|
||||
messageId: "prependVoid",
|
||||
fix(fixer) {
|
||||
return voidPrependFixer(sourceCode, node.argument, fixer);
|
||||
}
|
||||
}]
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag usage of __proto__ property
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const { getStaticPropertyName } = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow the use of the `__proto__` property",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-proto"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpectedProto: "The '__proto__' property is deprecated."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
|
||||
MemberExpression(node) {
|
||||
if (getStaticPropertyName(node) === "__proto__") {
|
||||
context.report({ node, messageId: "unexpectedProto" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow use of Object.prototype builtins on objects
|
||||
* @author Andrew Levine
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns true if the node or any of the objects
|
||||
* to the left of it in the member/call chain is optional.
|
||||
*
|
||||
* e.g. `a?.b`, `a?.b.c`, `a?.()`, `a()?.()`
|
||||
* @param {ASTNode} node The expression to check
|
||||
* @returns {boolean} `true` if there is a short-circuiting optional `?.`
|
||||
* in the same option chain to the left of this call or member expression,
|
||||
* or the node itself is an optional call or member `?.`.
|
||||
*/
|
||||
function isAfterOptional(node) {
|
||||
let leftNode;
|
||||
|
||||
if (node.type === "MemberExpression") {
|
||||
leftNode = node.object;
|
||||
} else if (node.type === "CallExpression") {
|
||||
leftNode = node.callee;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (node.optional) {
|
||||
return true;
|
||||
}
|
||||
return isAfterOptional(leftNode);
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow calling some `Object.prototype` methods directly on objects",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-prototype-builtins"
|
||||
},
|
||||
|
||||
hasSuggestions: true,
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
prototypeBuildIn: "Do not access Object.prototype method '{{prop}}' from target object.",
|
||||
callObjectPrototype: "Call Object.prototype.{{prop}} explicitly."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const DISALLOWED_PROPS = new Set([
|
||||
"hasOwnProperty",
|
||||
"isPrototypeOf",
|
||||
"propertyIsEnumerable"
|
||||
]);
|
||||
|
||||
/**
|
||||
* Reports if a disallowed property is used in a CallExpression
|
||||
* @param {ASTNode} node The CallExpression node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function disallowBuiltIns(node) {
|
||||
|
||||
const callee = astUtils.skipChainExpression(node.callee);
|
||||
|
||||
if (callee.type !== "MemberExpression") {
|
||||
return;
|
||||
}
|
||||
|
||||
const propName = astUtils.getStaticPropertyName(callee);
|
||||
|
||||
if (propName !== null && DISALLOWED_PROPS.has(propName)) {
|
||||
context.report({
|
||||
messageId: "prototypeBuildIn",
|
||||
loc: callee.property.loc,
|
||||
data: { prop: propName },
|
||||
node,
|
||||
suggest: [
|
||||
{
|
||||
messageId: "callObjectPrototype",
|
||||
data: { prop: propName },
|
||||
fix(fixer) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/*
|
||||
* A call after an optional chain (e.g. a?.b.hasOwnProperty(c))
|
||||
* must be fixed manually because the call can be short-circuited
|
||||
*/
|
||||
if (isAfterOptional(node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* A call on a ChainExpression (e.g. (a?.hasOwnProperty)(c)) will trigger
|
||||
* no-unsafe-optional-chaining which should be fixed before this suggestion
|
||||
*/
|
||||
if (node.callee.type === "ChainExpression") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objectVariable = astUtils.getVariableByName(sourceCode.getScope(node), "Object");
|
||||
|
||||
/*
|
||||
* We can't use Object if the global Object was shadowed,
|
||||
* or Object does not exist in the global scope for some reason
|
||||
*/
|
||||
if (!objectVariable || objectVariable.scope.type !== "global" || objectVariable.defs.length > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let objectText = sourceCode.getText(callee.object);
|
||||
|
||||
if (astUtils.getPrecedence(callee.object) <= astUtils.getPrecedence({ type: "SequenceExpression" })) {
|
||||
objectText = `(${objectText})`;
|
||||
}
|
||||
|
||||
const openParenToken = sourceCode.getTokenAfter(
|
||||
node.callee,
|
||||
astUtils.isOpeningParenToken
|
||||
);
|
||||
const isEmptyParameters = node.arguments.length === 0;
|
||||
const delim = isEmptyParameters ? "" : ", ";
|
||||
const fixes = [
|
||||
fixer.replaceText(callee, `Object.prototype.${propName}.call`),
|
||||
fixer.insertTextAfter(openParenToken, objectText + delim)
|
||||
];
|
||||
|
||||
return fixes;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression: disallowBuiltIns
|
||||
};
|
||||
}
|
||||
};
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag when the same variable is declared more then once.
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow variable redeclaration",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-redeclare"
|
||||
},
|
||||
|
||||
messages: {
|
||||
redeclared: "'{{id}}' is already defined.",
|
||||
redeclaredAsBuiltin: "'{{id}}' is already defined as a built-in global variable.",
|
||||
redeclaredBySyntax: "'{{id}}' is already defined by a variable declaration."
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
builtinGlobals: { type: "boolean", default: true }
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = {
|
||||
builtinGlobals: Boolean(
|
||||
context.options.length === 0 ||
|
||||
context.options[0].builtinGlobals
|
||||
)
|
||||
};
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Iterate declarations of a given variable.
|
||||
* @param {escope.variable} variable The variable object to iterate declarations.
|
||||
* @returns {IterableIterator<{type:string,node:ASTNode,loc:SourceLocation}>} The declarations.
|
||||
*/
|
||||
function *iterateDeclarations(variable) {
|
||||
if (options.builtinGlobals && (
|
||||
variable.eslintImplicitGlobalSetting === "readonly" ||
|
||||
variable.eslintImplicitGlobalSetting === "writable"
|
||||
)) {
|
||||
yield { type: "builtin" };
|
||||
}
|
||||
|
||||
for (const id of variable.identifiers) {
|
||||
yield { type: "syntax", node: id, loc: id.loc };
|
||||
}
|
||||
|
||||
if (variable.eslintExplicitGlobalComments) {
|
||||
for (const comment of variable.eslintExplicitGlobalComments) {
|
||||
yield {
|
||||
type: "comment",
|
||||
node: comment,
|
||||
loc: astUtils.getNameLocationInGlobalDirectiveComment(
|
||||
sourceCode,
|
||||
comment,
|
||||
variable.name
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find variables in a given scope and flag redeclared ones.
|
||||
* @param {Scope} scope An eslint-scope scope object.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function findVariablesInScope(scope) {
|
||||
for (const variable of scope.variables) {
|
||||
const [
|
||||
declaration,
|
||||
...extraDeclarations
|
||||
] = iterateDeclarations(variable);
|
||||
|
||||
if (extraDeclarations.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the type of a declaration is different from the type of
|
||||
* the first declaration, it shows the location of the first
|
||||
* declaration.
|
||||
*/
|
||||
const detailMessageId = declaration.type === "builtin"
|
||||
? "redeclaredAsBuiltin"
|
||||
: "redeclaredBySyntax";
|
||||
const data = { id: variable.name };
|
||||
|
||||
// Report extra declarations.
|
||||
for (const { type, node, loc } of extraDeclarations) {
|
||||
const messageId = type === declaration.type
|
||||
? "redeclared"
|
||||
: detailMessageId;
|
||||
|
||||
context.report({ node, loc, messageId, data });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find variables in the current scope.
|
||||
* @param {ASTNode} node The node of the current scope.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkForBlock(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
|
||||
/*
|
||||
* In ES5, some node type such as `BlockStatement` doesn't have that scope.
|
||||
* `scope.block` is a different node in such a case.
|
||||
*/
|
||||
if (scope.block === node) {
|
||||
findVariablesInScope(scope);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
|
||||
findVariablesInScope(scope);
|
||||
|
||||
// Node.js or ES modules has a special scope.
|
||||
if (
|
||||
scope.type === "global" &&
|
||||
scope.childScopes[0] &&
|
||||
|
||||
// The special scope's block is the Program node.
|
||||
scope.block === scope.childScopes[0].block
|
||||
) {
|
||||
findVariablesInScope(scope.childScopes[0]);
|
||||
}
|
||||
},
|
||||
|
||||
FunctionDeclaration: checkForBlock,
|
||||
FunctionExpression: checkForBlock,
|
||||
ArrowFunctionExpression: checkForBlock,
|
||||
|
||||
StaticBlock: checkForBlock,
|
||||
|
||||
BlockStatement: checkForBlock,
|
||||
ForStatement: checkForBlock,
|
||||
ForInStatement: checkForBlock,
|
||||
ForOfStatement: checkForBlock,
|
||||
SwitchStatement: checkForBlock
|
||||
};
|
||||
}
|
||||
};
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* @fileoverview Rule to count multiple spaces in regular expressions
|
||||
* @author Matt DuVall <http://www.mattduvall.com/>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
const regexpp = require("@eslint-community/regexpp");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const regExpParser = new regexpp.RegExpParser();
|
||||
const DOUBLE_SPACE = / {2}/u;
|
||||
|
||||
/**
|
||||
* Check if node is a string
|
||||
* @param {ASTNode} node node to evaluate
|
||||
* @returns {boolean} True if its a string
|
||||
* @private
|
||||
*/
|
||||
function isString(node) {
|
||||
return node && node.type === "Literal" && typeof node.value === "string";
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow multiple spaces in regular expressions",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-regex-spaces"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
fixable: "code",
|
||||
|
||||
messages: {
|
||||
multipleSpaces: "Spaces are hard to count. Use {{{length}}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Validate regular expression
|
||||
* @param {ASTNode} nodeToReport Node to report.
|
||||
* @param {string} pattern Regular expression pattern to validate.
|
||||
* @param {string} rawPattern Raw representation of the pattern in the source code.
|
||||
* @param {number} rawPatternStartRange Start range of the pattern in the source code.
|
||||
* @param {string} flags Regular expression flags.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkRegex(nodeToReport, pattern, rawPattern, rawPatternStartRange, flags) {
|
||||
|
||||
// Skip if there are no consecutive spaces in the source code, to avoid reporting e.g., RegExp(' \ ').
|
||||
if (!DOUBLE_SPACE.test(rawPattern)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const characterClassNodes = [];
|
||||
let regExpAST;
|
||||
|
||||
try {
|
||||
regExpAST = regExpParser.parsePattern(pattern, 0, pattern.length, { unicode: flags.includes("u"), unicodeSets: flags.includes("v") });
|
||||
} catch {
|
||||
|
||||
// Ignore regular expressions with syntax errors
|
||||
return;
|
||||
}
|
||||
|
||||
regexpp.visitRegExpAST(regExpAST, {
|
||||
onCharacterClassEnter(ccNode) {
|
||||
characterClassNodes.push(ccNode);
|
||||
}
|
||||
});
|
||||
|
||||
const spacesPattern = /( {2,})(?: [+*{?]|[^+*{?]|$)/gu;
|
||||
let match;
|
||||
|
||||
while ((match = spacesPattern.exec(pattern))) {
|
||||
const { 1: { length }, index } = match;
|
||||
|
||||
// Report only consecutive spaces that are not in character classes.
|
||||
if (
|
||||
characterClassNodes.every(({ start, end }) => index < start || end <= index)
|
||||
) {
|
||||
context.report({
|
||||
node: nodeToReport,
|
||||
messageId: "multipleSpaces",
|
||||
data: { length },
|
||||
fix(fixer) {
|
||||
if (pattern !== rawPattern) {
|
||||
return null;
|
||||
}
|
||||
return fixer.replaceTextRange(
|
||||
[rawPatternStartRange + index, rawPatternStartRange + index + length],
|
||||
` {${length}}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Report only the first occurrence of consecutive spaces
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate regular expression literals
|
||||
* @param {ASTNode} node node to validate
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkLiteral(node) {
|
||||
if (node.regex) {
|
||||
const pattern = node.regex.pattern;
|
||||
const rawPattern = node.raw.slice(1, node.raw.lastIndexOf("/"));
|
||||
const rawPatternStartRange = node.range[0] + 1;
|
||||
const flags = node.regex.flags;
|
||||
|
||||
checkRegex(
|
||||
node,
|
||||
pattern,
|
||||
rawPattern,
|
||||
rawPatternStartRange,
|
||||
flags
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate strings passed to the RegExp constructor
|
||||
* @param {ASTNode} node node to validate
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkFunction(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
const regExpVar = astUtils.getVariableByName(scope, "RegExp");
|
||||
const shadowed = regExpVar && regExpVar.defs.length > 0;
|
||||
const patternNode = node.arguments[0];
|
||||
|
||||
if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(patternNode) && !shadowed) {
|
||||
const pattern = patternNode.value;
|
||||
const rawPattern = patternNode.raw.slice(1, -1);
|
||||
const rawPatternStartRange = patternNode.range[0] + 1;
|
||||
let flags;
|
||||
|
||||
if (node.arguments.length < 2) {
|
||||
|
||||
// It has no flags.
|
||||
flags = "";
|
||||
} else {
|
||||
const flagsNode = node.arguments[1];
|
||||
|
||||
if (isString(flagsNode)) {
|
||||
flags = flagsNode.value;
|
||||
} else {
|
||||
|
||||
// The flags cannot be determined.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
checkRegex(
|
||||
node,
|
||||
pattern,
|
||||
rawPattern,
|
||||
rawPatternStartRange,
|
||||
flags
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Literal: checkLiteral,
|
||||
CallExpression: checkFunction,
|
||||
NewExpression: checkFunction
|
||||
};
|
||||
}
|
||||
};
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow specified names in exports
|
||||
* @author Milos Djermanovic
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow specified names in exports",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-restricted-exports"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
anyOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
restrictedNamedExports: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
},
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
restrictedNamedExports: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
pattern: "^(?!default$)"
|
||||
},
|
||||
uniqueItems: true
|
||||
},
|
||||
restrictDefaultExports: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
||||
// Allow/Disallow `export default foo; export default 42; export default function foo() {}` format
|
||||
direct: {
|
||||
type: "boolean"
|
||||
},
|
||||
|
||||
// Allow/Disallow `export { foo as default };` declarations
|
||||
named: {
|
||||
type: "boolean"
|
||||
},
|
||||
|
||||
// Allow/Disallow `export { default } from "mod"; export { default as default } from "mod";` declarations
|
||||
defaultFrom: {
|
||||
type: "boolean"
|
||||
},
|
||||
|
||||
// Allow/Disallow `export { foo as default } from "mod";` declarations
|
||||
namedFrom: {
|
||||
type: "boolean"
|
||||
},
|
||||
|
||||
// Allow/Disallow `export * as default from "mod"`; declarations
|
||||
namespaceFrom: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}],
|
||||
|
||||
messages: {
|
||||
restrictedNamed: "'{{name}}' is restricted from being used as an exported name.",
|
||||
restrictedDefault: "Exporting 'default' is restricted."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const restrictedNames = new Set(context.options[0] && context.options[0].restrictedNamedExports);
|
||||
const restrictDefaultExports = context.options[0] && context.options[0].restrictDefaultExports;
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Checks and reports given exported name.
|
||||
* @param {ASTNode} node exported `Identifier` or string `Literal` node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkExportedName(node) {
|
||||
const name = astUtils.getModuleExportName(node);
|
||||
|
||||
if (restrictedNames.has(name)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "restrictedNamed",
|
||||
data: { name }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "default") {
|
||||
if (node.parent.type === "ExportAllDeclaration") {
|
||||
if (restrictDefaultExports && restrictDefaultExports.namespaceFrom) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "restrictedDefault"
|
||||
});
|
||||
}
|
||||
|
||||
} else { // ExportSpecifier
|
||||
const isSourceSpecified = !!node.parent.parent.source;
|
||||
const specifierLocalName = astUtils.getModuleExportName(node.parent.local);
|
||||
|
||||
if (!isSourceSpecified && restrictDefaultExports && restrictDefaultExports.named) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "restrictedDefault"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSourceSpecified && restrictDefaultExports) {
|
||||
if (
|
||||
(specifierLocalName === "default" && restrictDefaultExports.defaultFrom) ||
|
||||
(specifierLocalName !== "default" && restrictDefaultExports.namedFrom)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "restrictedDefault"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ExportAllDeclaration(node) {
|
||||
if (node.exported) {
|
||||
checkExportedName(node.exported);
|
||||
}
|
||||
},
|
||||
|
||||
ExportDefaultDeclaration(node) {
|
||||
if (restrictDefaultExports && restrictDefaultExports.direct) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "restrictedDefault"
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
ExportNamedDeclaration(node) {
|
||||
const declaration = node.declaration;
|
||||
|
||||
if (declaration) {
|
||||
if (declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") {
|
||||
checkExportedName(declaration.id);
|
||||
} else if (declaration.type === "VariableDeclaration") {
|
||||
sourceCode.getDeclaredVariables(declaration)
|
||||
.map(v => v.defs.find(d => d.parent === declaration))
|
||||
.map(d => d.name) // Identifier nodes
|
||||
.forEach(checkExportedName);
|
||||
}
|
||||
} else {
|
||||
node.specifiers
|
||||
.map(s => s.exported)
|
||||
.forEach(checkExportedName);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @fileoverview Restrict usage of specified globals.
|
||||
* @author Benoît Zugmeyer
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow specified global variables",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-restricted-globals"
|
||||
},
|
||||
|
||||
schema: {
|
||||
type: "array",
|
||||
items: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
message: { type: "string" }
|
||||
},
|
||||
required: ["name"],
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
uniqueItems: true,
|
||||
minItems: 0
|
||||
},
|
||||
|
||||
messages: {
|
||||
defaultMessage: "Unexpected use of '{{name}}'.",
|
||||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
||||
customMessage: "Unexpected use of '{{name}}'. {{customMessage}}"
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
// If no globals are restricted, we don't need to do anything
|
||||
if (context.options.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const restrictedGlobalMessages = context.options.reduce((memo, option) => {
|
||||
if (typeof option === "string") {
|
||||
memo[option] = null;
|
||||
} else {
|
||||
memo[option.name] = option.message;
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* Report a variable to be used as a restricted global.
|
||||
* @param {Reference} reference the variable reference
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function reportReference(reference) {
|
||||
const name = reference.identifier.name,
|
||||
customMessage = restrictedGlobalMessages[name],
|
||||
messageId = customMessage
|
||||
? "customMessage"
|
||||
: "defaultMessage";
|
||||
|
||||
context.report({
|
||||
node: reference.identifier,
|
||||
messageId,
|
||||
data: {
|
||||
name,
|
||||
customMessage
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given name is a restricted global name.
|
||||
* @param {string} name name of a variable
|
||||
* @returns {boolean} whether the variable is a restricted global or not
|
||||
* @private
|
||||
*/
|
||||
function isRestricted(name) {
|
||||
return Object.prototype.hasOwnProperty.call(restrictedGlobalMessages, name);
|
||||
}
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
|
||||
// Report variables declared elsewhere (ex: variables defined as "global" by eslint)
|
||||
scope.variables.forEach(variable => {
|
||||
if (!variable.defs.length && isRestricted(variable.name)) {
|
||||
variable.references.forEach(reportReference);
|
||||
}
|
||||
});
|
||||
|
||||
// Report variables not declared at all
|
||||
scope.through.forEach(reference => {
|
||||
if (isRestricted(reference.identifier.name)) {
|
||||
reportReference(reference);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* @fileoverview Restrict usage of specified node imports.
|
||||
* @author Guy Ellis
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const ignore = require("ignore");
|
||||
|
||||
const arrayOfStringsOrObjects = {
|
||||
type: "array",
|
||||
items: {
|
||||
anyOf: [
|
||||
{ type: "string" },
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
message: {
|
||||
type: "string",
|
||||
minLength: 1
|
||||
},
|
||||
importNames: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ["name"]
|
||||
}
|
||||
]
|
||||
},
|
||||
uniqueItems: true
|
||||
};
|
||||
|
||||
const arrayOfStringsOrObjectPatterns = {
|
||||
anyOf: [
|
||||
{
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
},
|
||||
uniqueItems: true
|
||||
},
|
||||
{
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
importNames: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
},
|
||||
minItems: 1,
|
||||
uniqueItems: true
|
||||
},
|
||||
group: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
},
|
||||
minItems: 1,
|
||||
uniqueItems: true
|
||||
},
|
||||
importNamePattern: {
|
||||
type: "string"
|
||||
},
|
||||
message: {
|
||||
type: "string",
|
||||
minLength: 1
|
||||
},
|
||||
caseSensitive: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ["group"]
|
||||
},
|
||||
uniqueItems: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow specified modules when loaded by `import`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-restricted-imports"
|
||||
},
|
||||
|
||||
messages: {
|
||||
path: "'{{importSource}}' import is restricted from being used.",
|
||||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
||||
pathWithCustomMessage: "'{{importSource}}' import is restricted from being used. {{customMessage}}",
|
||||
|
||||
patterns: "'{{importSource}}' import is restricted from being used by a pattern.",
|
||||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
||||
patternWithCustomMessage: "'{{importSource}}' import is restricted from being used by a pattern. {{customMessage}}",
|
||||
|
||||
patternAndImportName: "'{{importName}}' import from '{{importSource}}' is restricted from being used by a pattern.",
|
||||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
||||
patternAndImportNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}",
|
||||
|
||||
patternAndEverything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern.",
|
||||
|
||||
patternAndEverythingWithRegexImportName: "* import is invalid because import name matching '{{importNames}}' pattern from '{{importSource}}' is restricted from being used.",
|
||||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
||||
patternAndEverythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}",
|
||||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
||||
patternAndEverythingWithRegexImportNameAndCustomMessage: "* import is invalid because import name matching '{{importNames}}' pattern from '{{importSource}}' is restricted from being used. {{customMessage}}",
|
||||
|
||||
everything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.",
|
||||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
||||
everythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted. {{customMessage}}",
|
||||
|
||||
importName: "'{{importName}}' import from '{{importSource}}' is restricted.",
|
||||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
||||
importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}"
|
||||
},
|
||||
|
||||
schema: {
|
||||
anyOf: [
|
||||
arrayOfStringsOrObjects,
|
||||
{
|
||||
type: "array",
|
||||
items: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
paths: arrayOfStringsOrObjects,
|
||||
patterns: arrayOfStringsOrObjectPatterns
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
additionalItems: false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const options = Array.isArray(context.options) ? context.options : [];
|
||||
const isPathAndPatternsObject =
|
||||
typeof options[0] === "object" &&
|
||||
(Object.prototype.hasOwnProperty.call(options[0], "paths") || Object.prototype.hasOwnProperty.call(options[0], "patterns"));
|
||||
|
||||
const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
|
||||
const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => {
|
||||
if (typeof importSource === "string") {
|
||||
memo[importSource] = { message: null };
|
||||
} else {
|
||||
memo[importSource.name] = {
|
||||
message: importSource.message,
|
||||
importNames: importSource.importNames
|
||||
};
|
||||
}
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
// Handle patterns too, either as strings or groups
|
||||
let restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
|
||||
|
||||
// standardize to array of objects if we have an array of strings
|
||||
if (restrictedPatterns.length > 0 && typeof restrictedPatterns[0] === "string") {
|
||||
restrictedPatterns = [{ group: restrictedPatterns }];
|
||||
}
|
||||
|
||||
// relative paths are supported for this rule
|
||||
const restrictedPatternGroups = restrictedPatterns.map(({ group, message, caseSensitive, importNames, importNamePattern }) => ({
|
||||
matcher: ignore({ allowRelativePaths: true, ignorecase: !caseSensitive }).add(group),
|
||||
customMessage: message,
|
||||
importNames,
|
||||
importNamePattern
|
||||
}));
|
||||
|
||||
// if no imports are restricted we don't need to check
|
||||
if (Object.keys(restrictedPaths).length === 0 && restrictedPatternGroups.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a restricted path.
|
||||
* @param {string} importSource path of the import
|
||||
* @param {Map<string,Object[]>} importNames Map of import names that are being imported
|
||||
* @param {node} node representing the restricted path reference
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkRestrictedPathAndReport(importSource, importNames, node) {
|
||||
if (!Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customMessage = restrictedPathMessages[importSource].message;
|
||||
const restrictedImportNames = restrictedPathMessages[importSource].importNames;
|
||||
|
||||
if (restrictedImportNames) {
|
||||
if (importNames.has("*")) {
|
||||
const specifierData = importNames.get("*")[0];
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: customMessage ? "everythingWithCustomMessage" : "everything",
|
||||
loc: specifierData.loc,
|
||||
data: {
|
||||
importSource,
|
||||
importNames: restrictedImportNames,
|
||||
customMessage
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
restrictedImportNames.forEach(importName => {
|
||||
if (importNames.has(importName)) {
|
||||
const specifiers = importNames.get(importName);
|
||||
|
||||
specifiers.forEach(specifier => {
|
||||
context.report({
|
||||
node,
|
||||
messageId: customMessage ? "importNameWithCustomMessage" : "importName",
|
||||
loc: specifier.loc,
|
||||
data: {
|
||||
importSource,
|
||||
customMessage,
|
||||
importName
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
node,
|
||||
messageId: customMessage ? "pathWithCustomMessage" : "path",
|
||||
data: {
|
||||
importSource,
|
||||
customMessage
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a restricted path specifically for patterns.
|
||||
* @param {node} node representing the restricted path reference
|
||||
* @param {Object} group contains an Ignore instance for paths, the customMessage to show on failure,
|
||||
* and any restricted import names that have been specified in the config
|
||||
* @param {Map<string,Object[]>} importNames Map of import names that are being imported
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function reportPathForPatterns(node, group, importNames) {
|
||||
const importSource = node.source.value.trim();
|
||||
|
||||
const customMessage = group.customMessage;
|
||||
const restrictedImportNames = group.importNames;
|
||||
const restrictedImportNamePattern = group.importNamePattern ? new RegExp(group.importNamePattern, "u") : null;
|
||||
|
||||
/*
|
||||
* If we are not restricting to any specific import names and just the pattern itself,
|
||||
* report the error and move on
|
||||
*/
|
||||
if (!restrictedImportNames && !restrictedImportNamePattern) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: customMessage ? "patternWithCustomMessage" : "patterns",
|
||||
data: {
|
||||
importSource,
|
||||
customMessage
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
importNames.forEach((specifiers, importName) => {
|
||||
if (importName === "*") {
|
||||
const [specifier] = specifiers;
|
||||
|
||||
if (restrictedImportNames) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: customMessage ? "patternAndEverythingWithCustomMessage" : "patternAndEverything",
|
||||
loc: specifier.loc,
|
||||
data: {
|
||||
importSource,
|
||||
importNames: restrictedImportNames,
|
||||
customMessage
|
||||
}
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
node,
|
||||
messageId: customMessage ? "patternAndEverythingWithRegexImportNameAndCustomMessage" : "patternAndEverythingWithRegexImportName",
|
||||
loc: specifier.loc,
|
||||
data: {
|
||||
importSource,
|
||||
importNames: restrictedImportNamePattern,
|
||||
customMessage
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(restrictedImportNames && restrictedImportNames.includes(importName)) ||
|
||||
(restrictedImportNamePattern && restrictedImportNamePattern.test(importName))
|
||||
) {
|
||||
specifiers.forEach(specifier => {
|
||||
context.report({
|
||||
node,
|
||||
messageId: customMessage ? "patternAndImportNameWithCustomMessage" : "patternAndImportName",
|
||||
loc: specifier.loc,
|
||||
data: {
|
||||
importSource,
|
||||
customMessage,
|
||||
importName
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given importSource is restricted by a pattern.
|
||||
* @param {string} importSource path of the import
|
||||
* @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails
|
||||
* @returns {boolean} whether the variable is a restricted pattern or not
|
||||
* @private
|
||||
*/
|
||||
function isRestrictedPattern(importSource, group) {
|
||||
return group.matcher.ignores(importSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a node to see if any problems should be reported.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkNode(node) {
|
||||
const importSource = node.source.value.trim();
|
||||
const importNames = new Map();
|
||||
|
||||
if (node.type === "ExportAllDeclaration") {
|
||||
const starToken = sourceCode.getFirstToken(node, 1);
|
||||
|
||||
importNames.set("*", [{ loc: starToken.loc }]);
|
||||
} else if (node.specifiers) {
|
||||
for (const specifier of node.specifiers) {
|
||||
let name;
|
||||
const specifierData = { loc: specifier.loc };
|
||||
|
||||
if (specifier.type === "ImportDefaultSpecifier") {
|
||||
name = "default";
|
||||
} else if (specifier.type === "ImportNamespaceSpecifier") {
|
||||
name = "*";
|
||||
} else if (specifier.imported) {
|
||||
name = astUtils.getModuleExportName(specifier.imported);
|
||||
} else if (specifier.local) {
|
||||
name = astUtils.getModuleExportName(specifier.local);
|
||||
}
|
||||
|
||||
if (typeof name === "string") {
|
||||
if (importNames.has(name)) {
|
||||
importNames.get(name).push(specifierData);
|
||||
} else {
|
||||
importNames.set(name, [specifierData]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkRestrictedPathAndReport(importSource, importNames, node);
|
||||
restrictedPatternGroups.forEach(group => {
|
||||
if (isRestrictedPattern(importSource, group)) {
|
||||
reportPathForPatterns(node, group, importNames);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration: checkNode,
|
||||
ExportNamedDeclaration(node) {
|
||||
if (node.source) {
|
||||
checkNode(node);
|
||||
}
|
||||
},
|
||||
ExportAllDeclaration: checkNode
|
||||
};
|
||||
}
|
||||
};
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* @fileoverview Restrict usage of specified node modules.
|
||||
* @author Christian Schulz
|
||||
* @deprecated in ESLint v7.0.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const ignore = require("ignore");
|
||||
|
||||
const arrayOfStrings = {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
uniqueItems: true
|
||||
};
|
||||
|
||||
const arrayOfStringsOrObjects = {
|
||||
type: "array",
|
||||
items: {
|
||||
anyOf: [
|
||||
{ type: "string" },
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
message: {
|
||||
type: "string",
|
||||
minLength: 1
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ["name"]
|
||||
}
|
||||
]
|
||||
},
|
||||
uniqueItems: true
|
||||
};
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: [],
|
||||
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow specified modules when loaded by `require`",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-restricted-modules"
|
||||
},
|
||||
|
||||
schema: {
|
||||
anyOf: [
|
||||
arrayOfStringsOrObjects,
|
||||
{
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
paths: arrayOfStringsOrObjects,
|
||||
patterns: arrayOfStrings
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
additionalItems: false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
messages: {
|
||||
defaultMessage: "'{{name}}' module is restricted from being used.",
|
||||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
||||
customMessage: "'{{name}}' module is restricted from being used. {{customMessage}}",
|
||||
patternMessage: "'{{name}}' module is restricted from being used by a pattern."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = Array.isArray(context.options) ? context.options : [];
|
||||
const isPathAndPatternsObject =
|
||||
typeof options[0] === "object" &&
|
||||
(Object.prototype.hasOwnProperty.call(options[0], "paths") || Object.prototype.hasOwnProperty.call(options[0], "patterns"));
|
||||
|
||||
const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
|
||||
const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
|
||||
|
||||
const restrictedPathMessages = restrictedPaths.reduce((memo, importName) => {
|
||||
if (typeof importName === "string") {
|
||||
memo[importName] = null;
|
||||
} else {
|
||||
memo[importName.name] = importName.message;
|
||||
}
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
// if no imports are restricted we don't need to check
|
||||
if (Object.keys(restrictedPaths).length === 0 && restrictedPatterns.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// relative paths are supported for this rule
|
||||
const ig = ignore({ allowRelativePaths: true }).add(restrictedPatterns);
|
||||
|
||||
|
||||
/**
|
||||
* Function to check if a node is a string literal.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} If the node is a string literal.
|
||||
*/
|
||||
function isStringLiteral(node) {
|
||||
return node && node.type === "Literal" && typeof node.value === "string";
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to check if a node is a require call.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} If the node is a require call.
|
||||
*/
|
||||
function isRequireCall(node) {
|
||||
return node.callee.type === "Identifier" && node.callee.name === "require";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract string from Literal or TemplateLiteral node
|
||||
* @param {ASTNode} node The node to extract from
|
||||
* @returns {string|null} Extracted string or null if node doesn't represent a string
|
||||
*/
|
||||
function getFirstArgumentString(node) {
|
||||
if (isStringLiteral(node)) {
|
||||
return node.value.trim();
|
||||
}
|
||||
|
||||
if (astUtils.isStaticTemplateLiteral(node)) {
|
||||
return node.quasis[0].value.cooked.trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a restricted path.
|
||||
* @param {node} node representing the restricted path reference
|
||||
* @param {string} name restricted path
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function reportPath(node, name) {
|
||||
const customMessage = restrictedPathMessages[name];
|
||||
const messageId = customMessage
|
||||
? "customMessage"
|
||||
: "defaultMessage";
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId,
|
||||
data: {
|
||||
name,
|
||||
customMessage
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given name is a restricted path name
|
||||
* @param {string} name name of a variable
|
||||
* @returns {boolean} whether the variable is a restricted path or not
|
||||
* @private
|
||||
*/
|
||||
function isRestrictedPath(name) {
|
||||
return Object.prototype.hasOwnProperty.call(restrictedPathMessages, name);
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (isRequireCall(node)) {
|
||||
|
||||
// node has arguments
|
||||
if (node.arguments.length) {
|
||||
const name = getFirstArgumentString(node.arguments[0]);
|
||||
|
||||
// if first argument is a string literal or a static string template literal
|
||||
if (name) {
|
||||
|
||||
// check if argument value is in restricted modules array
|
||||
if (isRestrictedPath(name)) {
|
||||
reportPath(node, name);
|
||||
}
|
||||
|
||||
if (restrictedPatterns.length > 0 && ig.ignores(name)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "patternMessage",
|
||||
data: { name }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+168
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow certain object properties
|
||||
* @author Will Klein & Eli White
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow certain properties on certain objects",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-restricted-properties"
|
||||
},
|
||||
|
||||
schema: {
|
||||
type: "array",
|
||||
items: {
|
||||
anyOf: [ // `object` and `property` are both optional, but at least one of them must be provided.
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
object: {
|
||||
type: "string"
|
||||
},
|
||||
property: {
|
||||
type: "string"
|
||||
},
|
||||
message: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ["object"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
object: {
|
||||
type: "string"
|
||||
},
|
||||
property: {
|
||||
type: "string"
|
||||
},
|
||||
message: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ["property"]
|
||||
}
|
||||
]
|
||||
},
|
||||
uniqueItems: true
|
||||
},
|
||||
|
||||
messages: {
|
||||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
||||
restrictedObjectProperty: "'{{objectName}}.{{propertyName}}' is restricted from being used.{{message}}",
|
||||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
||||
restrictedProperty: "'{{propertyName}}' is restricted from being used.{{message}}"
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const restrictedCalls = context.options;
|
||||
|
||||
if (restrictedCalls.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const restrictedProperties = new Map();
|
||||
const globallyRestrictedObjects = new Map();
|
||||
const globallyRestrictedProperties = new Map();
|
||||
|
||||
restrictedCalls.forEach(option => {
|
||||
const objectName = option.object;
|
||||
const propertyName = option.property;
|
||||
|
||||
if (typeof objectName === "undefined") {
|
||||
globallyRestrictedProperties.set(propertyName, { message: option.message });
|
||||
} else if (typeof propertyName === "undefined") {
|
||||
globallyRestrictedObjects.set(objectName, { message: option.message });
|
||||
} else {
|
||||
if (!restrictedProperties.has(objectName)) {
|
||||
restrictedProperties.set(objectName, new Map());
|
||||
}
|
||||
|
||||
restrictedProperties.get(objectName).set(propertyName, {
|
||||
message: option.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks to see whether a property access is restricted, and reports it if so.
|
||||
* @param {ASTNode} node The node to report
|
||||
* @param {string} objectName The name of the object
|
||||
* @param {string} propertyName The name of the property
|
||||
* @returns {undefined}
|
||||
*/
|
||||
function checkPropertyAccess(node, objectName, propertyName) {
|
||||
if (propertyName === null) {
|
||||
return;
|
||||
}
|
||||
const matchedObject = restrictedProperties.get(objectName);
|
||||
const matchedObjectProperty = matchedObject ? matchedObject.get(propertyName) : globallyRestrictedObjects.get(objectName);
|
||||
const globalMatchedProperty = globallyRestrictedProperties.get(propertyName);
|
||||
|
||||
if (matchedObjectProperty) {
|
||||
const message = matchedObjectProperty.message ? ` ${matchedObjectProperty.message}` : "";
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "restrictedObjectProperty",
|
||||
data: {
|
||||
objectName,
|
||||
propertyName,
|
||||
message
|
||||
}
|
||||
});
|
||||
} else if (globalMatchedProperty) {
|
||||
const message = globalMatchedProperty.message ? ` ${globalMatchedProperty.message}` : "";
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "restrictedProperty",
|
||||
data: {
|
||||
propertyName,
|
||||
message
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
checkPropertyAccess(node, node.object && node.object.name, astUtils.getStaticPropertyName(node));
|
||||
},
|
||||
ObjectPattern(node) {
|
||||
let objectName = null;
|
||||
|
||||
if (node.parent.type === "VariableDeclarator") {
|
||||
if (node.parent.init && node.parent.init.type === "Identifier") {
|
||||
objectName = node.parent.init.name;
|
||||
}
|
||||
} else if (node.parent.type === "AssignmentExpression" || node.parent.type === "AssignmentPattern") {
|
||||
if (node.parent.right.type === "Identifier") {
|
||||
objectName = node.parent.right.name;
|
||||
}
|
||||
}
|
||||
|
||||
node.properties.forEach(property => {
|
||||
checkPropertyAccess(node, objectName, astUtils.getStaticPropertyName(property));
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of certain node types
|
||||
* @author Burak Yigit Kaya
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow specified syntax",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-restricted-syntax"
|
||||
},
|
||||
|
||||
schema: {
|
||||
type: "array",
|
||||
items: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
selector: { type: "string" },
|
||||
message: { type: "string" }
|
||||
},
|
||||
required: ["selector"],
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
uniqueItems: true,
|
||||
minItems: 0
|
||||
},
|
||||
|
||||
messages: {
|
||||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
||||
restrictedSyntax: "{{message}}"
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return context.options.reduce((result, selectorOrObject) => {
|
||||
const isStringFormat = (typeof selectorOrObject === "string");
|
||||
const hasCustomMessage = !isStringFormat && Boolean(selectorOrObject.message);
|
||||
|
||||
const selector = isStringFormat ? selectorOrObject : selectorOrObject.selector;
|
||||
const message = hasCustomMessage ? selectorOrObject.message : `Using '${selector}' is not allowed.`;
|
||||
|
||||
return Object.assign(result, {
|
||||
[selector](node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "restrictedSyntax",
|
||||
data: { message }
|
||||
});
|
||||
}
|
||||
});
|
||||
}, {});
|
||||
|
||||
}
|
||||
};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag when return statement contains assignment
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const SENTINEL_TYPE = /^(?:[a-zA-Z]+?Statement|ArrowFunctionExpression|FunctionExpression|ClassExpression)$/u;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow assignment operators in `return` statements",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-return-assign"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["except-parens", "always"]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
returnAssignment: "Return statement should not contain assignment.",
|
||||
arrowAssignment: "Arrow function should not return assignment."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const always = (context.options[0] || "except-parens") !== "except-parens";
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
AssignmentExpression(node) {
|
||||
if (!always && astUtils.isParenthesised(sourceCode, node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentChild = node;
|
||||
let parent = currentChild.parent;
|
||||
|
||||
// Find ReturnStatement or ArrowFunctionExpression in ancestors.
|
||||
while (parent && !SENTINEL_TYPE.test(parent.type)) {
|
||||
currentChild = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
// Reports.
|
||||
if (parent && parent.type === "ReturnStatement") {
|
||||
context.report({
|
||||
node: parent,
|
||||
messageId: "returnAssignment"
|
||||
});
|
||||
} else if (parent && parent.type === "ArrowFunctionExpression" && parent.body === currentChild) {
|
||||
context.report({
|
||||
node: parent,
|
||||
messageId: "arrowAssignment"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @fileoverview Disallows unnecessary `return await`
|
||||
* @author Jordan Harband
|
||||
* @deprecated in ESLint v8.46.0
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
hasSuggestions: true,
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow unnecessary `return await`",
|
||||
|
||||
recommended: false,
|
||||
|
||||
url: "https://eslint.org/docs/latest/rules/no-return-await"
|
||||
},
|
||||
|
||||
fixable: null,
|
||||
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: [],
|
||||
|
||||
schema: [
|
||||
],
|
||||
|
||||
messages: {
|
||||
removeAwait: "Remove redundant `await`.",
|
||||
redundantUseOfAwait: "Redundant use of `await` on a return value."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/**
|
||||
* Reports a found unnecessary `await` expression.
|
||||
* @param {ASTNode} node The node representing the `await` expression to report
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportUnnecessaryAwait(node) {
|
||||
context.report({
|
||||
node: context.sourceCode.getFirstToken(node),
|
||||
loc: node.loc,
|
||||
messageId: "redundantUseOfAwait",
|
||||
suggest: [
|
||||
{
|
||||
messageId: "removeAwait",
|
||||
fix(fixer) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const [awaitToken, tokenAfterAwait] = sourceCode.getFirstTokens(node, 2);
|
||||
|
||||
const areAwaitAndAwaitedExpressionOnTheSameLine = awaitToken.loc.start.line === tokenAfterAwait.loc.start.line;
|
||||
|
||||
if (!areAwaitAndAwaitedExpressionOnTheSameLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [startOfAwait, endOfAwait] = awaitToken.range;
|
||||
|
||||
const characterAfterAwait = sourceCode.text[endOfAwait];
|
||||
const trimLength = characterAfterAwait === " " ? 1 : 0;
|
||||
|
||||
const range = [startOfAwait, endOfAwait + trimLength];
|
||||
|
||||
return fixer.removeRange(range);
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a thrown error from this node will be caught/handled within this function rather than immediately halting
|
||||
* this function. For example, a statement in a `try` block will always have an error handler. A statement in
|
||||
* a `catch` block will only have an error handler if there is also a `finally` block.
|
||||
* @param {ASTNode} node A node representing a location where an could be thrown
|
||||
* @returns {boolean} `true` if a thrown error will be caught/handled in this function
|
||||
*/
|
||||
function hasErrorHandler(node) {
|
||||
let ancestor = node;
|
||||
|
||||
while (!astUtils.isFunction(ancestor) && ancestor.type !== "Program") {
|
||||
if (ancestor.parent.type === "TryStatement" && (ancestor === ancestor.parent.block || ancestor === ancestor.parent.handler && ancestor.parent.finalizer)) {
|
||||
return true;
|
||||
}
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node is placed in tail call position. Once `return` arguments (or arrow function expressions) can be a complex expression,
|
||||
* an `await` expression could or could not be unnecessary by the definition of this rule. So we're looking for `await` expressions that are in tail position.
|
||||
* @param {ASTNode} node A node representing the `await` expression to check
|
||||
* @returns {boolean} The checking result
|
||||
*/
|
||||
function isInTailCallPosition(node) {
|
||||
if (node.parent.type === "ArrowFunctionExpression") {
|
||||
return true;
|
||||
}
|
||||
if (node.parent.type === "ReturnStatement") {
|
||||
return !hasErrorHandler(node.parent);
|
||||
}
|
||||
if (node.parent.type === "ConditionalExpression" && (node === node.parent.consequent || node === node.parent.alternate)) {
|
||||
return isInTailCallPosition(node.parent);
|
||||
}
|
||||
if (node.parent.type === "LogicalExpression" && node === node.parent.right) {
|
||||
return isInTailCallPosition(node.parent);
|
||||
}
|
||||
if (node.parent.type === "SequenceExpression" && node === node.parent.expressions[node.parent.expressions.length - 1]) {
|
||||
return isInTailCallPosition(node.parent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
AwaitExpression(node) {
|
||||
if (isInTailCallPosition(node) && !hasErrorHandler(node)) {
|
||||
reportUnnecessaryAwait(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag when using javascript: urls
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
/* eslint no-script-url: 0 -- Code is checking to report such URLs */
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `javascript:` urls",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-script-url"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpectedScriptURL: "Script URL is a form of eval."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/**
|
||||
* Check whether a node's static value starts with "javascript:" or not.
|
||||
* And report an error for unexpected script URL.
|
||||
* @param {ASTNode} node node to check
|
||||
* @returns {void}
|
||||
*/
|
||||
function check(node) {
|
||||
const value = astUtils.getStaticStringValue(node);
|
||||
|
||||
if (typeof value === "string" && value.toLowerCase().indexOf("javascript:") === 0) {
|
||||
context.report({ node, messageId: "unexpectedScriptURL" });
|
||||
}
|
||||
}
|
||||
return {
|
||||
Literal(node) {
|
||||
if (node.value && typeof node.value === "string") {
|
||||
check(node);
|
||||
}
|
||||
},
|
||||
TemplateLiteral(node) {
|
||||
if (!(node.parent && node.parent.type === "TaggedTemplateExpression")) {
|
||||
check(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow assignments where both sides are exactly the same
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const SPACES = /\s+/gu;
|
||||
|
||||
/**
|
||||
* Traverses 2 Pattern nodes in parallel, then reports self-assignments.
|
||||
* @param {ASTNode|null} left A left node to traverse. This is a Pattern or
|
||||
* a Property.
|
||||
* @param {ASTNode|null} right A right node to traverse. This is a Pattern or
|
||||
* a Property.
|
||||
* @param {boolean} props The flag to check member expressions as well.
|
||||
* @param {Function} report A callback function to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function eachSelfAssignment(left, right, props, report) {
|
||||
if (!left || !right) {
|
||||
|
||||
// do nothing
|
||||
} else if (
|
||||
left.type === "Identifier" &&
|
||||
right.type === "Identifier" &&
|
||||
left.name === right.name
|
||||
) {
|
||||
report(right);
|
||||
} else if (
|
||||
left.type === "ArrayPattern" &&
|
||||
right.type === "ArrayExpression"
|
||||
) {
|
||||
const end = Math.min(left.elements.length, right.elements.length);
|
||||
|
||||
for (let i = 0; i < end; ++i) {
|
||||
const leftElement = left.elements[i];
|
||||
const rightElement = right.elements[i];
|
||||
|
||||
// Avoid cases such as [...a] = [...a, 1]
|
||||
if (
|
||||
leftElement &&
|
||||
leftElement.type === "RestElement" &&
|
||||
i < right.elements.length - 1
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
eachSelfAssignment(leftElement, rightElement, props, report);
|
||||
|
||||
// After a spread element, those indices are unknown.
|
||||
if (rightElement && rightElement.type === "SpreadElement") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
left.type === "RestElement" &&
|
||||
right.type === "SpreadElement"
|
||||
) {
|
||||
eachSelfAssignment(left.argument, right.argument, props, report);
|
||||
} else if (
|
||||
left.type === "ObjectPattern" &&
|
||||
right.type === "ObjectExpression" &&
|
||||
right.properties.length >= 1
|
||||
) {
|
||||
|
||||
/*
|
||||
* Gets the index of the last spread property.
|
||||
* It's possible to overwrite properties followed by it.
|
||||
*/
|
||||
let startJ = 0;
|
||||
|
||||
for (let i = right.properties.length - 1; i >= 0; --i) {
|
||||
const propType = right.properties[i].type;
|
||||
|
||||
if (propType === "SpreadElement" || propType === "ExperimentalSpreadProperty") {
|
||||
startJ = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < left.properties.length; ++i) {
|
||||
for (let j = startJ; j < right.properties.length; ++j) {
|
||||
eachSelfAssignment(
|
||||
left.properties[i],
|
||||
right.properties[j],
|
||||
props,
|
||||
report
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
left.type === "Property" &&
|
||||
right.type === "Property" &&
|
||||
right.kind === "init" &&
|
||||
!right.method
|
||||
) {
|
||||
const leftName = astUtils.getStaticPropertyName(left);
|
||||
|
||||
if (leftName !== null && leftName === astUtils.getStaticPropertyName(right)) {
|
||||
eachSelfAssignment(left.value, right.value, props, report);
|
||||
}
|
||||
} else if (
|
||||
props &&
|
||||
astUtils.skipChainExpression(left).type === "MemberExpression" &&
|
||||
astUtils.skipChainExpression(right).type === "MemberExpression" &&
|
||||
astUtils.isSameReference(left, right)
|
||||
) {
|
||||
report(right);
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow assignments where both sides are exactly the same",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-self-assign"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
props: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
selfAssignment: "'{{name}}' is assigned to itself."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const [{ props = true } = {}] = context.options;
|
||||
|
||||
/**
|
||||
* Reports a given node as self assignments.
|
||||
* @param {ASTNode} node A node to report. This is an Identifier node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "selfAssignment",
|
||||
data: {
|
||||
name: sourceCode.getText(node).replace(SPACES, "")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
AssignmentExpression(node) {
|
||||
if (["=", "&&=", "||=", "??="].includes(node.operator)) {
|
||||
eachSelfAssignment(node.left, node.right, props, report);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag comparison where left part is the same as the right
|
||||
* part.
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow comparisons where both sides are exactly the same",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-self-compare"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
comparingToSelf: "Comparing to itself is potentially pointless."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Determines whether two nodes are composed of the same tokens.
|
||||
* @param {ASTNode} nodeA The first node
|
||||
* @param {ASTNode} nodeB The second node
|
||||
* @returns {boolean} true if the nodes have identical token representations
|
||||
*/
|
||||
function hasSameTokens(nodeA, nodeB) {
|
||||
const tokensA = sourceCode.getTokens(nodeA);
|
||||
const tokensB = sourceCode.getTokens(nodeB);
|
||||
|
||||
return tokensA.length === tokensB.length &&
|
||||
tokensA.every((token, index) => token.type === tokensB[index].type && token.value === tokensB[index].value);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
BinaryExpression(node) {
|
||||
const operators = new Set(["===", "==", "!==", "!=", ">", "<", ">=", "<="]);
|
||||
|
||||
if (operators.has(node.operator) && hasSameTokens(node.left, node.right)) {
|
||||
context.report({ node, messageId: "comparingToSelf" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of comma operator
|
||||
* @author Brandon Mills
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
allowInParentheses: true
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow comma operators",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-sequences"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
properties: {
|
||||
allowInParentheses: {
|
||||
type: "boolean",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
unexpectedCommaExpression: "Unexpected use of comma operator."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = Object.assign({}, DEFAULT_OPTIONS, context.options[0]);
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Parts of the grammar that are required to have parens.
|
||||
*/
|
||||
const parenthesized = {
|
||||
DoWhileStatement: "test",
|
||||
IfStatement: "test",
|
||||
SwitchStatement: "discriminant",
|
||||
WhileStatement: "test",
|
||||
WithStatement: "object",
|
||||
ArrowFunctionExpression: "body"
|
||||
|
||||
/*
|
||||
* Omitting CallExpression - commas are parsed as argument separators
|
||||
* Omitting NewExpression - commas are parsed as argument separators
|
||||
* Omitting ForInStatement - parts aren't individually parenthesised
|
||||
* Omitting ForStatement - parts aren't individually parenthesised
|
||||
*/
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether a node is required by the grammar to be wrapped in
|
||||
* parens, e.g. the test of an if statement.
|
||||
* @param {ASTNode} node The AST node
|
||||
* @returns {boolean} True if parens around node belong to parent node.
|
||||
*/
|
||||
function requiresExtraParens(node) {
|
||||
return node.parent && parenthesized[node.parent.type] &&
|
||||
node === node.parent[parenthesized[node.parent.type]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node is wrapped in parens.
|
||||
* @param {ASTNode} node The AST node
|
||||
* @returns {boolean} True if the node has a paren on each side.
|
||||
*/
|
||||
function isParenthesised(node) {
|
||||
return astUtils.isParenthesised(sourceCode, node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node is wrapped in two levels of parens.
|
||||
* @param {ASTNode} node The AST node
|
||||
* @returns {boolean} True if two parens surround the node on each side.
|
||||
*/
|
||||
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];
|
||||
}
|
||||
|
||||
return {
|
||||
SequenceExpression(node) {
|
||||
|
||||
// Always allow sequences in for statement update
|
||||
if (node.parent.type === "ForStatement" &&
|
||||
(node === node.parent.init || node === node.parent.update)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wrapping a sequence in extra parens indicates intent
|
||||
if (options.allowInParentheses) {
|
||||
if (requiresExtraParens(node)) {
|
||||
if (isParenthesisedTwice(node)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (isParenthesised(node)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const firstCommaToken = sourceCode.getTokenAfter(node.expressions[0], astUtils.isCommaToken);
|
||||
|
||||
context.report({ node, loc: firstCommaToken.loc, messageId: "unexpectedCommaExpression" });
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow returning values from setters
|
||||
* @author Milos Djermanovic
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
const { findVariable } = require("@eslint-community/eslint-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines whether the given identifier node is a reference to a global variable.
|
||||
* @param {ASTNode} node `Identifier` node to check.
|
||||
* @param {Scope} scope Scope to which the node belongs.
|
||||
* @returns {boolean} True if the identifier is a reference to a global variable.
|
||||
*/
|
||||
function isGlobalReference(node, scope) {
|
||||
const variable = findVariable(scope, node);
|
||||
|
||||
return variable !== null && variable.scope.type === "global" && variable.defs.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given node is an argument of the specified global method call, at the given `index` position.
|
||||
* E.g., for given `index === 1`, this function checks for `objectName.methodName(foo, node)`, where objectName is a global variable.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @param {Scope} scope Scope to which the node belongs.
|
||||
* @param {string} objectName Name of the global object.
|
||||
* @param {string} methodName Name of the method.
|
||||
* @param {number} index The given position.
|
||||
* @returns {boolean} `true` if the node is argument at the given position.
|
||||
*/
|
||||
function isArgumentOfGlobalMethodCall(node, scope, objectName, methodName, index) {
|
||||
const callNode = node.parent;
|
||||
|
||||
return callNode.type === "CallExpression" &&
|
||||
callNode.arguments[index] === node &&
|
||||
astUtils.isSpecificMemberAccess(callNode.callee, objectName, methodName) &&
|
||||
isGlobalReference(astUtils.skipChainExpression(callNode.callee).object, scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given node is used as a property descriptor.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @param {Scope} scope Scope to which the node belongs.
|
||||
* @returns {boolean} `true` if the node is a property descriptor.
|
||||
*/
|
||||
function isPropertyDescriptor(node, scope) {
|
||||
if (
|
||||
isArgumentOfGlobalMethodCall(node, scope, "Object", "defineProperty", 2) ||
|
||||
isArgumentOfGlobalMethodCall(node, scope, "Reflect", "defineProperty", 2)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const parent = node.parent;
|
||||
|
||||
if (
|
||||
parent.type === "Property" &&
|
||||
parent.value === node
|
||||
) {
|
||||
const grandparent = parent.parent;
|
||||
|
||||
if (
|
||||
grandparent.type === "ObjectExpression" &&
|
||||
(
|
||||
isArgumentOfGlobalMethodCall(grandparent, scope, "Object", "create", 1) ||
|
||||
isArgumentOfGlobalMethodCall(grandparent, scope, "Object", "defineProperties", 1)
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given function node is used as a setter function.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @param {Scope} scope Scope to which the node belongs.
|
||||
* @returns {boolean} `true` if the node is a setter.
|
||||
*/
|
||||
function isSetter(node, scope) {
|
||||
const parent = node.parent;
|
||||
|
||||
if (
|
||||
(parent.type === "Property" || parent.type === "MethodDefinition") &&
|
||||
parent.kind === "set" &&
|
||||
parent.value === node
|
||||
) {
|
||||
|
||||
// Setter in an object literal or in a class
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
parent.type === "Property" &&
|
||||
parent.value === node &&
|
||||
astUtils.getStaticPropertyName(parent) === "set" &&
|
||||
parent.parent.type === "ObjectExpression" &&
|
||||
isPropertyDescriptor(parent.parent, scope)
|
||||
) {
|
||||
|
||||
// Setter in a property descriptor
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds function's outer scope.
|
||||
* @param {Scope} scope Function's own scope.
|
||||
* @returns {Scope} Function's outer scope.
|
||||
*/
|
||||
function getOuterScope(scope) {
|
||||
const upper = scope.upper;
|
||||
|
||||
if (upper.type === "function-expression-name") {
|
||||
return upper.upper;
|
||||
}
|
||||
|
||||
return upper;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow returning values from setters",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-setter-return"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
returnsValue: "Setter cannot return a value."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
let funcInfo = null;
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Creates and pushes to the stack a function info object for the given function node.
|
||||
* @param {ASTNode} node The function node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function enterFunction(node) {
|
||||
const outerScope = getOuterScope(sourceCode.getScope(node));
|
||||
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
isSetter: isSetter(node, outerScope)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops the current function info object from the stack.
|
||||
* @returns {void}
|
||||
*/
|
||||
function exitFunction() {
|
||||
funcInfo = funcInfo.upper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the given node.
|
||||
* @param {ASTNode} node Node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(node) {
|
||||
context.report({ node, messageId: "returnsValue" });
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
/*
|
||||
* Function declarations cannot be setters, but we still have to track them in the `funcInfo` stack to avoid
|
||||
* false positives, because a ReturnStatement node can belong to a function declaration inside a setter.
|
||||
*
|
||||
* Note: A previously declared function can be referenced and actually used as a setter in a property descriptor,
|
||||
* but that's out of scope for this rule.
|
||||
*/
|
||||
FunctionDeclaration: enterFunction,
|
||||
FunctionExpression: enterFunction,
|
||||
ArrowFunctionExpression(node) {
|
||||
enterFunction(node);
|
||||
|
||||
if (funcInfo.isSetter && node.expression) {
|
||||
|
||||
// { set: foo => bar } property descriptor. Report implicit return 'bar' as the equivalent for a return statement.
|
||||
report(node.body);
|
||||
}
|
||||
},
|
||||
|
||||
"FunctionDeclaration:exit": exitFunction,
|
||||
"FunctionExpression:exit": exitFunction,
|
||||
"ArrowFunctionExpression:exit": exitFunction,
|
||||
|
||||
ReturnStatement(node) {
|
||||
|
||||
// Global returns (e.g., at the top level of a Node module) don't have `funcInfo`.
|
||||
if (funcInfo && funcInfo.isSetter && node.argument) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @fileoverview Disallow shadowing of NaN, undefined, and Infinity (ES5 section 15.1.1)
|
||||
* @author Michael Ficarra
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Determines if a variable safely shadows undefined.
|
||||
* This is the case when a variable named `undefined` is never assigned to a value (i.e. it always shares the same value
|
||||
* as the global).
|
||||
* @param {eslintScope.Variable} variable The variable to check
|
||||
* @returns {boolean} true if this variable safely shadows `undefined`
|
||||
*/
|
||||
function safelyShadowsUndefined(variable) {
|
||||
return variable.name === "undefined" &&
|
||||
variable.references.every(ref => !ref.isWrite()) &&
|
||||
variable.defs.every(def => def.node.type === "VariableDeclarator" && def.node.init === null);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow identifiers from shadowing restricted names",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-shadow-restricted-names"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
shadowingRestrictedName: "Shadowing of global property '{{name}}'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
|
||||
const RESTRICTED = new Set(["undefined", "NaN", "Infinity", "arguments", "eval"]);
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
return {
|
||||
"VariableDeclaration, :function, CatchClause"(node) {
|
||||
for (const variable of sourceCode.getDeclaredVariables(node)) {
|
||||
if (variable.defs.length > 0 && RESTRICTED.has(variable.name) && !safelyShadowsUndefined(variable)) {
|
||||
context.report({
|
||||
node: variable.defs[0].name,
|
||||
messageId: "shadowingRestrictedName",
|
||||
data: {
|
||||
name: variable.name
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag on declaring variables already declared in the outer scope
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const FUNC_EXPR_NODE_TYPES = new Set(["ArrowFunctionExpression", "FunctionExpression"]);
|
||||
const CALL_EXPR_NODE_TYPE = new Set(["CallExpression"]);
|
||||
const FOR_IN_OF_TYPE = /^For(?:In|Of)Statement$/u;
|
||||
const SENTINEL_TYPE = /^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/u;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow variable declarations from shadowing variables declared in the outer scope",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-shadow"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
builtinGlobals: { type: "boolean", default: false },
|
||||
hoist: { enum: ["all", "functions", "never"], default: "functions" },
|
||||
allow: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
ignoreOnInitialization: { type: "boolean", default: false }
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
noShadow: "'{{name}}' is already declared in the upper scope on line {{shadowedLine}} column {{shadowedColumn}}.",
|
||||
noShadowGlobal: "'{{name}}' is already a global variable."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const options = {
|
||||
builtinGlobals: context.options[0] && context.options[0].builtinGlobals,
|
||||
hoist: (context.options[0] && context.options[0].hoist) || "functions",
|
||||
allow: (context.options[0] && context.options[0].allow) || [],
|
||||
ignoreOnInitialization: context.options[0] && context.options[0].ignoreOnInitialization
|
||||
};
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Checks whether or not a given location is inside of the range of a given node.
|
||||
* @param {ASTNode} node An node to check.
|
||||
* @param {number} location A location to check.
|
||||
* @returns {boolean} `true` if the location is inside of the range of the node.
|
||||
*/
|
||||
function isInRange(node, location) {
|
||||
return node && node.range[0] <= location && location <= node.range[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches from the current node through its ancestry to find a matching node.
|
||||
* @param {ASTNode} node a node to get.
|
||||
* @param {(node: ASTNode) => boolean} match a callback that checks whether or not the node verifies its condition or not.
|
||||
* @returns {ASTNode|null} the matching node.
|
||||
*/
|
||||
function findSelfOrAncestor(node, match) {
|
||||
let currentNode = node;
|
||||
|
||||
while (currentNode && !match(currentNode)) {
|
||||
currentNode = currentNode.parent;
|
||||
}
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds function's outer scope.
|
||||
* @param {Scope} scope Function's own scope.
|
||||
* @returns {Scope} Function's outer scope.
|
||||
*/
|
||||
function getOuterScope(scope) {
|
||||
const upper = scope.upper;
|
||||
|
||||
if (upper.type === "function-expression-name") {
|
||||
return upper.upper;
|
||||
}
|
||||
return upper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a variable and a shadowedVariable have the same init pattern ancestor.
|
||||
* @param {Object} variable a variable to check.
|
||||
* @param {Object} shadowedVariable a shadowedVariable to check.
|
||||
* @returns {boolean} Whether or not the variable and the shadowedVariable have the same init pattern ancestor.
|
||||
*/
|
||||
function isInitPatternNode(variable, shadowedVariable) {
|
||||
const outerDef = shadowedVariable.defs[0];
|
||||
|
||||
if (!outerDef) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { variableScope } = variable.scope;
|
||||
|
||||
|
||||
if (!(FUNC_EXPR_NODE_TYPES.has(variableScope.block.type) && getOuterScope(variableScope) === shadowedVariable.scope)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fun = variableScope.block;
|
||||
const { parent } = fun;
|
||||
|
||||
const callExpression = findSelfOrAncestor(
|
||||
parent,
|
||||
node => CALL_EXPR_NODE_TYPE.has(node.type)
|
||||
);
|
||||
|
||||
if (!callExpression) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let node = outerDef.name;
|
||||
const location = callExpression.range[1];
|
||||
|
||||
while (node) {
|
||||
if (node.type === "VariableDeclarator") {
|
||||
if (isInRange(node.init, location)) {
|
||||
return true;
|
||||
}
|
||||
if (FOR_IN_OF_TYPE.test(node.parent.parent.type) &&
|
||||
isInRange(node.parent.parent.right, location)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
} else if (node.type === "AssignmentPattern") {
|
||||
if (isInRange(node.right, location)) {
|
||||
return true;
|
||||
}
|
||||
} else if (SENTINEL_TYPE.test(node.type)) {
|
||||
break;
|
||||
}
|
||||
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if variable name is allowed.
|
||||
* @param {ASTNode} variable The variable to check.
|
||||
* @returns {boolean} Whether or not the variable name is allowed.
|
||||
*/
|
||||
function isAllowed(variable) {
|
||||
return options.allow.includes(variable.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a variable of the class name in the class scope of ClassDeclaration.
|
||||
*
|
||||
* ClassDeclaration creates two variables of its name into its outer scope and its class scope.
|
||||
* So we should ignore the variable in the class scope.
|
||||
* @param {Object} variable The variable to check.
|
||||
* @returns {boolean} Whether or not the variable of the class name in the class scope of ClassDeclaration.
|
||||
*/
|
||||
function isDuplicatedClassNameVariable(variable) {
|
||||
const block = variable.scope.block;
|
||||
|
||||
return block.type === "ClassDeclaration" && block.id === variable.identifiers[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a variable is inside the initializer of scopeVar.
|
||||
*
|
||||
* To avoid reporting at declarations such as `var a = function a() {};`.
|
||||
* But it should report `var a = function(a) {};` or `var a = function() { function a() {} };`.
|
||||
* @param {Object} variable The variable to check.
|
||||
* @param {Object} scopeVar The scope variable to look for.
|
||||
* @returns {boolean} Whether or not the variable is inside initializer of scopeVar.
|
||||
*/
|
||||
function isOnInitializer(variable, scopeVar) {
|
||||
const outerScope = scopeVar.scope;
|
||||
const outerDef = scopeVar.defs[0];
|
||||
const outer = outerDef && outerDef.parent && outerDef.parent.range;
|
||||
const innerScope = variable.scope;
|
||||
const innerDef = variable.defs[0];
|
||||
const inner = innerDef && innerDef.name.range;
|
||||
|
||||
return (
|
||||
outer &&
|
||||
inner &&
|
||||
outer[0] < inner[0] &&
|
||||
inner[1] < outer[1] &&
|
||||
((innerDef.type === "FunctionName" && innerDef.node.type === "FunctionExpression") || innerDef.node.type === "ClassExpression") &&
|
||||
outerScope === innerScope.upper
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a range of a variable's identifier node.
|
||||
* @param {Object} variable The variable to get.
|
||||
* @returns {Array|undefined} The range of the variable's identifier node.
|
||||
*/
|
||||
function getNameRange(variable) {
|
||||
const def = variable.defs[0];
|
||||
|
||||
return def && def.name.range;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get declared line and column of a variable.
|
||||
* @param {eslint-scope.Variable} variable The variable to get.
|
||||
* @returns {Object} The declared line and column of the variable.
|
||||
*/
|
||||
function getDeclaredLocation(variable) {
|
||||
const identifier = variable.identifiers[0];
|
||||
let obj;
|
||||
|
||||
if (identifier) {
|
||||
obj = {
|
||||
global: false,
|
||||
line: identifier.loc.start.line,
|
||||
column: identifier.loc.start.column + 1
|
||||
};
|
||||
} else {
|
||||
obj = {
|
||||
global: true
|
||||
};
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a variable is in TDZ of scopeVar.
|
||||
* @param {Object} variable The variable to check.
|
||||
* @param {Object} scopeVar The variable of TDZ.
|
||||
* @returns {boolean} Whether or not the variable is in TDZ of scopeVar.
|
||||
*/
|
||||
function isInTdz(variable, scopeVar) {
|
||||
const outerDef = scopeVar.defs[0];
|
||||
const inner = getNameRange(variable);
|
||||
const outer = getNameRange(scopeVar);
|
||||
|
||||
return (
|
||||
inner &&
|
||||
outer &&
|
||||
inner[1] < outer[0] &&
|
||||
|
||||
// Excepts FunctionDeclaration if is {"hoist":"function"}.
|
||||
(options.hoist !== "functions" || !outerDef || outerDef.node.type !== "FunctionDeclaration")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the current context for shadowed variables.
|
||||
* @param {Scope} scope Fixme
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkForShadows(scope) {
|
||||
const variables = scope.variables;
|
||||
|
||||
for (let i = 0; i < variables.length; ++i) {
|
||||
const variable = variables[i];
|
||||
|
||||
// Skips "arguments" or variables of a class name in the class scope of ClassDeclaration.
|
||||
if (variable.identifiers.length === 0 ||
|
||||
isDuplicatedClassNameVariable(variable) ||
|
||||
isAllowed(variable)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Gets shadowed variable.
|
||||
const shadowed = astUtils.getVariableByName(scope.upper, variable.name);
|
||||
|
||||
if (shadowed &&
|
||||
(shadowed.identifiers.length > 0 || (options.builtinGlobals && "writeable" in shadowed)) &&
|
||||
!isOnInitializer(variable, shadowed) &&
|
||||
!(options.ignoreOnInitialization && isInitPatternNode(variable, shadowed)) &&
|
||||
!(options.hoist !== "all" && isInTdz(variable, shadowed))
|
||||
) {
|
||||
const location = getDeclaredLocation(shadowed);
|
||||
const messageId = location.global ? "noShadowGlobal" : "noShadow";
|
||||
const data = { name: variable.name };
|
||||
|
||||
if (!location.global) {
|
||||
data.shadowedLine = location.line;
|
||||
data.shadowedColumn = location.column;
|
||||
}
|
||||
context.report({
|
||||
node: variable.identifiers[0],
|
||||
messageId,
|
||||
data
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"Program:exit"(node) {
|
||||
const globalScope = sourceCode.getScope(node);
|
||||
const stack = globalScope.childScopes.slice();
|
||||
|
||||
while (stack.length) {
|
||||
const scope = stack.pop();
|
||||
|
||||
stack.push(...scope.childScopes);
|
||||
checkForShadows(scope);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @fileoverview Rule to check that spaced function application
|
||||
* @author Matt DuVall <http://www.mattduvall.com>
|
||||
* @deprecated in ESLint v3.3.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Disallow spacing between function identifiers and their applications (deprecated)",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-spaced-func"
|
||||
},
|
||||
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: ["func-call-spacing"],
|
||||
|
||||
fixable: "whitespace",
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
noSpacedFunction: "Unexpected space between function name and paren."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.sourceCode;
|
||||
|
||||
/**
|
||||
* Check if open space is present in a function name
|
||||
* @param {ASTNode} node node to evaluate
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function detectOpenSpaces(node) {
|
||||
const lastCalleeToken = sourceCode.getLastToken(node.callee);
|
||||
let prevToken = lastCalleeToken,
|
||||
parenToken = sourceCode.getTokenAfter(lastCalleeToken);
|
||||
|
||||
// advances to an open parenthesis.
|
||||
while (
|
||||
parenToken &&
|
||||
parenToken.range[1] < node.range[1] &&
|
||||
parenToken.value !== "("
|
||||
) {
|
||||
prevToken = parenToken;
|
||||
parenToken = sourceCode.getTokenAfter(parenToken);
|
||||
}
|
||||
|
||||
// look for a space between the callee and the open paren
|
||||
if (parenToken &&
|
||||
parenToken.range[1] < node.range[1] &&
|
||||
sourceCode.isSpaceBetweenTokens(prevToken, parenToken)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
loc: lastCalleeToken.loc.start,
|
||||
messageId: "noSpacedFunction",
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([prevToken.range[1], parenToken.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression: detectOpenSpaces,
|
||||
NewExpression: detectOpenSpaces
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @fileoverview Disallow sparse arrays
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow sparse arrays",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-sparse-arrays"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpectedSparseArray: "Unexpected comma in middle of array."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
|
||||
ArrayExpression(node) {
|
||||
|
||||
const emptySpot = node.elements.includes(null);
|
||||
|
||||
if (emptySpot) {
|
||||
context.report({ node, messageId: "unexpectedSparseArray" });
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @fileoverview Rule to check for properties whose identifier ends with the string Sync
|
||||
* @author Matt DuVall<http://mattduvall.com/>
|
||||
* @deprecated in ESLint v7.0.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
|
||||
replacedBy: [],
|
||||
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow synchronous methods",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-sync"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowAtRootLevel: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
noSync: "Unexpected sync method: '{{propertyName}}'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const selector = context.options[0] && context.options[0].allowAtRootLevel
|
||||
? ":function MemberExpression[property.name=/.*Sync$/]"
|
||||
: "MemberExpression[property.name=/.*Sync$/]";
|
||||
|
||||
return {
|
||||
[selector](node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noSync",
|
||||
data: {
|
||||
propertyName: node.property.name
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @fileoverview Rule to check for tabs inside a file
|
||||
* @author Gyandeep Singh
|
||||
* @deprecated in ESLint v8.53.0
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const tabRegex = /\t+/gu;
|
||||
const anyNonWhitespaceRegex = /\S/u;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: [],
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "Disallow all tabs",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-tabs"
|
||||
},
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowIndentationTabs: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
unexpectedTab: "Unexpected tab character."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const allowIndentationTabs = context.options && context.options[0] && context.options[0].allowIndentationTabs;
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
sourceCode.getLines().forEach((line, index) => {
|
||||
let match;
|
||||
|
||||
while ((match = tabRegex.exec(line)) !== null) {
|
||||
if (allowIndentationTabs && !anyNonWhitespaceRegex.test(line.slice(0, match.index))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: {
|
||||
start: {
|
||||
line: index + 1,
|
||||
column: match.index
|
||||
},
|
||||
end: {
|
||||
line: index + 1,
|
||||
column: match.index + match[0].length
|
||||
}
|
||||
},
|
||||
messageId: "unexpectedTab"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @fileoverview Warn when using template string syntax in regular strings
|
||||
* @author Jeroen Engels
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow template literal placeholder syntax in regular strings",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-template-curly-in-string"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
unexpectedTemplateExpression: "Unexpected template string expression."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const regex = /\$\{[^}]+\}/u;
|
||||
|
||||
return {
|
||||
Literal(node) {
|
||||
if (typeof node.value === "string" && regex.test(node.value)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedTemplateExpression"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag use of ternary operators.
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow ternary operators",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-ternary"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
noTernaryOperator: "Ternary operator used."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
|
||||
ConditionalExpression(node) {
|
||||
context.report({ node, messageId: "noTernaryOperator" });
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* @fileoverview A rule to disallow using `this`/`super` before `super()`.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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"
|
||||
);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "Disallow `this`/`super` before calling `super()` in constructors",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/latest/rules/no-this-before-super"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
noBeforeSuper: "'{{kind}}' is not allowed before 'super()'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/*
|
||||
* Information for each constructor.
|
||||
* - upper: Information of the upper constructor.
|
||||
* - hasExtends: A flag which shows whether the owner class has a valid
|
||||
* `extends` part.
|
||||
* - scope: The scope of the owner class.
|
||||
* - codePath: The code path of this constructor.
|
||||
*/
|
||||
let funcInfo = null;
|
||||
|
||||
/*
|
||||
* Information for each code path segment.
|
||||
* Each key is the id of a code path segment.
|
||||
* Each value is an object:
|
||||
* - superCalled: The flag which shows `super()` called in all code paths.
|
||||
* - invalidNodes: The array of invalid ThisExpression and Super nodes.
|
||||
*/
|
||||
let segInfoMap = Object.create(null);
|
||||
|
||||
/**
|
||||
* Gets whether or not `super()` is called in a given code path segment.
|
||||
* @param {CodePathSegment} segment A code path segment to get.
|
||||
* @returns {boolean} `true` if `super()` is called.
|
||||
*/
|
||||
function isCalled(segment) {
|
||||
return !segment.reachable || segInfoMap[segment.id].superCalled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not this is in a constructor.
|
||||
* @returns {boolean} `true` if this is in a constructor.
|
||||
*/
|
||||
function isInConstructorOfDerivedClass() {
|
||||
return Boolean(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if every segment in a set has been called.
|
||||
* @param {Set<CodePathSegment>} segments The segments to search.
|
||||
* @returns {boolean} True if every segment has been called; false otherwise.
|
||||
*/
|
||||
function isEverySegmentCalled(segments) {
|
||||
for (const segment of segments) {
|
||||
if (!isCalled(segment)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not this is before `super()` is called.
|
||||
* @returns {boolean} `true` if this is before `super()` is called.
|
||||
*/
|
||||
function isBeforeCallOfSuper() {
|
||||
return (
|
||||
isInConstructorOfDerivedClass() &&
|
||||
!isEverySegmentCalled(funcInfo.currentSegments)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a given node as invalid.
|
||||
* @param {ASTNode} node A node to set as invalid. This is one of
|
||||
* a ThisExpression and a Super.
|
||||
* @returns {void}
|
||||
*/
|
||||
function setInvalid(node) {
|
||||
const segments = funcInfo.currentSegments;
|
||||
|
||||
for (const segment of segments) {
|
||||
if (segment.reachable) {
|
||||
segInfoMap[segment.id].invalidNodes.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current segment as `super` was called.
|
||||
* @returns {void}
|
||||
*/
|
||||
function setSuperCalled() {
|
||||
const segments = funcInfo.currentSegments;
|
||||
|
||||
for (const segment of segments) {
|
||||
if (segment.reachable) {
|
||||
segInfoMap[segment.id].superCalled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* Adds information of a constructor into the stack.
|
||||
* @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;
|
||||
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
isConstructor: true,
|
||||
hasExtends: Boolean(
|
||||
classNode.superClass &&
|
||||
!astUtils.isNullOrUndefined(classNode.superClass)
|
||||
),
|
||||
codePath,
|
||||
currentSegments: new Set()
|
||||
};
|
||||
} else {
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
isConstructor: false,
|
||||
hasExtends: false,
|
||||
codePath,
|
||||
currentSegments: new Set()
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the top of stack item.
|
||||
*
|
||||
* And this traverses all segments of this code path then reports every
|
||||
* invalid node.
|
||||
* @param {CodePath} codePath A code path which was ended.
|
||||
* @returns {void}
|
||||
*/
|
||||
onCodePathEnd(codePath) {
|
||||
const isDerivedClass = funcInfo.hasExtends;
|
||||
|
||||
funcInfo = funcInfo.upper;
|
||||
if (!isDerivedClass) {
|
||||
return;
|
||||
}
|
||||
|
||||
codePath.traverseSegments((segment, controller) => {
|
||||
const info = segInfoMap[segment.id];
|
||||
|
||||
for (let i = 0; i < info.invalidNodes.length; ++i) {
|
||||
const invalidNode = info.invalidNodes[i];
|
||||
|
||||
context.report({
|
||||
messageId: "noBeforeSuper",
|
||||
node: invalidNode,
|
||||
data: {
|
||||
kind: invalidNode.type === "Super" ? "super" : "this"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (info.superCalled) {
|
||||
controller.skip();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize information of a given code path segment.
|
||||
* @param {CodePathSegment} segment A code path segment to initialize.
|
||||
* @returns {void}
|
||||
*/
|
||||
onCodePathSegmentStart(segment) {
|
||||
funcInfo.currentSegments.add(segment);
|
||||
|
||||
if (!isInConstructorOfDerivedClass()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize info.
|
||||
segInfoMap[segment.id] = {
|
||||
superCalled: (
|
||||
segment.prevSegments.length > 0 &&
|
||||
segment.prevSegments.every(isCalled)
|
||||
),
|
||||
invalidNodes: []
|
||||
};
|
||||
},
|
||||
|
||||
onUnreachableCodePathSegmentStart(segment) {
|
||||
funcInfo.currentSegments.add(segment);
|
||||
},
|
||||
|
||||
onUnreachableCodePathSegmentEnd(segment) {
|
||||
funcInfo.currentSegments.delete(segment);
|
||||
},
|
||||
|
||||
onCodePathSegmentEnd(segment) {
|
||||
funcInfo.currentSegments.delete(segment);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 (!isInConstructorOfDerivedClass()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update information inside of the loop.
|
||||
funcInfo.codePath.traverseSegments(
|
||||
{ first: toSegment, last: fromSegment },
|
||||
(segment, controller) => {
|
||||
const info = segInfoMap[segment.id];
|
||||
|
||||
if (info.superCalled) {
|
||||
info.invalidNodes = [];
|
||||
controller.skip();
|
||||
} else if (
|
||||
segment.prevSegments.length > 0 &&
|
||||
segment.prevSegments.every(isCalled)
|
||||
) {
|
||||
info.superCalled = true;
|
||||
info.invalidNodes = [];
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reports if this is before `super()`.
|
||||
* @param {ASTNode} node A target node.
|
||||
* @returns {void}
|
||||
*/
|
||||
ThisExpression(node) {
|
||||
if (isBeforeCallOfSuper()) {
|
||||
setInvalid(node);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reports if this is before `super()`.
|
||||
* @param {ASTNode} node A target node.
|
||||
* @returns {void}
|
||||
*/
|
||||
Super(node) {
|
||||
if (!astUtils.isCallee(node) && isBeforeCallOfSuper()) {
|
||||
setInvalid(node);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Marks `super()` called.
|
||||
* @param {ASTNode} node A target node.
|
||||
* @returns {void}
|
||||
*/
|
||||
"CallExpression:exit"(node) {
|
||||
if (node.callee.type === "Super" && isBeforeCallOfSuper()) {
|
||||
setSuperCalled();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets state.
|
||||
* @returns {void}
|
||||
*/
|
||||
"Program:exit"() {
|
||||
segInfoMap = Object.create(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @fileoverview Rule to restrict what can be thrown as an exception.
|
||||
* @author Dieter Oberkofler
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('../shared/types').Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "Disallow throwing literals as exceptions",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/latest/rules/no-throw-literal"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
object: "Expected an error object to be thrown.",
|
||||
undef: "Do not throw undefined."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
|
||||
ThrowStatement(node) {
|
||||
if (!astUtils.couldBeError(node.argument)) {
|
||||
context.report({ node, messageId: "object" });
|
||||
} else if (node.argument.type === "Identifier") {
|
||||
if (node.argument.name === "undefined") {
|
||||
context.report({ node, messageId: "undef" });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user