eslint-plugin-react-snob
Version:
An ESLint plugin for React best practices
287 lines (286 loc) • 13.8 kB
JavaScript
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',
});
;