UNPKG

eslint-plugin-functional

Version:
1,490 lines (1,477 loc) 163 kB
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"