UNPKG

eslint-plugin-react-snob

Version:
287 lines (286 loc) 13.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.requireBooleanPrefixIs = void 0; const utils_1 = require("../utils"); const DEFAULT_OPTIONS = { allowedPrefixes: ['is'], }; exports.requireBooleanPrefixIs = (0, utils_1.createRule)({ create(context, [options = DEFAULT_OPTIONS]) { const { allowedPrefixes } = options; function formatPrefixes(prefixes) { if (prefixes.length === 1) { return `"${prefixes[0]}"`; } if (prefixes.length === 2) { return `"${prefixes[0]}", or "${prefixes[1]}"`; } const lastPrefix = prefixes[prefixes.length - 1]; const otherPrefixes = prefixes.slice(0, -1); return `${otherPrefixes.map((p) => `"${p}"`).join(', ')}, or "${lastPrefix}"`; } function reportBooleanPrefixError(node, name) { const suggested = (0, utils_1.suggestPrefixedName)(name, allowedPrefixes); const prefixes = formatPrefixes(allowedPrefixes); context.report({ data: { name, prefixes, suggested, }, messageId: 'booleanShouldStartWithPrefix', node, }); } function checkBooleanVariable(node, name) { if ((0, utils_1.hasAnyValidPrefix)(name, allowedPrefixes)) return; // Check if it's in a context we should ignore if ((0, utils_1.isInZodOmitOrPickMethod)(node) || (0, utils_1.isInConstructorCall)(node)) return; // Check for boolean literal values if (node.type === 'VariableDeclarator' && node.init && (0, utils_1.isBooleanLiteral)(node.init)) { reportBooleanPrefixError(node, name); return; } if (node.type === 'AssignmentPattern' && (0, utils_1.isBooleanLiteral)(node.right)) { reportBooleanPrefixError(node, name); return; } // Check for boolean type annotation if (node.type === 'VariableDeclarator' && node.id.type === 'Identifier') { if (node.id.typeAnnotation && (0, utils_1.isBooleanType)(node.id.typeAnnotation)) { reportBooleanPrefixError(node, name); return; } } // Check for boolean expressions (not nullish coalescing with non-boolean) if (node.type === 'VariableDeclarator' && node.init && (0, utils_1.isLikelyBooleanExpression)(node.init)) { // Skip nullish coalescing unless both operands are boolean if (node.init.type === 'LogicalExpression' && node.init.operator === '??') { if ((0, utils_1.isBooleanLiteral)(node.init.left) || (0, utils_1.isBooleanLiteral)(node.init.right)) { reportBooleanPrefixError(node, name); } } else { reportBooleanPrefixError(node, name); } } if (node.type === 'AssignmentPattern' && (0, utils_1.isLikelyBooleanExpression)(node.right)) { // Skip nullish coalescing unless both operands are boolean if (node.right.type === 'LogicalExpression' && node.right.operator === '??') { if ((0, utils_1.isBooleanLiteral)(node.right.left) || (0, utils_1.isBooleanLiteral)(node.right.right)) { reportBooleanPrefixError(node, name); } } else { reportBooleanPrefixError(node, name); } } } function checkArrayPattern(node) { // Check useState destructuring const parent = node.parent; if (parent?.type === 'VariableDeclarator' && parent.init?.type === 'CallExpression' && (0, utils_1.isUseStateWithBoolean)(parent.init) && node.elements.length > 0 && node.elements[0]?.type === 'Identifier') { const stateVarName = node.elements[0].name; if (!(0, utils_1.hasAnyValidPrefix)(stateVarName, allowedPrefixes)) { reportBooleanPrefixError(node.elements[0], stateVarName); } } } function checkObjectProperty(node) { if (node.key.type === 'Identifier' && !node.computed && node.value && !(0, utils_1.hasAnyValidPrefix)(node.key.name, allowedPrefixes)) { // Skip if in Zod omit/pick or constructor calls if ((0, utils_1.isInZodOmitOrPickMethod)(node) || (0, utils_1.isInConstructorCall)(node)) return; // Skip if this is a function call argument let current = node.parent; while (current) { if (current.type === 'CallExpression') { return; // Skip function call arguments } if (current.type === 'VariableDeclarator' || current.type === 'AssignmentExpression') { break; // This is a variable assignment, proceed with checks } current = current.parent || undefined; } // Type guard to check if value is an Expression const isExpression = (val) => { return (val !== null && val !== undefined && // @ts-expect-error ignore val.type !== 'AssignmentPattern' && // @ts-expect-error ignore val.type !== 'TSEmptyBodyFunctionExpression'); }; // Check for boolean literal values if (isExpression(node.value) && (0, utils_1.isBooleanLiteral)(node.value)) { reportBooleanPrefixError(node.key, node.key.name); return; } // Check for boolean expressions if (isExpression(node.value) && (0, utils_1.isLikelyBooleanExpression)(node.value)) { reportBooleanPrefixError(node.key, node.key.name); } } } function checkParameter(node) { if (node.type === 'Identifier' && !(0, utils_1.hasAnyValidPrefix)(node.name, allowedPrefixes)) { // Only check parameters in React components or hooks if (!(0, utils_1.isComponentOrHookParameter)(node)) return; // Check type annotation if (node.typeAnnotation && (0, utils_1.isBooleanType)(node.typeAnnotation)) { reportBooleanPrefixError(node, node.name); } } else if (node.type === 'AssignmentPattern' && node.left.type === 'Identifier') { const name = node.left.name; if (!(0, utils_1.hasAnyValidPrefix)(name, allowedPrefixes)) { // Only check parameters in React components or hooks if (!(0, utils_1.isComponentOrHookParameter)(node)) return; // Check for boolean default value if ((0, utils_1.isBooleanLiteral)(node.right)) { reportBooleanPrefixError(node.left, name); return; } // Check type annotation if (node.left.typeAnnotation && (0, utils_1.isBooleanType)(node.left.typeAnnotation)) { reportBooleanPrefixError(node.left, name); } } } } function checkObjectPatternProperty(node) { if (node.type === 'Property' && node.key.type === 'Identifier' && node.value.type === 'Identifier' && !(0, utils_1.hasAnyValidPrefix)(node.value.name, allowedPrefixes)) { // Check if this is a parameter destructuring in a component/hook if (!(0, utils_1.isComponentOrHookParameter)(node)) return; // The property name in object destructuring is the same as the variable name // So we need to check if the key (not value) has a boolean type annotation // But in parameter destructuring, we need to check the original interface/type // For now, let's check if the destructured property has boolean type annotation if (node.value.typeAnnotation && (0, utils_1.isBooleanType)(node.value.typeAnnotation)) { reportBooleanPrefixError(node.value, node.value.name); } } } function checkInterfaceProperty(node) { if (node.key.type === 'Identifier' && !(0, utils_1.hasAnyValidPrefix)(node.key.name, allowedPrefixes) && node.typeAnnotation && (0, utils_1.isBooleanType)(node.typeAnnotation)) { reportBooleanPrefixError(node.key, node.key.name); } } function checkClassProperty(node) { if (node.key.type === 'Identifier' && !(0, utils_1.hasAnyValidPrefix)(node.key.name, allowedPrefixes)) { // Check for boolean value if (node.value && (0, utils_1.isBooleanLiteral)(node.value)) { reportBooleanPrefixError(node.key, node.key.name); return; } // Check for boolean type annotation if (node.typeAnnotation && (0, utils_1.isBooleanType)(node.typeAnnotation)) { reportBooleanPrefixError(node.key, node.key.name); } } } function checkParameterPattern(node) { // Check if this is a React component or hook parameter if (!(0, utils_1.isComponentOrHookParameter)(node)) return; // Get the type annotation from the parameter const typeAnnotation = node.typeAnnotation?.typeAnnotation; if (!typeAnnotation || typeAnnotation.type !== 'TSTypeLiteral') return; // Create a map of property types const propertyTypes = new Map(); typeAnnotation.members.forEach((member) => { if (member.type === 'TSPropertySignature' && member.key.type === 'Identifier' && member.typeAnnotation && (0, utils_1.isBooleanType)(member.typeAnnotation)) { propertyTypes.set(member.key.name, true); } }); // Check each destructured property node.properties.forEach((prop) => { if (prop.type === 'Property' && prop.key.type === 'Identifier' && prop.value.type === 'Identifier' && !(0, utils_1.hasAnyValidPrefix)(prop.value.name, allowedPrefixes) && propertyTypes.has(prop.key.name)) { reportBooleanPrefixError(prop.value, prop.value.name); } }); } return { ArrayPattern: checkArrayPattern, AssignmentPattern(node) { if (node.left.type === 'Identifier') { checkBooleanVariable(node, node.left.name); } }, 'FunctionDeclaration > :first-child[type="Identifier"]'(node) { checkParameter(node); }, 'MethodDefinition[value.params] > FunctionExpression > :first-child[type="Identifier"]'(node) { checkParameter(node); }, ObjectPattern(node) { // Check if this is a parameter pattern first checkParameterPattern(node); // Then check for variable destructuring patterns if (node.parent?.type === 'VariableDeclarator') { node.properties.forEach((prop) => { checkObjectPatternProperty(prop); }); } }, 'Property[kind="init"]': checkObjectProperty, PropertyDefinition: checkClassProperty, 'TSInterfaceDeclaration TSPropertySignature': checkInterfaceProperty, 'TSTypeAliasDeclaration TSPropertySignature': checkInterfaceProperty, VariableDeclarator(node) { if (node.id.type === 'Identifier') { checkBooleanVariable(node, node.id.name); } }, }; }, defaultOptions: [DEFAULT_OPTIONS], meta: { docs: { description: 'Enforce boolean variables, state, and props to start with "is" prefix (or custom prefixes) in developer-controlled contexts', }, messages: { booleanShouldStartWithPrefix: 'Boolean identifier "{{name}}" should start with {{prefixes}} prefix. Consider renaming to "{{suggested}}".', }, schema: [ { additionalProperties: false, properties: { allowedPrefixes: { items: { type: 'string' }, type: 'array', }, }, type: 'object', }, ], type: 'suggestion', }, name: 'require-boolean-prefix-is', });