eslint-plugin-functional
Version:
ESLint rules to promote functional programming in TypeScript.
1,490 lines (1,477 loc) • 163 kB
JavaScript
import { deepmerge } from 'deepmerge-ts';
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
import { RuleCreator, getParserServices } from '@typescript-eslint/utils/eslint-utils';
import { createRequire } from 'node:module';
import { Immutability, getDefaultOverrides, getTypeImmutability } from 'is-immutable-type';
import { isIntrinsicErrorType } from 'ts-api-utils';
import typeMatchesSpecifier from 'ts-declaration-location';
import escapeRegExp from 'escape-string-regexp';
const require$1 = createRequire(import.meta.url);
const typescript = (() => {
try {
return require$1("typescript");
}
catch {
return undefined;
}
})();
// export default (await import("typescript")
// .then((r) => r.default)
// .catch(() => undefined)) as typeof typescript | undefined;
/**
* The settings that have been loaded - so we don't have to reload them.
*/
const cachedSettings = new WeakMap();
/**
* Get the immutability overrides defined in the settings.
*/
function getImmutabilityOverrides({ immutability, }) {
if (immutability === undefined) {
return undefined;
}
if (!cachedSettings.has(immutability)) {
const overrides = loadImmutabilityOverrides(immutability);
cachedSettings.set(immutability, overrides);
return overrides;
}
return cachedSettings.get(immutability);
}
/**
* Get all the overrides and upgrade them.
*/
function loadImmutabilityOverrides(immutabilitySettings) {
const overridesSetting = immutabilitySettings?.overrides;
if (overridesSetting === undefined) {
return undefined;
}
const raw = Array.isArray(overridesSetting) ? overridesSetting : (overridesSetting.values ?? []);
const upgraded = raw.map((rawValue) => {
const { type, to, from, ...rest } = rawValue;
const value = {
type,
to: typeof to === "string" ? Immutability[to] : to,
from: from === undefined ? undefined : typeof from === "string" ? Immutability[from] : from,
};
if (value.type === undefined) {
throw new Error(`Override is missing required "type" property. Value: "${JSON.stringify(rawValue)}"`);
}
if (value.to === undefined) {
throw new Error(`Override is missing required "to" property. Value: "${JSON.stringify(rawValue)}"`);
}
const restKeys = Object.keys(rest);
if (restKeys.length > 0) {
throw new Error(`Override is contains unknown property(s) "${restKeys.join(", ")}". Value: "${JSON.stringify(rawValue)}"`);
}
return value;
});
const keepDefault = Array.isArray(overridesSetting) || overridesSetting.keepDefault !== false;
return keepDefault ? [...getDefaultOverrides(), ...upgraded] : upgraded;
}
// eslint-disable-next-line ts/naming-convention -- This is a special var.
const __VERSION__ = "9.0.1";
function typeMatchesPattern(program, type, typeNode, include, exclude = []) {
if (include.length === 0) {
return false;
}
let mut_shouldInclude = false;
const typeNameAlias = getTypeAliasName(type, typeNode);
if (typeNameAlias !== null) {
const testTypeNameAlias = (pattern) => typeof pattern === "string" ? pattern === typeNameAlias : pattern.test(typeNameAlias);
if (exclude.some(testTypeNameAlias)) {
return false;
}
mut_shouldInclude ||= include.some(testTypeNameAlias);
}
const typeValue = getTypeAsString(program, type, typeNode);
const testTypeValue = (pattern) => typeof pattern === "string" ? pattern === typeValue : pattern.test(typeValue);
if (exclude.some(testTypeValue)) {
return false;
}
mut_shouldInclude ||= include.some(testTypeValue);
const typeNameName = extractTypeName(typeValue);
if (typeNameName !== null) {
const testTypeNameName = (pattern) => typeof pattern === "string" ? pattern === typeNameName : pattern.test(typeNameName);
if (exclude.some(testTypeNameName)) {
return false;
}
mut_shouldInclude ||= include.some(testTypeNameName);
}
// Special handling for arrays not written in generic syntax.
if (program.getTypeChecker().isArrayType(type) && typeNode !== null) {
if ((typescript.isTypeOperatorNode(typeNode) && typeNode.operator === typescript.SyntaxKind.ReadonlyKeyword) ||
(typescript.isTypeOperatorNode(typeNode.parent) && typeNode.parent.operator === typescript.SyntaxKind.ReadonlyKeyword)) {
const testIsReadonlyArray = (pattern) => typeof pattern === "string" && pattern === "ReadonlyArray";
if (exclude.some(testIsReadonlyArray)) {
return false;
}
mut_shouldInclude ||= include.some(testIsReadonlyArray);
}
else {
const testIsArray = (pattern) => typeof pattern === "string" && pattern === "Array";
if (exclude.some(testIsArray)) {
return false;
}
mut_shouldInclude ||= include.some(testIsArray);
}
}
return mut_shouldInclude;
}
/**
* Get the type alias name from the given type data.
*
* Null will be returned if the type is not a type alias.
*/
function getTypeAliasName(type, typeNode) {
if (typeNode === null) {
const t = "target" in type ? type.target : type;
return t.aliasSymbol?.getName() ?? null;
}
return typescript.isTypeAliasDeclaration(typeNode.parent) ? typeNode.parent.name.getText() : null;
}
/**
* Get the type as a string.
*/
function getTypeAsString(program, type, typeNode) {
return typeNode === null
? program
.getTypeChecker()
.typeToString(type, undefined, typescript.TypeFormatFlags.AddUndefined |
typescript.TypeFormatFlags.NoTruncation |
typescript.TypeFormatFlags.OmitParameterModifiers |
typescript.TypeFormatFlags.UseFullyQualifiedType |
typescript.TypeFormatFlags.WriteArrayAsGenericType |
typescript.TypeFormatFlags.WriteArrowStyleSignature |
typescript.TypeFormatFlags.WriteTypeArgumentsOfSignature)
: typeNode.getText();
}
/**
* Get the type name extracted from the the type's string.
*
* This only work if the type is a type reference.
*/
function extractTypeName(typeValue) {
const match = /^([^<]+)<.+>$/u.exec(typeValue);
return match?.[1] ?? null;
}
/**
* Create a function that processes common options and then runs the given
* check.
*/
function checkNode(check, context, options) {
return (node) => {
const result = check(node, context, options);
// eslint-disable-next-line functional/no-loop-statements -- can't really be avoided.
for (const descriptor of result.descriptors) {
result.context.report(descriptor);
}
};
}
/**
* Create a rule.
*/
function createRule(name, meta, defaultOptions, ruleFunctionsMap) {
return createRuleUsingFunction(name, meta, defaultOptions, () => ruleFunctionsMap);
}
/**
* Create a rule.
*/
function createRuleUsingFunction(name, meta, defaultOptions, createFunction) {
const ruleCreator = RuleCreator((ruleName) => `https://github.com/eslint-functional/eslint-plugin-functional/blob/v${__VERSION__}/docs/rules/${ruleName}.md`);
return ruleCreator({
name,
meta,
defaultOptions,
create: (context, options) => {
const ruleFunctionsMap = createFunction(context, options);
return Object.fromEntries(Object.entries(ruleFunctionsMap).map(([nodeSelector, ruleFunction]) => [
nodeSelector,
checkNode(ruleFunction, context, options),
]));
},
});
}
/**
* Get the type of the the given node.
*/
function getTypeOfNode(node, context) {
const { esTreeNodeToTSNodeMap } = getParserServices(context);
const tsNode = esTreeNodeToTSNodeMap.get(node);
return getTypeOfTSNode(tsNode, context);
}
/**
* Get the type of the the given node.
*/
function getTypeDataOfNode(node, context) {
const { esTreeNodeToTSNodeMap } = getParserServices(context);
const tsNode = esTreeNodeToTSNodeMap.get(node);
return [getTypeOfTSNode(tsNode, context), tsNode.type ?? null];
}
/**
* Get the type of the the given ts node.
*/
function getTypeOfTSNode(node, context) {
const { program } = getParserServices(context);
const checker = program.getTypeChecker();
const nodeType = checker.getTypeAtLocation(node);
const constrained = checker.getBaseConstraintOfType(nodeType);
return constrained ?? nodeType;
}
/**
* Get the return type of the the given function node.
*/
function getReturnTypesOfFunction(node, context) {
if (typescript === undefined) {
return null;
}
const parserServices = getParserServices(context);
const checker = parserServices.program.getTypeChecker();
const type = getTypeOfNode(node, context);
const signatures = checker.getSignaturesOfType(type, typescript.SignatureKind.Call);
return signatures.map((signature) => checker.getReturnTypeOfSignature(signature));
}
/**
* Does the given function have overloads?
*/
function isImplementationOfOverload(func, context) {
if (typescript === undefined) {
return false;
}
const parserServices = getParserServices(context);
const checker = parserServices.program.getTypeChecker();
const signature = parserServices.esTreeNodeToTSNodeMap.get(func);
return checker.isImplementationOfOverload(signature) === true;
}
/**
* Get the type immutability of the the given node or type.
*/
function getTypeImmutabilityOfNode(node, context, maxImmutability, explicitOverrides) {
if (typescript === undefined) {
return Immutability.Unknown;
}
const parserServices = getParserServices(context);
const overrides = getImmutabilityOverrides(context.settings);
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
let mut_typeLike = tsNode.type;
if (mut_typeLike === undefined) {
mut_typeLike = getTypeOfTSNode(tsNode, context);
if (isIntrinsicErrorType(mut_typeLike)) {
return Immutability.Unknown;
}
}
return getTypeImmutability(parserServices.program, mut_typeLike, overrides,
// Don't use the global cache in testing environments as it may cause errors when switching between different config options.
process.env["NODE_ENV"] !== "test", maxImmutability, typeMatchesPattern);
}
/**
* Get the type immutability of the the given type.
*/
function getTypeImmutabilityOfType(typeOrTypeNode, context, maxImmutability, explicitOverrides) {
const parserServices = getParserServices(context);
const overrides = getImmutabilityOverrides(context.settings);
return getTypeImmutability(parserServices.program, typeOrTypeNode, overrides,
// Don't use the global cache in testing environments as it may cause errors when switching between different config options.
process.env["NODE_ENV"] !== "test", maxImmutability);
}
/**
* @file Functions that type guard the given node/type.
*/
const libSpecifier = {
from: "lib",
};
/*
* Node type guards.
*/
function isArrayExpression(node) {
return node.type === AST_NODE_TYPES.ArrayExpression;
}
function isArrayPattern(node) {
return node.type === AST_NODE_TYPES.ArrayPattern;
}
function isAssignmentExpression(node) {
return node.type === AST_NODE_TYPES.AssignmentExpression;
}
function isAssignmentPattern(node) {
return node.type === AST_NODE_TYPES.AssignmentPattern;
}
function isBlockStatement(node) {
return node.type === AST_NODE_TYPES.BlockStatement;
}
function isBreakStatement(node) {
return node.type === AST_NODE_TYPES.BreakStatement;
}
function isCallExpression(node) {
return node.type === AST_NODE_TYPES.CallExpression;
}
function isChainExpression(node) {
return node.type === AST_NODE_TYPES.ChainExpression;
}
function isPropertyDefinition(node) {
return node.type === AST_NODE_TYPES.PropertyDefinition;
}
/**
* Is the given node a class node?
*
* It doesn't matter what type of class.
*/
function isClassLike(node) {
return node.type === AST_NODE_TYPES.ClassDeclaration || node.type === AST_NODE_TYPES.ClassExpression;
}
function isContinueStatement(node) {
return node.type === AST_NODE_TYPES.ContinueStatement;
}
function isExpressionStatement(node) {
return node.type === AST_NODE_TYPES.ExpressionStatement;
}
function isForStatement(node) {
return node.type === AST_NODE_TYPES.ForStatement;
}
function isFunctionDeclaration(node) {
return node.type === AST_NODE_TYPES.FunctionDeclaration;
}
/**
* Is the given node a function expression node?
*
* It doesn't matter what type of function expression.
*/
function isFunctionExpressionLike(node) {
return node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression;
}
/**
* Is the given node a function node?
*
* It doesn't matter what type of function.
*/
function isFunctionLike(node) {
return isFunctionDeclaration(node) || isFunctionExpressionLike(node);
}
function isIdentifier(node) {
return node.type === AST_NODE_TYPES.Identifier;
}
function isIfStatement(node) {
return node.type === AST_NODE_TYPES.IfStatement;
}
function isLabeledStatement(node) {
return node.type === AST_NODE_TYPES.LabeledStatement;
}
function isMemberExpression(node) {
return node.type === AST_NODE_TYPES.MemberExpression;
}
function isMethodDefinition(node) {
return node.type === AST_NODE_TYPES.MethodDefinition;
}
function isNewExpression(node) {
return node.type === AST_NODE_TYPES.NewExpression;
}
function isObjectExpression(node) {
return node.type === AST_NODE_TYPES.ObjectExpression;
}
function isObjectPattern(node) {
return node.type === AST_NODE_TYPES.ObjectPattern;
}
function isPrivateIdentifier(node) {
return node.type === AST_NODE_TYPES.PrivateIdentifier;
}
function isProgram(node) {
return node.type === AST_NODE_TYPES.Program;
}
function isProperty(node) {
return node.type === AST_NODE_TYPES.Property;
}
function isRestElement(node) {
return node.type === AST_NODE_TYPES.RestElement;
}
function isReturnStatement(node) {
return node.type === AST_NODE_TYPES.ReturnStatement;
}
function isSwitchStatement(node) {
return node.type === AST_NODE_TYPES.SwitchStatement;
}
function isThisExpression(node) {
return node.type === AST_NODE_TYPES.ThisExpression;
}
function isThrowStatement(node) {
return node.type === AST_NODE_TYPES.ThrowStatement;
}
function isTryStatement(node) {
return node.type === AST_NODE_TYPES.TryStatement;
}
function isTSArrayType(node) {
return node.type === AST_NODE_TYPES.TSArrayType;
}
function isTSAsExpression(node) {
return node.type === AST_NODE_TYPES.TSAsExpression;
}
function isTSFunctionType(node) {
return node.type === AST_NODE_TYPES.TSFunctionType;
}
function isTSIndexSignature(node) {
return node.type === AST_NODE_TYPES.TSIndexSignature;
}
function isTSMethodSignature(node) {
return node.type === AST_NODE_TYPES.TSMethodSignature;
}
function isTSCallSignatureDeclaration(node) {
return node.type === AST_NODE_TYPES.TSCallSignatureDeclaration;
}
function isTSConstructSignatureDeclaration(node) {
return node.type === AST_NODE_TYPES.TSConstructSignatureDeclaration;
}
function isTSInterfaceBody(node) {
return node.type === AST_NODE_TYPES.TSInterfaceBody;
}
function isTSInterfaceDeclaration(node) {
return node.type === AST_NODE_TYPES.TSInterfaceDeclaration;
}
function isTSInterfaceHeritage(node) {
return node.type === AST_NODE_TYPES.TSInterfaceHeritage;
}
function isTSNonNullExpression(node) {
return node.type === AST_NODE_TYPES.TSNonNullExpression;
}
function isTSNullKeyword(node) {
return node.type === AST_NODE_TYPES.TSNullKeyword;
}
function isTSParameterProperty(node) {
return node.type === AST_NODE_TYPES.TSParameterProperty;
}
function isTSPropertySignature(node) {
return node.type === AST_NODE_TYPES.TSPropertySignature;
}
function isTSTupleType(node) {
return node.type === AST_NODE_TYPES.TSTupleType;
}
function isTSTypeAnnotation(node) {
return node.type === AST_NODE_TYPES.TSTypeAnnotation;
}
function isTSTypeLiteral(node) {
return node.type === AST_NODE_TYPES.TSTypeLiteral;
}
function isTSTypeOperator(node) {
return node.type === AST_NODE_TYPES.TSTypeOperator;
}
function isTSTypePredicate(node) {
return node.type === AST_NODE_TYPES.TSTypePredicate;
}
function isTSTypeReference(node) {
return node.type === AST_NODE_TYPES.TSTypeReference;
}
function isTSUndefinedKeyword(node) {
return node.type === AST_NODE_TYPES.TSUndefinedKeyword;
}
function isTSVoidKeyword(node) {
return node.type === AST_NODE_TYPES.TSVoidKeyword;
}
function isUnaryExpression(node) {
return node.type === AST_NODE_TYPES.UnaryExpression;
}
function isVariableDeclaration(node) {
return node.type === AST_NODE_TYPES.VariableDeclaration;
}
function isYieldExpression(node) {
return node.type === AST_NODE_TYPES.YieldExpression;
}
function hasID(node) {
return Object.hasOwn(node, "id");
}
function hasKey(node) {
return Object.hasOwn(node, "key");
}
function isDefined(value) {
return value !== null && value !== undefined;
}
/*
* TS types type guards.
*/
function isUnionType(type) {
return typescript !== undefined && type.flags === typescript.TypeFlags.Union;
}
function isFunctionLikeType(type) {
return type !== null && type.getCallSignatures().length > 0;
}
function isArrayType(context, type) {
return typeMatches(context, "Array", type);
}
function isMapType(context, type) {
return typeMatches(context, "Map", type);
}
function isSetType(context, type) {
return typeMatches(context, "Set", type);
}
function isArrayConstructorType(context, type) {
return typeMatches(context, "ArrayConstructor", type);
}
function isMapConstructorType(context, type) {
return typeMatches(context, "MapConstructor", type);
}
function isSetConstructorType(context, type) {
return typeMatches(context, "SetConstructor", type);
}
function isObjectConstructorType(context, type) {
return typeMatches(context, "ObjectConstructor", type);
}
function isPromiseType(context, type) {
return typeMatches(context, "Promise", type);
}
function typeMatches(context, typeName, type) {
if (type === null) {
return false;
}
const program = context.sourceCode.parserServices?.program ?? undefined;
if (program === undefined) {
return false;
}
return typeMatchesHelper(program, typeName)(type);
}
function typeMatchesHelper(program, typeName) {
return function test(type) {
return ((type.symbol !== undefined &&
type.symbol.name === typeName &&
typeMatchesSpecifier(program, libSpecifier, type)) ||
(isUnionType(type) && type.types.some(test)));
};
}
/**
* Return the first ancestor that meets the given check criteria.
*/
function getAncestorOfType(checker, node, child = null) {
return checker(node, child) ? node : isDefined(node.parent) ? getAncestorOfType(checker, node.parent, node) : null;
}
/**
* Test if the given node is in a function's body.
*
* @param node - The node to test.
* @param async - Whether the function must be async or sync. Use `undefined` for either.
*/
function isInFunctionBody(node, async) {
const functionNode = getEnclosingFunction(node);
return functionNode !== null && (async === undefined);
}
/**
* Get the function the given node is in.
*
* Will return null if not in a function.
*/
function getEnclosingFunction(node) {
return getAncestorOfType((n, c) => isFunctionLike(n) && n.body === c, node);
}
/**
* Get the function the given node is in.
*
* Will return null if not in a function.
*/
function getEnclosingTryStatement(node) {
return getAncestorOfType((n, c) => isTryStatement(n) && n.block === c, node);
}
/**
* Test if the given node is in a class.
*/
function isInClass(node) {
return getAncestorOfType(isClassLike, node) !== null;
}
/**
* Test if the given node is in a for loop initializer.
*/
function isInForLoopInitializer(node) {
return getAncestorOfType((n, c) => isForStatement(n) && n.init === c, node) !== null;
}
/**
* Test if the given node is shallowly inside a `Readonly<{...}>`.
*/
function isInReadonly(node) {
return getReadonly(node) !== null;
}
/**
* Test if the given node is in a handler function callback of a promise.
*/
function isInPromiseHandlerFunction(node, context) {
const functionNode = getAncestorOfType((n, c) => isFunctionLike(n) && n.body === c, node);
if (functionNode === null ||
!isCallExpression(functionNode.parent) ||
!isMemberExpression(functionNode.parent.callee) ||
!isIdentifier(functionNode.parent.callee.property)) {
return false;
}
const objectType = getTypeOfNode(functionNode.parent.callee.object, context);
return isPromiseType(context, objectType);
}
/**
* Test if the given node is shallowly inside a `Readonly<{...}>`.
*/
function getReadonly(node) {
// For nested cases, we shouldn't look for any parent, but the immediate parent.
if (isDefined(node.parent) &&
isTSTypeLiteral(node.parent) &&
isDefined(node.parent.parent) &&
isTSTypeAnnotation(node.parent.parent)) {
return null;
}
const typeRef = getAncestorOfType(isTSTypeReference, node);
const intHeritage = getAncestorOfType(isTSInterfaceHeritage, node);
const expressionOrTypeName = typeRef?.typeName ?? intHeritage?.expression;
return expressionOrTypeName !== undefined &&
isIdentifier(expressionOrTypeName) &&
expressionOrTypeName.name === "Readonly"
? (typeRef ?? intHeritage)
: null;
}
/**
* Test if the given node is in a TS Property Signature.
*/
function isInInterface(node) {
return getAncestorOfType(isTSInterfaceBody, node) !== null;
}
/**
* Test if the given node is in a Constructor.
*/
function isInConstructor(node) {
const methodDefinition = getAncestorOfType(isMethodDefinition, node);
return methodDefinition !== null && isIdentifier(methodDefinition.key) && methodDefinition.key.name === "constructor";
}
/**
* Is the given node in the return type.
*/
function isInReturnType(node) {
return (getAncestorOfType((n) => isDefined(n.parent) && isFunctionLike(n.parent) && n.parent.returnType === n, node) !== null);
}
/**
* Test if the given node is nested inside another statement.
*/
function isNested(node) {
return node.parent !== undefined && !(isProgram(node.parent) || isBlockStatement(node.parent));
}
/**
* Is the given identifier a property of an object?
*/
function isPropertyAccess(node) {
return node.parent !== undefined && isMemberExpression(node.parent) && node.parent.property === node;
}
/**
* Is the given identifier a property name?
*/
function isPropertyName(node) {
return node.parent !== undefined && isProperty(node.parent) && node.parent.key === node;
}
/**
* Is the given function an IIFE?
*/
function isIIFE(node) {
return (isFunctionExpressionLike(node) &&
node.parent !== undefined &&
isCallExpression(node.parent) &&
node.parent.callee === node);
}
/**
* Is the given node being passed as an argument?
*/
function isArgument(node) {
return node.parent !== undefined && isCallExpression(node.parent) && node.parent.arguments.includes(node);
}
/**
* Is the given node a parameter?
*/
function isParameter(node) {
return node.parent !== undefined && isFunctionLike(node.parent) && node.parent.params.includes(node);
}
/**
* Is the given node a getter function?
*/
function isGetter(node) {
return node.parent !== undefined && isProperty(node.parent) && node.parent.kind === "get";
}
/**
* Is the given node a setter function?
*/
function isSetter(node) {
return node.parent !== undefined && isProperty(node.parent) && node.parent.kind === "set";
}
/**
* Get the key the given node is assigned to in its parent ObjectExpression.
*/
function getKeyOfValueInObjectExpression(node) {
if (!isDefined(node.parent)) {
return null;
}
const objectExpression = getAncestorOfType(isObjectExpression, node);
if (objectExpression === null) {
return null;
}
const objectExpressionProps = objectExpression.properties.filter((prop) => isProperty(prop) && prop.value === node);
if (objectExpressionProps.length !== 1) {
return null;
}
const objectExpressionProp = objectExpressionProps[0];
if (!isProperty(objectExpressionProp) || !isIdentifier(objectExpressionProp.key)) {
return null;
}
return objectExpressionProp.key.name;
}
/**
* Is the given identifier defined by a mutable variable (let or var)?
*/
function isDefinedByMutableVariable(node, context, treatParametersAsMutable) {
const services = getParserServices(context);
const symbol = services.getSymbolAtLocation(node);
const variableDeclaration = symbol?.valueDeclaration;
if (variableDeclaration === undefined) {
return true;
}
const variableDeclarationNode = services.tsNodeToESTreeNodeMap.get(variableDeclaration);
if (variableDeclarationNode !== undefined && isParameter(variableDeclarationNode)) {
return treatParametersAsMutable(variableDeclarationNode);
}
if (!typescript.isVariableDeclaration(variableDeclaration)) {
return true;
}
const variableDeclarator = services.tsNodeToESTreeNodeMap.get(variableDeclaration);
if (variableDeclarator?.parent === undefined || !isVariableDeclaration(variableDeclarator.parent)) {
return true;
}
return variableDeclarator.parent.kind !== "const";
}
/**
* Get the root identifier of an expression.
*/
function findRootIdentifier(node) {
if (isIdentifier(node)) {
return node;
}
if (isMemberExpression(node)) {
return findRootIdentifier(node.object);
}
return undefined;
}
const ruleNameScope = "functional";
/**
* Does the given ExpressionStatement specify directive prologues.
*/
function isDirectivePrologue(node) {
return (node.expression.type === AST_NODE_TYPES.Literal &&
typeof node.expression.value === "string" &&
node.expression.value.startsWith("use "));
}
/**
* Get the identifier text of the given node.
*/
function getNodeIdentifierText(node, context) {
if (!isDefined(node)) {
return undefined;
}
let mut_identifierText = null;
if (isIdentifier(node)) {
mut_identifierText = node.name;
}
else if (isPrivateIdentifier(node)) {
mut_identifierText = `#${node.name}`;
}
else if (hasID(node) && isDefined(node.id)) {
mut_identifierText = getNodeIdentifierText(node.id, context);
}
else if (hasKey(node) && isDefined(node.key)) {
mut_identifierText = getNodeIdentifierText(node.key, context);
}
else if (isAssignmentExpression(node)) {
mut_identifierText = getNodeIdentifierText(node.left, context);
}
else if (isMemberExpression(node)) {
mut_identifierText = `${getNodeIdentifierText(node.object, context)}.${getNodeIdentifierText(node.property, context)}`;
}
else if (isThisExpression(node)) {
mut_identifierText = "this";
}
else if (isUnaryExpression(node)) {
mut_identifierText = getNodeIdentifierText(node.argument, context);
}
else if (isTSTypeAnnotation(node)) {
mut_identifierText = context.sourceCode.getText(node.typeAnnotation).replaceAll(/\s+/gu, "");
}
else if (isTSAsExpression(node) || isTSNonNullExpression(node) || isChainExpression(node)) {
mut_identifierText = getNodeIdentifierText(node.expression, context);
}
if (mut_identifierText !== null) {
return mut_identifierText;
}
const keyInObjectExpression = getKeyOfValueInObjectExpression(node);
if (keyInObjectExpression !== null) {
return keyInObjectExpression;
}
return undefined;
}
/**
* Get the code of the given node.
*/
function getNodeCode(node, context) {
return context.sourceCode.getText(node);
}
/**
* Get all the identifier texts of the given node.
*/
function getNodeIdentifierTexts(node, context) {
return (isVariableDeclaration(node)
? node.declarations.flatMap((declarator) => getNodeIdentifierText(declarator, context))
: [getNodeIdentifierText(node, context)]).filter((text) => text !== undefined);
}
/**
* The schema for the option to ignore patterns.
*/
const ignoreIdentifierPatternOptionSchema = {
ignoreIdentifierPattern: {
type: ["string", "array"],
items: {
type: "string",
},
},
};
/**
* The schema for the option to ignore patterns.
*/
const ignoreCodePatternOptionSchema = {
ignoreCodePattern: {
type: ["string", "array"],
items: {
type: "string",
},
},
};
/**
* The schema for the option to ignore accessor patterns.
*/
const ignoreAccessorPatternOptionSchema = {
ignoreAccessorPattern: {
type: ["string", "array"],
items: {
type: "string",
},
},
};
/**
* The schema for the option to ignore classes.
*/
const ignoreClassesOptionSchema = {
ignoreClasses: {
oneOf: [
{
type: "boolean",
},
{
type: "string",
enum: ["fieldsOnly"],
},
],
},
};
/**
* The schema for the option to ignore maps and sets.
*/
const ignoreMapsAndSetsOptionSchema = {
ignoreMapsAndSets: {
type: "boolean",
},
};
/**
* The schema for the option to ignore prefix selector.
*/
const ignorePrefixSelectorOptionSchema = {
ignorePrefixSelector: {
type: ["string", "array"],
items: {
type: "string",
},
},
};
/**
* Should the given text be allowed?
*
* Test using the given pattern(s).
*/
function shouldIgnoreViaPattern(text, pattern) {
const patterns = Array.isArray(pattern) ? pattern : [pattern];
// One or more patterns match?
return patterns.some((p) => new RegExp(p, "u").test(text));
}
/**
* Recursive callback of `shouldIgnoreViaAccessorPattern`.
*
* This function not be called from anywhere else.
*
* Does the given text match the given pattern.
*/
function accessorPatternMatch([pattern, ...remainingPatternParts], textParts, allowExtra = false) {
return pattern === undefined
? allowExtra || textParts.length === 0
: // Match any depth (including 0)?
pattern === "**"
? textParts.length === 0
? accessorPatternMatch(remainingPatternParts, [], allowExtra)
: Array.from({ length: textParts.length })
.map((element, index) => index)
.some((offset) => accessorPatternMatch(remainingPatternParts, textParts.slice(offset), true))
: // Match anything?
pattern === "*"
? textParts.length > 0 && accessorPatternMatch(remainingPatternParts, textParts.slice(1), allowExtra)
: // Text matches pattern?
new RegExp(`^${escapeRegExp(pattern).replaceAll("\\*", ".*")}$`, "u").test(textParts[0]) &&
accessorPatternMatch(remainingPatternParts, textParts.slice(1), allowExtra);
}
/**
* Should the given text be allowed?
*
* Test using the given accessor pattern(s).
*/
function shouldIgnoreViaAccessorPattern(text, pattern) {
const patterns = Array.isArray(pattern) ? pattern : [pattern];
// One or more patterns match?
return patterns.some((p) => accessorPatternMatch(p.split("."), text.split(".")));
}
/**
* Should the given node be allowed base off the following rule options?
*
* - AllowInFunctionOption.
*/
function shouldIgnoreInFunction(node, context, allowInFunction) {
return allowInFunction === true && isInFunctionBody(node);
}
/**
* Should the given node be allowed base off the following rule options?
*
* - IgnoreClassesOption.
*/
function shouldIgnoreClasses(node, context, ignoreClasses) {
return ((ignoreClasses === true && (isClassLike(node) || isInClass(node))) ||
(ignoreClasses === "fieldsOnly" &&
(isPropertyDefinition(node) ||
(isAssignmentExpression(node) &&
isInClass(node) &&
isMemberExpression(node.left) &&
isThisExpression(node.left.object)))));
}
/**
* Should the given node be allowed base off the following rule options?
*
* - IgnoreAccessorPatternOption.
* - IgnoreIdentifierPatternOption.
*/
function shouldIgnorePattern(node, context, ignoreIdentifierPattern, ignoreAccessorPattern, ignoreCodePattern) {
const texts = getNodeIdentifierTexts(node, context);
if (texts.length === 0) {
return ignoreCodePattern !== undefined && shouldIgnoreViaPattern(getNodeCode(node, context), ignoreCodePattern);
}
return (
// Ignore if ignoreIdentifierPattern is set and a pattern matches.
(ignoreIdentifierPattern !== undefined &&
texts.every((text) => shouldIgnoreViaPattern(text, ignoreIdentifierPattern))) ||
// Ignore if ignoreAccessorPattern is set and an accessor pattern matches.
(ignoreAccessorPattern !== undefined &&
texts.every((text) => shouldIgnoreViaAccessorPattern(text, ignoreAccessorPattern))) ||
// Ignore if ignoreCodePattern is set and a code pattern matches.
(ignoreCodePattern !== undefined && shouldIgnoreViaPattern(getNodeCode(node, context), ignoreCodePattern)));
}
function upgradeRawOverridableOptions(raw) {
return {
...raw,
overrides: raw.overrides?.map((override) => ({
...override,
specifiers: override.specifiers === undefined
? []
: Array.isArray(override.specifiers)
? override.specifiers.map(upgradeRawTypeSpecifier)
: [upgradeRawTypeSpecifier(override.specifiers)],
})) ?? [],
};
}
function upgradeRawTypeSpecifier(raw) {
const { ignoreName, ignorePattern, name, pattern, ...rest } = raw;
const names = name === undefined ? [] : Array.isArray(name) ? name : [name];
const patterns = (pattern === undefined ? [] : Array.isArray(pattern) ? pattern : [pattern]).map((p) => new RegExp(p, "u"));
const ignoreNames = ignoreName === undefined ? [] : Array.isArray(ignoreName) ? ignoreName : [ignoreName];
const ignorePatterns = (ignorePattern === undefined ? [] : Array.isArray(ignorePattern) ? ignorePattern : [ignorePattern]).map((p) => new RegExp(p, "u"));
const include = [...names, ...patterns];
const exclude = [...ignoreNames, ...ignorePatterns];
return {
...rest,
include,
exclude,
};
}
/**
* Get the core options to use, taking into account overrides.
*/
function getCoreOptions(node, context, options) {
const program = context.sourceCode.parserServices?.program ?? undefined;
if (program === undefined) {
return options;
}
const [type, typeNode] = getTypeDataOfNode(node, context);
return getCoreOptionsForType(type, typeNode, context, options);
}
function getCoreOptionsForType(type, typeNode, context, options) {
const program = context.sourceCode.parserServices?.program ?? undefined;
if (program === undefined) {
return options;
}
const found = options.overrides?.find((override) => (Array.isArray(override.specifiers) ? override.specifiers : [override.specifiers]).some((specifier) => typeMatchesSpecifierDeep(program, specifier, type) &&
(specifier.include === undefined ||
specifier.include.length === 0 ||
typeMatchesPattern(program, type, typeNode, specifier.include, specifier.exclude))));
if (found !== undefined) {
if (found.disable === true) {
return null;
}
if (found.inherit !== false) {
return deepmerge(options, found.options);
}
return found.options;
}
return options;
}
function typeMatchesSpecifierDeep(program, specifier, type) {
const mut_stack = [type];
// eslint-disable-next-line functional/no-loop-statements -- best to do this iteratively.
while (mut_stack.length > 0) {
const t = mut_stack.pop();
if (typeMatchesSpecifier(program, specifier, t)) {
return true;
}
if (t.aliasTypeArguments !== undefined) {
mut_stack.push(...t.aliasTypeArguments);
}
}
return false;
}
const typeSpecifierPatternSchemaProperties = {
name: schemaInstanceOrInstanceArray({
type: "string",
}),
pattern: schemaInstanceOrInstanceArray({
type: "string",
}),
ignoreName: schemaInstanceOrInstanceArray({
type: "string",
}),
ignorePattern: schemaInstanceOrInstanceArray({
type: "string",
}),
};
const typeSpecifierSchema = {
oneOf: [
{
type: "object",
properties: {
...typeSpecifierPatternSchemaProperties,
from: {
type: "string",
enum: ["file"],
},
path: {
type: "string",
},
},
additionalProperties: false,
},
{
type: "object",
properties: {
...typeSpecifierPatternSchemaProperties,
from: {
type: "string",
enum: ["lib"],
},
},
additionalProperties: false,
},
{
type: "object",
properties: {
...typeSpecifierPatternSchemaProperties,
from: {
type: "string",
enum: ["package"],
},
package: {
type: "string",
},
},
additionalProperties: false,
},
],
};
function schemaInstanceOrInstanceArray(items) {
return {
oneOf: [
items,
{
type: "array",
items,
},
],
};
}
function overridableOptionsSchema(coreOptionsPropertiesSchema) {
return {
type: "object",
properties: deepmerge(coreOptionsPropertiesSchema, {
overrides: {
type: "array",
items: {
type: "object",
properties: {
specifiers: schemaInstanceOrInstanceArray(typeSpecifierSchema),
options: {
type: "object",
properties: coreOptionsPropertiesSchema,
additionalProperties: false,
},
inherit: {
type: "boolean",
},
disable: {
type: "boolean",
},
},
additionalProperties: false,
},
},
}),
additionalProperties: false,
};
}
/**
* The name of this rule.
*/
const name$j = "functional-parameters";
/**
* The full name of this rule.
*/
const fullName$a = `${ruleNameScope}/${name$j}`;
const coreOptionsPropertiesSchema$2 = deepmerge(ignoreIdentifierPatternOptionSchema, ignorePrefixSelectorOptionSchema, {
allowRestParameter: {
type: "boolean",
},
allowArgumentsKeyword: {
type: "boolean",
},
enforceParameterCount: {
oneOf: [
{
type: "boolean",
enum: [false],
},
{
type: "string",
enum: ["atLeastOne", "exactlyOne"],
},
{
type: "object",
properties: {
count: {
type: "string",
enum: ["atLeastOne", "exactlyOne"],
},
ignoreGettersAndSetters: {
type: "boolean",
},
ignoreLambdaExpression: {
type: "boolean",
},
ignoreIIFE: {
type: "boolean",
},
},
additionalProperties: false,
},
],
},
});
/**
* The schema for the rule options.
*/
const schema$j = [overridableOptionsSchema(coreOptionsPropertiesSchema$2)];
/**
* The default options for the rule.
*/
const defaultOptions$j = [
{
allowRestParameter: false,
allowArgumentsKeyword: false,
enforceParameterCount: {
count: "atLeastOne",
ignoreLambdaExpression: false,
ignoreIIFE: true,
ignoreGettersAndSetters: true,
},
},
];
/**
* The possible error messages.
*/
const errorMessages$j = {
restParam: "Unexpected rest parameter. Use a regular parameter of type array instead.",
arguments: "Unexpected use of `arguments`. Use regular function arguments instead.",
paramCountAtLeastOne: "Functions must have at least one parameter.",
paramCountExactlyOne: "Functions must have exactly one parameter.",
};
/**
* The meta data for this rule.
*/
const meta$k = {
type: "suggestion",
docs: {
category: "Currying",
description: "Enforce functional parameters.",
recommended: "recommended",
recommendedSeverity: "error",
requiresTypeChecking: true,
},
messages: errorMessages$j,
schema: schema$j,
};
/**
* Get the rest parameter violations.
*/
function getRestParamViolations({ allowRestParameter }, node) {
return !allowRestParameter && node.params.length > 0 && isRestElement(node.params.at(-1))
? [
{
node: node.params.at(-1),
messageId: "restParam",
},
]
: [];
}
/**
* Get the parameter count violations.
*/
function getParamCountViolations({ enforceParameterCount }, node) {
if (enforceParameterCount === false ||
(node.params.length === 0 &&
typeof enforceParameterCount === "object" &&
((enforceParameterCount.ignoreIIFE && isIIFE(node)) ||
(enforceParameterCount.ignoreLambdaExpression && isArgument(node)) ||
(enforceParameterCount.ignoreGettersAndSetters && (isGetter(node) || isSetter(node)))))) {
return [];
}
if (node.params.length === 0 &&
(enforceParameterCount === "atLeastOne" ||
(typeof enforceParameterCount === "object" && enforceParameterCount.count === "atLeastOne"))) {
return [
{
node,
messageId: "paramCountAtLeastOne",
},
];
}
if (node.params.length !== 1 &&
(enforceParameterCount === "exactlyOne" ||
(typeof enforceParameterCount === "object" && enforceParameterCount.count === "exactlyOne"))) {
return [
{
node,
messageId: "paramCountExactlyOne",
},
];
}
return [];
}
/**
* Add the default options to the given options.
*/
function getOptionsWithDefaults$2(options) {
if (options === null) {
return null;
}
const topLevel = {
...defaultOptions$j[0],
...options,
};
return typeof topLevel.enforceParameterCount === "object"
? {
...topLevel,
enforceParameterCount: {
...defaultOptions$j[0].enforceParameterCount,
...topLevel.enforceParameterCount,
},
}
: topLevel;
}
/**
* Check if the given function node has a reset parameter this rule.
*/
function checkFunction$3(node, context, rawOptions) {
const options = upgradeRawOverridableOptions(rawOptions[0]);
const optionsToUse = getOptionsWithDefaults$2(getCoreOptions(node, context, options));
if (optionsToUse === null) {
return {
context,
descriptors: [],
};
}
const { ignoreIdentifierPattern } = optionsToUse;
if (shouldIgnorePattern(node, context, ignoreIdentifierPattern)) {
return {
context,
descriptors: [],
};
}
return {
context,
descriptors: [...getRestParamViolations(optionsToUse, node), ...getParamCountViolations(optionsToUse, node)],
};
}
/**
* Check if the given identifier is for the "arguments" keyword.
*/
function checkIdentifier(node, context, rawOptions) {
if (node.name !== "arguments") {
return {
context,
descriptors: [],
};
}
const functionNode = getEnclosingFunction(node);
const options = upgradeRawOverridableOptions(rawOptions[0]);
const optionsToUse = getOptionsWithDefaults$2(functionNode === null ? options : getCoreOptions(functionNode, context, options));
if (optionsToUse === null) {
return {
context,
descriptors: [],
};
}
const { ignoreIdentifierPattern } = optionsToUse;
if (shouldIgnorePattern(node, context, ignoreIdentifierPattern)) {
return {
context,
descriptors: [],
};
}
const { allowArgumentsKeyword } = optionsToUse;
return {
context,
descriptors: !allowArgumentsKeyword && !isPropertyName(node) && !isPropertyAccess(node)
? [
{
node,
messageId: "arguments",
},
]
: [],
};
}
// Create the rule.
const rule$j = createRuleUsingFunction(name$j, meta$k, defaultOptions$j, (context, options) => {
const [optionsObject] = options;
const { ignorePrefixSelector } = optionsObject;
const baseFunctionSelectors = ["ArrowFunctionExpression", "FunctionDeclaration", "FunctionExpression"];
const ignoreSelectors = ignorePrefixSelector === undefined
? undefined
: Array.isArray(ignorePrefixSelector)
? ignorePrefixSelector
: [ignorePrefixSelector];
const fullFunctionSelectors = baseFunctionSelectors.flatMap((baseSelector) => ignoreSelectors === undefined ? [baseSelector] : `:not(:matches(${ignoreSelectors.join(",")})) > ${baseSelector}`);
return {
...Object.fromEntries(fullFunctionSelectors.map((selector) => [selector, checkFunction$3])),
Identifier: checkIdentifier,
};
});
/**
* The name of this rule.
*/
const name$i = "immutable-data";
/**
* The full name of this rule.
*/
const fullName$9 = `${ruleNameScope}/${name$i}`;
const coreOptionsPropertiesSchema$1 = deepmerge(ignoreIdentifierPatternOptionSchema, ignoreAccessorPatternOptionSchema, ignoreClassesOptionSchema, ignoreMapsAndSetsOptionSchema, {
ignoreImmediateMutation: {
type: "boolean",
},
ignoreNonConstDeclarations: {
oneOf: [
{
type: "boolean",
},
{
type: "object",
properties: {
treatParametersAsConst: {
type: "boolean",
},
},
additionalProperties: false,
},
],
},
});
/**
* The schema for the rule options.
*/
const schema$i = [overridableOptionsSchema(coreOptionsPropertiesSchema$1)];
/**
* The default options for the rule.
*/
const defaultOptions$i = [
{
ignoreClasses: false,
ignoreMapsAndSets: false,
ignoreImmediateMutation: true,
ignoreNonConstDeclarations: false,
},
];
/**
* The possible error messages.
*/
const errorMessages$i = {
generic: "Modifying an existing object/array is not allowed.",
object: "Modifying properties of existing object not allowed.",
array: "Modifying an array is not allowed.",
map: "Modifying a map is not allowed.",
set: "Modifying a set is not allowed.",
};
/**
* The meta data for this rule.
*/
const meta$j = {
type: "suggestion",
docs: {
category: "No Mutations",
description: "Enforce treating data as immutable.",
recommended: "recommended",
recommendedSeverity: "error",
requiresTypeChecking: true,
},
messages: errorMessages$i,
schema: schema$i,
};
/**
* Array methods that mutate an array.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype#Methods#Mutator_methods
*/
const arrayMutatorMethods = new Set([
"copyWithin",
"fill",
"pop",
"push",
"reverse",
"shift",
"sort",
"splice",
"unshift",
]);
/**
* Array methods that return a new object (or array) without mutating the original.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype#Methods#Accessor_methods
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype#Iteration_methods
*/
const arrayNewObjectReturningMethods = new Set(["concat", "slice", "filter", "map", "reduce", "reduceRight"]);
/**
* Array constructor functions that create a new array.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#Methods
*/
const arrayConstructorFunctions = new Set(["from", "of"]);
/**
* Map methods that mutate an map.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
*/
const mapMutatorMethods = new Set(["clear"