Ajout de promotion et de commande

This commit is contained in:
Aubert Marvin
2026-04-25 15:28:39 +02:00
parent eddb103755
commit faa3d7718c
8428 changed files with 1126442 additions and 6 deletions
+420
View File
@@ -0,0 +1,420 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
defaultOptions: [
{
enforceForTSTypes: false,
enforceForClassMembers: true,
getWithoutSet: false,
setWithoutGet: true,
},
],
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",
},
setWithoutGet: {
type: "boolean",
},
enforceForClassMembers: {
type: "boolean",
},
enforceForTSTypes: {
type: "boolean",
},
},
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 }}.",
missingGetterInType: "Getter is not present for type {{ name }}.",
missingSetterInType: "Setter is not present for type {{ name }}.",
},
},
create(context) {
const [
{
getWithoutSet: checkGetWithoutSet,
setWithoutGet: checkSetWithoutGet,
enforceForClassMembers,
enforceForTSTypes,
},
] = context.options;
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 if (node.type === "TSMethodSignature") {
context.report({
node,
messageId: `${messageKind}InType`,
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
data: {
name: astUtils.getFunctionNameWithKind(node),
},
});
} 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));
}
/**
* Checks the given type.
* @param {ASTNode} node `TSTypeLiteral` or `TSInterfaceBody` node to check.
* @returns {void}
* @private
*/
function checkType(node) {
const members =
node.type === "TSTypeLiteral" ? node.members : node.body;
const methodDefinitions = members.filter(
m => m.type === "TSMethodSignature",
);
checkList(methodDefinitions);
}
const listeners = {};
if (checkSetWithoutGet || checkGetWithoutSet) {
listeners.ObjectExpression = checkObjectExpression;
if (enforceForClassMembers) {
listeners.ClassBody = checkClassBody;
}
if (enforceForTSTypes) {
listeners["TSTypeLiteral, TSInterfaceBody"] = checkType;
}
}
return listeners;
},
};
+291
View File
@@ -0,0 +1,291 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "array-bracket-newline",
url: "https://eslint.style/rules/array-bracket-newline",
},
},
],
},
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;
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,
};
},
};
+301
View File
@@ -0,0 +1,301 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "array-bracket-spacing",
url: "https://eslint.style/rules/array-bracket-spacing",
},
},
],
},
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.at(-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,
};
},
};
+493
View File
@@ -0,0 +1,493 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "problem",
defaultOptions: [
{
allowImplicit: false,
checkForEach: false,
allowVoid: false,
},
],
docs: {
description:
"Enforce `return` statements in callbacks of array methods",
recommended: false,
url: "https://eslint.org/docs/latest/rules/array-callback-return",
},
hasSuggestions: true,
schema: [
{
type: "object",
properties: {
allowImplicit: {
type: "boolean",
},
checkForEach: {
type: "boolean",
},
allowVoid: {
type: "boolean",
},
},
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;
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,
};
},
};
+374
View File
@@ -0,0 +1,374 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "array-element-newline",
url: "https://eslint.style/rules/array-element-newline",
},
},
],
},
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,
};
},
};
+418
View File
@@ -0,0 +1,418 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
defaultOptions: ["as-needed"],
docs: {
description: "Require braces around arrow function bodies",
recommended: false,
frozen: true,
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] === "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 ||
blockBody[0].type !== "ReturnStatement"
) {
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;
},
};
},
};
+237
View File
@@ -0,0 +1,237 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "arrow-parens",
url: "https://eslint.style/rules/arrow-parens",
},
},
],
},
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],
]);
},
});
}
},
};
},
};
+188
View File
@@ -0,0 +1,188 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "arrow-spacing",
url: "https://eslint.style/rules/arrow-spacing",
},
},
],
},
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,
};
},
};
+137
View File
@@ -0,0 +1,137 @@
/**
* @fileoverview Rule to check for "block scoped" variables by binding context
* @author Matt DuVall <http://www.mattduvall.com>
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
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.at(-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,
};
},
};
+202
View File
@@ -0,0 +1,202 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "block-spacing",
url: "https://eslint.style/rules/block-spacing",
},
},
],
},
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,
};
},
};
+278
View File
@@ -0,0 +1,278 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "brace-style",
url: "https://eslint.style/rules/brace-style",
},
},
],
},
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),
);
}
},
};
},
};
+216
View File
@@ -0,0 +1,216 @@
/**
* @fileoverview Enforce return after a callback.
* @author Jamund Ferguson
* @deprecated in ESLint v7.0.0
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Node.js rules were moved out of ESLint core.",
url: "https://eslint.org/docs/latest/use/migrating-to-7.0.0#deprecate-node-rules",
deprecatedSince: "7.0.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"eslint-plugin-n now maintains deprecated Node.js-related rules.",
plugin: {
name: "eslint-plugin-n",
url: "https://github.com/eslint-community/eslint-plugin-n",
},
rule: {
name: "callback-return",
url: "https://github.com/eslint-community/eslint-plugin-n/tree/master/docs/rules/callback-return.md",
},
},
],
},
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.at(-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.at(-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" });
}
},
};
},
};
+422
View File
@@ -0,0 +1,422 @@
/**
* @fileoverview Rule to flag non-camelcased identifiers
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
defaultOptions: [
{
allow: [],
ignoreDestructuring: false,
ignoreGlobals: false,
ignoreImports: false,
properties: "always",
},
],
docs: {
description: "Enforce camelcase naming convention",
recommended: false,
frozen: true,
url: "https://eslint.org/docs/latest/rules/camelcase",
},
schema: [
{
type: "object",
properties: {
ignoreDestructuring: {
type: "boolean",
},
ignoreImports: {
type: "boolean",
},
ignoreGlobals: {
type: "boolean",
},
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 [
{
allow,
ignoreDestructuring,
ignoreGlobals,
ignoreImports,
properties,
},
] = context.options;
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;
}
/*
* Import attribute keys are always ignored
*/
if (astUtils.isImportAttributeKey(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) ||
astUtils.isImportAttributeKey(id)
) {
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" ||
astUtils.isImportAttributeKey(node) ||
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);
},
};
},
};
+325
View File
@@ -0,0 +1,325 @@
/**
* @fileoverview enforce or disallow capitalization of the first letter of a comment
* @author Kevin Partington
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
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?
LETTER_PATTERN = /\p{L}/u;
/*
* 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('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
docs: {
description:
"Enforce or disallow capitalization of the first letter of a comment",
recommended: false,
frozen: true,
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;
}
// Get the first Unicode character (1 or 2 code units).
const [firstWordChar] = commentWordCharsOnly;
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);
const char = match[0];
// Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*)
const charIndex = comment.range[0] + match.index + 2;
return fixer.replaceTextRange(
[charIndex, charIndex + char.length],
capitalize === "always"
? char.toLocaleUpperCase()
: char.toLocaleLowerCase(),
);
},
});
}
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
Program() {
const comments = sourceCode.getAllComments();
comments
.filter(token => token.type !== "Shebang")
.forEach(processComment);
},
};
},
};
+250
View File
@@ -0,0 +1,250 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
dialects: ["javascript", "typescript"],
language: "javascript",
type: "suggestion",
defaultOptions: [
{
enforceForClassFields: true,
exceptMethods: [],
ignoreOverrideMethods: false,
},
],
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",
},
ignoreOverrideMethods: {
type: "boolean",
},
ignoreClassesWithImplements: {
enum: ["all", "public-fields"],
},
},
additionalProperties: false,
},
],
messages: {
missingThis: "Expected 'this' to be used by class {{name}}.",
},
},
create(context) {
const [options] = context.options;
const {
enforceForClassFields,
ignoreOverrideMethods,
ignoreClassesWithImplements,
} = options;
const exceptMethods = new Set(options.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 "AccessorProperty":
case "PropertyDefinition":
return !node.static && enforceForClassFields;
default:
return false;
}
}
/**
* Check if the node's parent class implements any interfaces
* @param {ASTNode} node node to check
* @returns {boolean} True if parent class implements interfaces
* @private
*/
function hasImplements(node) {
const classNode = node.parent.parent;
return (
classNode?.type === "ClassDeclaration" &&
classNode.implements?.length > 0
);
}
/**
* 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;
}
if (ignoreOverrideMethods && node.override) {
return false;
}
if (ignoreClassesWithImplements) {
const implementsInterfaces = hasImplements(node);
if (implementsInterfaces) {
if (
ignoreClassesWithImplements === "all" ||
(ignoreClassesWithImplements === "public-fields" &&
node.key.type !== "PrivateIdentifier" &&
(!node.accessibility ||
node.accessibility === "public"))
) {
return false;
}
}
}
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.
*/
"AccessorProperty > *.key:exit": pushContext,
"AccessorProperty:exit": popContext,
"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 && {
"AccessorProperty > ArrowFunctionExpression.value":
enterFunction,
"AccessorProperty > ArrowFunctionExpression.value:exit":
exitFunction,
"PropertyDefinition > ArrowFunctionExpression.value":
enterFunction,
"PropertyDefinition > ArrowFunctionExpression.value:exit":
exitFunction,
}),
};
},
};
+424
View File
@@ -0,0 +1,424 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "comma-dangle",
url: "https://eslint.style/rules/comma-dangle",
},
},
],
},
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.at(-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],
};
},
};
+208
View File
@@ -0,0 +1,208 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "comma-spacing",
url: "https://eslint.style/rules/comma-spacing",
},
},
],
},
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,
};
},
};
+391
View File
@@ -0,0 +1,391 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "comma-style",
url: "https://eslint.style/rules/comma-style",
},
},
],
},
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.hasOwn(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.at(-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;
},
};
+201
View File
@@ -0,0 +1,201 @@
/**
* @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
//------------------------------------------------------------------------------
const THRESHOLD_DEFAULT = 20;
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
defaultOptions: [THRESHOLD_DEFAULT],
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,
},
variant: {
enum: ["classic", "modified"],
},
},
additionalProperties: false,
},
],
},
],
messages: {
complex:
"{{name}} has a complexity of {{complexity}}. Maximum allowed is {{max}}.",
},
},
create(context) {
const sourceCode = context.sourceCode;
const option = context.options[0];
let threshold = THRESHOLD_DEFAULT;
let VARIANT = "classic";
if (typeof option === "object") {
if (
Object.hasOwn(option, "maximum") ||
Object.hasOwn(option, "max")
) {
threshold = option.maximum || option.max;
}
if (Object.hasOwn(option, "variant")) {
VARIANT = option.variant;
}
} else if (typeof option === "number") {
threshold = option;
}
const IS_MODIFIED_COMPLEXITY = VARIANT === "modified";
//--------------------------------------------------------------------------
// 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,
AssignmentPattern: increaseComplexity,
// Avoid `default`
"SwitchCase[test]": () =>
IS_MODIFIED_COMPLEXITY || increaseComplexity(),
SwitchStatement: () =>
IS_MODIFIED_COMPLEXITY && increaseComplexity(),
// Logical assignment operators have short-circuiting behavior
AssignmentExpression(node) {
if (astUtils.isLogicalAssignmentOperator(node.operator)) {
increaseComplexity();
}
},
MemberExpression(node) {
if (node.optional === true) {
increaseComplexity();
}
},
CallExpression(node) {
if (node.optional === true) {
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;
let loc = node.loc;
if (codePath.origin === "class-field-initializer") {
name = "class field initializer";
} else if (codePath.origin === "class-static-block") {
name = "class static block";
loc = sourceCode.getFirstToken(node).loc;
} else {
name = astUtils.getFunctionNameWithKind(node);
loc = astUtils.getFunctionHeadLoc(node, sourceCode);
}
context.report({
node,
loc,
messageId: "complex",
data: {
name: upperCaseFirst(name),
complexity,
max: threshold,
},
});
}
},
};
},
};
+251
View File
@@ -0,0 +1,251 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "computed-property-spacing",
url: "https://eslint.style/rules/computed-property-spacing",
},
},
],
},
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;
},
};
+221
View File
@@ -0,0 +1,221 @@
/**
* @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('../types').Rule.RuleModule} */
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",
},
},
additionalProperties: false,
},
],
defaultOptions: [{ treatUndefinedAsUnspecified: 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 [{ treatUndefinedAsUnspecified }] = context.options;
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,
};
},
};
+179
View File
@@ -0,0 +1,179 @@
/**
* @fileoverview Rule to enforce consistent naming of "this" context variables
* @author Raphael Pigulla
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
docs: {
description:
"Enforce consistent naming when capturing the current execution context",
recommended: false,
frozen: true,
url: "https://eslint.org/docs/latest/rules/consistent-this",
},
schema: {
type: "array",
items: {
type: "string",
minLength: 1,
},
uniqueItems: true,
},
defaultOptions: ["that"],
messages: {
aliasNotAssignedToThis:
"Designated alias '{{name}}' is not assigned to 'this'.",
unexpectedAlias: "Unexpected alias '{{name}}' for 'this'.",
},
},
create(context) {
const aliases = context.options;
const sourceCode = context.sourceCode;
/**
* 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);
// if this is program scope we also need to check module scope
const extraScope =
node.type === "Program" && node.sourceType === "module"
? scope.childScopes[0]
: null;
aliases.forEach(alias => {
checkWasAssigned(alias, scope);
if (extraScope) {
checkWasAssigned(alias, extraScope);
}
});
}
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);
}
},
};
},
};
+453
View File
@@ -0,0 +1,453 @@
/**
* @fileoverview A rule to verify `super()` callings in constructor.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// 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"
);
}
/**
* 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.at(-1);
return isPossibleConstructor(lastExpression);
}
default:
return false;
}
}
/**
* A class to store information about a code path segment.
*/
class SegmentInfo {
/**
* Indicates if super() is called in all code paths.
* @type {boolean}
*/
calledInEveryPaths = false;
/**
* Indicates if super() is called in any code paths.
* @type {boolean}
*/
calledInSomePaths = false;
/**
* The nodes which have been validated and don't need to be reconsidered.
* @type {ASTNode[]}
*/
validNodes = [];
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
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.",
},
},
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;
/**
* @type {Record<string, SegmentInfo>}
*/
const 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
);
}
/**
* Determines if a segment has been seen in the traversal.
* @param {CodePathSegment} segment A code path segment to check.
* @returns {boolean} `true` if the segment has been seen.
*/
function hasSegmentBeenSeen(segment) {
return !!segInfoMap[segment.id];
}
/**
* 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) {
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 returnedSegments = codePath.returnedSegments;
const calledInEveryPaths =
returnedSegments.every(isCalledInEveryPath);
const calledInSomePaths =
returnedSegments.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.
* @param {CodePathSegment} node Node that starts the segment.
* @returns {void}
*/
onCodePathSegmentStart(segment, node) {
funcInfo.currentSegments.add(segment);
if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
return;
}
// Initialize info.
const info = (segInfoMap[segment.id] = new SegmentInfo());
const seenPrevSegments =
segment.prevSegments.filter(hasSegmentBeenSeen);
// When there are previous segments, aggregates these.
if (seenPrevSegments.length > 0) {
info.calledInSomePaths =
seenPrevSegments.some(isCalledInSomePath);
info.calledInEveryPaths =
seenPrevSegments.every(isCalledInEveryPath);
}
/*
* ForStatement > *.update segments are a special case as they are created in advance,
* without seen previous segments. Since they logically don't affect `calledInEveryPaths`
* calculations, and they can never be a lone previous segment of another one, we'll set
* their `calledInEveryPaths` to `true` to effectively ignore them in those calculations.
* .
*/
if (
node.parent &&
node.parent.type === "ForStatement" &&
node.parent.update === node
) {
info.calledInEveryPaths = true;
}
},
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.isConstructor && funcInfo.hasExtends)) {
return;
}
funcInfo.codePath.traverseSegments(
{ first: toSegment, last: fromSegment },
(segment, controller) => {
const info = segInfoMap[segment.id];
// skip segments after the loop
if (!info) {
controller.skip();
return;
}
const seenPrevSegments =
segment.prevSegments.filter(hasSegmentBeenSeen);
const calledInSomePreviousPaths =
seenPrevSegments.some(isCalledInSomePath);
const calledInEveryPreviousPaths =
seenPrevSegments.every(isCalledInEveryPath);
info.calledInSomePaths ||= calledInSomePreviousPaths;
info.calledInEveryPaths ||= calledInEveryPreviousPaths;
// If flags become true anew, reports the valid nodes.
if (calledInSomePreviousPaths) {
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.isConstructor && funcInfo.hasExtends)) {
return;
}
// Skips except `super()`.
if (node.callee.type !== "Super") {
return;
}
// Reports if needed.
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);
}
}
},
/**
* 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.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;
}
}
},
};
},
};
+425
View File
@@ -0,0 +1,425 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
docs: {
description:
"Enforce consistent brace style for all control statements",
recommended: false,
frozen: true,
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,
},
],
},
defaultOptions: ["all"],
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 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;
}
/**
* 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 ||
astUtils.areBracesNecessary(body, sourceCode))
) {
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();
},
};
},
};
+51
View File
@@ -0,0 +1,51 @@
/**
* @fileoverview Rule to enforce `default` clauses in `switch` statements to be last
* @author Milos Djermanovic
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
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",
});
}
},
};
},
};
+103
View File
@@ -0,0 +1,103 @@
/**
* @fileoverview require default case in switch statements
* @author Aliaksei Shytkin
*/
"use strict";
const DEFAULT_COMMENT_PATTERN = /^no default$/iu;
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
defaultOptions: [{}],
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;
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.at(-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",
});
}
}
},
};
},
};
+78
View File
@@ -0,0 +1,78 @@
/**
* @fileoverview enforce default parameters to be last
* @author Chiawen Chen
*/
"use strict";
/**
* Checks if node is required: i.e. does not have a default value or ? optional indicator.
* @param {ASTNode} node the node to be evaluated
* @returns {boolean} true if the node is required, false if not.
*/
function isRequiredParameter(node) {
return !(
node.type === "AssignmentPattern" ||
node.type === "RestElement" ||
node.optional
);
}
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
dialects: ["javascript", "typescript"],
language: "javascript",
type: "suggestion",
docs: {
description: "Enforce default parameters to be last",
recommended: false,
frozen: true,
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 hasSeenRequiredParameter = false;
for (let i = node.params.length - 1; i >= 0; i -= 1) {
const current = node.params[i];
const param =
current.type === "TSParameterProperty"
? current.parameter
: current;
if (isRequiredParameter(param)) {
hasSeenRequiredParameter = true;
continue;
}
if (hasSeenRequiredParameter) {
context.report({
node: current,
messageId: "shouldBeLast",
});
}
}
}
return {
FunctionDeclaration: handleFunction,
FunctionExpression: handleFunction,
ArrowFunctionExpression: handleFunction,
};
},
};
+138
View File
@@ -0,0 +1,138 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "dot-location",
url: "https://eslint.style/rules/dot-location",
},
},
],
},
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,
};
},
};
+216
View File
@@ -0,0 +1,216 @@
/**
* @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_$][\w$]*$/u;
// `null` literal must be handled separately.
const literalTypesToCheck = new Set(["string", "boolean"]);
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
defaultOptions: [
{
allowKeywords: true,
allowPattern: "",
},
],
docs: {
description: "Enforce dot notation whenever possible",
recommended: false,
frozen: true,
url: "https://eslint.org/docs/latest/rules/dot-notation",
},
schema: [
{
type: "object",
properties: {
allowKeywords: {
type: "boolean",
},
allowPattern: {
type: "string",
},
},
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;
const allowKeywords = 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}"]`,
);
},
});
}
},
};
},
};
+135
View File
@@ -0,0 +1,135 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "eol-last",
url: "https://eslint.style/rules/eol-last",
},
},
],
},
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.at(-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.at(-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], "");
},
});
}
},
};
},
};
+210
View File
@@ -0,0 +1,210 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
hasSuggestions: true,
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}}'.",
replaceOperator:
"Use '{{expectedOperator}}' instead of '{{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,
);
const commonReportParams = {
node,
loc: operatorToken.loc,
messageId: "unexpected",
data: { expectedOperator, actualOperator: node.operator },
};
if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) {
context.report({
...commonReportParams,
fix(fixer) {
return fixer.replaceText(
operatorToken,
expectedOperator,
);
},
});
} else {
context.report({
...commonReportParams,
suggest: [
{
messageId: "replaceOperator",
data: {
expectedOperator,
actualOperator: node.operator,
},
fix: fixer =>
fixer.replaceText(
operatorToken,
expectedOperator,
),
},
],
});
}
}
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}=`);
},
};
},
};
+168
View File
@@ -0,0 +1,168 @@
/**
* @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('../types').Rule.RuleModule} */
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({
loc: {
start: node.loc.start,
end: sourceCode.getTokenBefore(node.body).loc.end,
},
messageId: "incorrectDirection",
});
}
/**
* check the right side of the assignment
* @param {ASTNode} update UpdateExpression to check
* @param {number} dir expected direction that could either be turned around or invalidated
* @returns {number} 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 {number} 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 {number} 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);
}
}
}
},
};
},
};
+281
View File
@@ -0,0 +1,281 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "function-call-spacing",
url: "https://eslint.style/rules/function-call-spacing",
},
},
],
},
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 The 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);
},
};
},
};
+338
View File
@@ -0,0 +1,338 @@
/**
* @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 {number} 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('../types').Rule.RuleModule} */
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,
frozen: true,
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);
}
},
};
},
};
+192
View File
@@ -0,0 +1,192 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
defaultOptions: ["always", {}],
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[1].generators) {
return context.options[1].generators;
}
return context.options[0];
}
/**
* 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,
};
},
};
+221
View File
@@ -0,0 +1,221 @@
/**
* @fileoverview Rule to enforce a particular function style
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
dialects: ["javascript", "typescript"],
language: "javascript",
type: "suggestion",
defaultOptions: [
"expression",
{
allowArrowFunctions: false,
allowTypeAnnotation: false,
overrides: {},
},
],
docs: {
description:
"Enforce the consistent use of either `function` declarations or expressions assigned to variables",
recommended: false,
frozen: true,
url: "https://eslint.org/docs/latest/rules/func-style",
},
schema: [
{
enum: ["declaration", "expression"],
},
{
type: "object",
properties: {
allowArrowFunctions: {
type: "boolean",
},
allowTypeAnnotation: {
type: "boolean",
},
overrides: {
type: "object",
properties: {
namedExports: {
enum: ["declaration", "expression", "ignore"],
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
],
messages: {
expression: "Expected a function expression.",
declaration: "Expected a function declaration.",
},
},
create(context) {
const [style, { allowArrowFunctions, allowTypeAnnotation, overrides }] =
context.options;
const enforceDeclarations = style === "declaration";
const { namedExports: exportFunctionStyle } = overrides;
const stack = [];
/**
* Checks if a function declaration is part of an overloaded function
* @param {ASTNode} node The function declaration node to check
* @returns {boolean} True if the function is overloaded
*/
function isOverloadedFunction(node) {
const functionName = node.id.name;
if (node.parent.type === "ExportNamedDeclaration") {
return node.parent.parent.body.some(
member =>
member.type === "ExportNamedDeclaration" &&
member.declaration?.type === "TSDeclareFunction" &&
member.declaration.id.name === functionName,
);
}
if (node.parent.type === "SwitchCase") {
return node.parent.parent.cases.some(switchCase =>
switchCase.consequent.some(
member =>
member.type === "TSDeclareFunction" &&
member.id.name === functionName,
),
);
}
return (
Array.isArray(node.parent.body) &&
node.parent.body.some(
member =>
member.type === "TSDeclareFunction" &&
member.id.name === functionName,
)
);
}
const nodesToCheck = {
FunctionDeclaration(node) {
stack.push(false);
if (
!enforceDeclarations &&
node.parent.type !== "ExportDefaultDeclaration" &&
(typeof exportFunctionStyle === "undefined" ||
node.parent.type !== "ExportNamedDeclaration") &&
!isOverloadedFunction(node)
) {
context.report({ node, messageId: "expression" });
}
if (
node.parent.type === "ExportNamedDeclaration" &&
exportFunctionStyle === "expression" &&
!isOverloadedFunction(node)
) {
context.report({ node, messageId: "expression" });
}
},
"FunctionDeclaration:exit"() {
stack.pop();
},
FunctionExpression(node) {
stack.push(false);
if (
enforceDeclarations &&
node.parent.type === "VariableDeclarator" &&
(typeof exportFunctionStyle === "undefined" ||
node.parent.parent.parent.type !==
"ExportNamedDeclaration") &&
!(allowTypeAnnotation && node.parent.id.typeAnnotation)
) {
context.report({
node: node.parent,
messageId: "declaration",
});
}
if (
node.parent.type === "VariableDeclarator" &&
node.parent.parent.parent.type ===
"ExportNamedDeclaration" &&
exportFunctionStyle === "declaration" &&
!(allowTypeAnnotation && node.parent.id.typeAnnotation)
) {
context.report({
node: node.parent,
messageId: "declaration",
});
}
},
"FunctionExpression:exit"() {
stack.pop();
},
"ThisExpression, Super"() {
if (stack.length > 0) {
stack[stack.length - 1] = true;
}
},
};
if (!allowArrowFunctions) {
nodesToCheck.ArrowFunctionExpression = function () {
stack.push(false);
};
nodesToCheck["ArrowFunctionExpression:exit"] = function (node) {
const hasThisOrSuperExpr = stack.pop();
if (
!hasThisOrSuperExpr &&
node.parent.type === "VariableDeclarator"
) {
if (
enforceDeclarations &&
(typeof exportFunctionStyle === "undefined" ||
node.parent.parent.parent.type !==
"ExportNamedDeclaration") &&
!(allowTypeAnnotation && node.parent.id.typeAnnotation)
) {
context.report({
node: node.parent,
messageId: "declaration",
});
}
if (
node.parent.parent.parent.type ===
"ExportNamedDeclaration" &&
exportFunctionStyle === "declaration" &&
!(allowTypeAnnotation && node.parent.id.typeAnnotation)
) {
context.report({
node: node.parent,
messageId: "declaration",
});
}
}
};
}
return nodesToCheck;
},
};
+166
View File
@@ -0,0 +1,166 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "function-call-argument-newline",
url: "https://eslint.style/rules/function-call-argument-newline",
},
},
],
},
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,
};
},
};
+368
View File
@@ -0,0 +1,368 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "function-paren-newline",
url: "https://eslint.style/rules/function-paren-newline",
},
},
],
},
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.at(-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.at(-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);
}
}
},
};
},
};
+246
View File
@@ -0,0 +1,246 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "generator-star-spacing",
url: "https://eslint.style/rules/generator-star-spacing",
},
},
],
},
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,
};
},
};
+242
View File
@@ -0,0 +1,242 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "problem",
defaultOptions: [
{
allowImplicit: false,
},
],
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",
},
},
additionalProperties: false,
},
],
messages: {
expected: "Expected to return a value in {{name}}.",
expectedAlways: "Expected {{name}} to always return a value.",
},
},
create(context) {
const [{ allowImplicit }] = context.options;
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 (!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,
};
},
};
+117
View File
@@ -0,0 +1,117 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Node.js rules were moved out of ESLint core.",
url: "https://eslint.org/docs/latest/use/migrating-to-7.0.0#deprecate-node-rules",
deprecatedSince: "7.0.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"eslint-plugin-n now maintains deprecated Node.js-related rules.",
plugin: {
name: "eslint-plugin-n",
url: "https://github.com/eslint-community/eslint-plugin-n",
},
rule: {
name: "global-require",
url: "https://github.com/eslint-community/eslint-plugin-n/tree/master/docs/rules/global-require.md",
},
},
],
},
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" });
}
}
},
};
},
};
+268
View File
@@ -0,0 +1,268 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
defaultOptions: [
"anyOrder",
{
enforceForTSTypes: false,
},
],
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"] },
{
type: "object",
properties: {
enforceForTSTypes: {
type: "boolean",
},
},
additionalProperties: false,
},
],
messages: {
notGrouped:
"Accessor pair {{ formerName }} and {{ latterName }} should be grouped.",
invalidOrder:
"Expected {{ latterName }} to be before {{ formerName }}.",
},
},
create(context) {
const [order, { enforceForTSTypes }] = context.options;
const { sourceCode } = context;
/**
* 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.type !== "TSMethodSignature"
? latterNode.value
: latterNode,
sourceCode,
),
data: {
formerName: astUtils.getFunctionNameWithKind(
formerNode.type !== "TSMethodSignature"
? formerNode.value
: formerNode,
),
latterName: astUtils.getFunctionNameWithKind(
latterNode.type !== "TSMethodSignature"
? latterNode.value
: latterNode,
),
},
});
}
/**
* 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,
);
},
"TSTypeLiteral, TSInterfaceBody"(node) {
if (enforceForTSTypes) {
checkList(
node.type === "TSTypeLiteral"
? node.members
: node.body,
n => n.type === "TSMethodSignature",
);
}
},
};
},
};
+85
View File
@@ -0,0 +1,85 @@
/**
* @fileoverview Rule to flag for-in loops without if statements inside
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
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" });
},
};
},
};
+122
View File
@@ -0,0 +1,122 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Node.js rules were moved out of ESLint core.",
url: "https://eslint.org/docs/latest/use/migrating-to-7.0.0#deprecate-node-rules",
deprecatedSince: "7.0.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"eslint-plugin-n now maintains deprecated Node.js-related rules.",
plugin: {
name: "eslint-plugin-n",
url: "https://github.com/eslint-community/eslint-plugin-n",
},
rule: {
name: "handle-callback-err",
url: "https://github.com/eslint-community/eslint-plugin-n/tree/master/docs/rules/handle-callback-err.md",
},
},
],
},
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,
};
},
};
+241
View File
@@ -0,0 +1,241 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "The rule was renamed.",
url: "https://eslint.org/blog/2020/07/eslint-v7.5.0-released/#deprecating-id-blacklist",
deprecatedSince: "7.5.0",
availableUntil: "11.0.0",
replacedBy: [
{
rule: {
name: "id-denylist",
url: "https://eslint.org/docs/rules/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);
}
},
};
},
};
+223
View File
@@ -0,0 +1,223 @@
/**
* @fileoverview Rule that warns when identifier names that are
* specified in the configuration are used.
* @author Keith Cirkel (http://keithcirkel.co.uk)
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// 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('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
defaultOptions: [],
docs: {
description: "Disallow specified identifiers",
recommended: false,
frozen: true,
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) {
// Import attributes are defined by environments, so naming conventions shouldn't apply to them
if (astUtils.isImportAttributeKey(node)) {
return false;
}
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);
}
},
};
},
};
+217
View File
@@ -0,0 +1,217 @@
/**
* @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");
const {
getModuleExportName,
isImportAttributeKey,
} = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
defaultOptions: [
{
exceptionPatterns: [],
exceptions: [],
min: 2,
properties: "always",
},
],
docs: {
description: "Enforce minimum and maximum identifier lengths",
recommended: false,
frozen: true,
url: "https://eslint.org/docs/latest/rules/id-length",
},
schema: [
{
type: "object",
properties: {
min: {
type: "integer",
},
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;
const { max: maxLength = Infinity, min: minLength } = options;
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 &&
!isImportAttributeKey(node) &&
!parent.computed &&
parent.key.name === node.name
);
},
ImportSpecifier(parent, node) {
return (
parent.local === node &&
getModuleExportName(parent.imported) !==
getModuleExportName(parent.local)
);
},
ImportDefaultSpecifier: true,
ImportNamespaceSpecifier: 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 },
});
}
},
};
},
};
+363
View File
@@ -0,0 +1,363 @@
/**
* @fileoverview Rule to flag non-matching identifiers
* @author Matthieu Larcher
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
defaultOptions: [
"^.+$",
{
classFields: false,
ignoreDestructuring: false,
onlyDeclarations: false,
properties: false,
},
],
docs: {
description:
"Require identifiers to match a specified regular expression",
recommended: false,
frozen: true,
url: "https://eslint.org/docs/latest/rules/id-match",
},
schema: [
{
type: "string",
},
{
type: "object",
properties: {
properties: {
type: "boolean",
},
classFields: {
type: "boolean",
},
onlyDeclarations: {
type: "boolean",
},
ignoreDestructuring: {
type: "boolean",
},
},
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,
{
classFields: checkClassFields,
ignoreDestructuring,
onlyDeclarations,
properties: checkProperties,
},
] = context.options;
const regexp = new RegExp(pattern, "u");
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) ||
astUtils.isImportAttributeKey(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);
}
},
};
},
};
+125
View File
@@ -0,0 +1,125 @@
/**
* @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('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "implicit-arrow-linebreak",
url: "https://eslint.style/rules/implicit-arrow-linebreak",
},
},
],
},
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),
};
},
};
+1369
View File
File diff suppressed because it is too large Load Diff

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