eslint-plugin-fsecond
Version:
ESLint plugin with misc rules. Typescript and React related.
665 lines (657 loc) • 27.8 kB
JavaScript
import { ASTUtils, AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
import * as ts from "typescript";
//#region package.json
var name = "eslint-plugin-fsecond";
var version = "0.0.0-development";
//#endregion
//#region src/utils.ts
const createEslintRule = ESLintUtils.RuleCreator((ruleName) => ruleName);
//#endregion
//#region src/rules/no-inline-interfaces.ts
const RULE_NAME$3 = "no-inline-interfaces";
const isStatementParent = ASTUtils.isNodeOfTypes([
AST_NODE_TYPES.Program,
AST_NODE_TYPES.BlockStatement,
AST_NODE_TYPES.TSModuleBlock
]);
const findStatementAncestor = (node) => {
if (!node.parent) return null;
if (isStatementParent(node.parent)) return node;
return findStatementAncestor(node.parent);
};
/**
* Recursively find all TSTypeLiteral nodes in a type annotation.
* Returns all inline object type literals that should be reported.
*/
const findTypeLiterals = (type, results = [], checkGenericTypes = false) => {
if (!type) return results;
switch (type.type) {
case AST_NODE_TYPES.TSTypeLiteral:
results.push(type);
type.members.forEach((member) => {
if (member.type === AST_NODE_TYPES.TSPropertySignature && member.typeAnnotation) findTypeLiterals(member.typeAnnotation.typeAnnotation, results, checkGenericTypes);
});
return results;
case AST_NODE_TYPES.TSUnionType:
case AST_NODE_TYPES.TSIntersectionType:
for (const typeNode of type.types) findTypeLiterals(typeNode, results, checkGenericTypes);
break;
case AST_NODE_TYPES.TSTypeReference:
if (checkGenericTypes && type.typeArguments) type.typeArguments.params.forEach((param) => {
findTypeLiterals(param, results, checkGenericTypes);
});
break;
case AST_NODE_TYPES.TSArrayType:
findTypeLiterals(type.elementType, results, checkGenericTypes);
break;
case AST_NODE_TYPES.TSTupleType:
type.elementTypes.forEach((elementType) => {
findTypeLiterals(elementType, results, checkGenericTypes);
});
break;
case AST_NODE_TYPES.TSOptionalType:
findTypeLiterals(type.typeAnnotation, results, checkGenericTypes);
break;
case AST_NODE_TYPES.TSRestType:
findTypeLiterals(type.typeAnnotation, results, checkGenericTypes);
break;
case AST_NODE_TYPES.TSTypeOperator:
findTypeLiterals(type.typeAnnotation, results, checkGenericTypes);
break;
case AST_NODE_TYPES.TSIndexedAccessType:
findTypeLiterals(type.objectType, results, checkGenericTypes);
findTypeLiterals(type.indexType, results, checkGenericTypes);
break;
case AST_NODE_TYPES.TSConditionalType:
findTypeLiterals(type.trueType, results, checkGenericTypes);
findTypeLiterals(type.falseType, results, checkGenericTypes);
findTypeLiterals(type.checkType, results, checkGenericTypes);
findTypeLiterals(type.extendsType, results, checkGenericTypes);
break;
case AST_NODE_TYPES.TSConstructorType:
case AST_NODE_TYPES.TSFunctionType:
type.params.forEach((param) => {
if (param.type === AST_NODE_TYPES.TSParameterProperty) findTypeLiterals(param.parameter.typeAnnotation?.typeAnnotation, results, checkGenericTypes);
else if (param.typeAnnotation) findTypeLiterals(param.typeAnnotation.typeAnnotation, results, checkGenericTypes);
});
findTypeLiterals(type.returnType?.typeAnnotation, results, checkGenericTypes);
break;
case AST_NODE_TYPES.TSImportType:
if (type.typeArguments) type.typeArguments.params.forEach((param) => {
findTypeLiterals(param, results, checkGenericTypes);
});
break;
case AST_NODE_TYPES.TSMappedType:
findTypeLiterals(type.typeAnnotation, results, checkGenericTypes);
if (type.nameType) findTypeLiterals(type.nameType, results, checkGenericTypes);
break;
case AST_NODE_TYPES.TSNamedTupleMember:
findTypeLiterals(type.elementType, results, checkGenericTypes);
break;
case AST_NODE_TYPES.TSTypePredicate:
if (type.typeAnnotation) findTypeLiterals(type.typeAnnotation.typeAnnotation, results, checkGenericTypes);
break;
default: break;
}
return results;
};
const isClassNode = ASTUtils.isNodeOfTypes([AST_NODE_TYPES.ClassDeclaration, AST_NODE_TYPES.ClassExpression]);
/**
* Check if a node is inside a class.
*/
const isInsideClass = (node) => {
if (!node.parent) return false;
if (isClassNode(node.parent)) return true;
return isInsideClass(node.parent);
};
var no_inline_interfaces_default = createEslintRule({
name: RULE_NAME$3,
meta: {
type: "suggestion",
docs: {
description: "disallow inline object type literals in variable and function annotations; extract to a named interface or type alias.",
url: "https://github.com/AndreaPontrandolfo/eslint-plugin-fsecond/blob/master/docs/rules/no-inline-interfaces.md",
recommended: true
},
fixable: "code",
schema: [{
type: "object",
properties: {
checkGenericTypes: {
type: "boolean",
description: "Check inline object types within generic type arguments (e.g., Array<{ a: string }>)"
},
checkReturnTypes: {
type: "boolean",
description: "Check inline object types in function return type annotations"
}
},
additionalProperties: false
}],
defaultOptions: [{
checkGenericTypes: false,
checkReturnTypes: false
}],
messages: { noInlineInterfaces: "Extract this inline object type into a named interface or type alias and reference it here." }
},
defaultOptions: [{
checkGenericTypes: false,
checkReturnTypes: false
}],
create(context) {
const options = context.options[0] ?? {};
const checkGenericTypes = options.checkGenericTypes ?? false;
const checkReturnTypes = options.checkReturnTypes ?? false;
let interfaceCounter = 0;
/**
* Report all inline object type literals found in a type annotation.
*/
const reportTypeAnnotation = (typeAnnotation) => {
const literals = findTypeLiterals(typeAnnotation, [], checkGenericTypes);
for (const literal of literals) {
interfaceCounter = interfaceCounter + 1;
const interfaceName = interfaceCounter === 1 ? "InlineInterface" : `InlineInterface${String(interfaceCounter)}`;
context.report({
node: literal,
messageId: "noInlineInterfaces",
fix(fixer) {
const statementNode = findStatementAncestor(literal);
if (!statementNode) return null;
const { sourceCode } = context;
const openBrace = sourceCode.getFirstToken(literal);
const closeBrace = sourceCode.getLastToken(literal);
if (!openBrace || !closeBrace) return null;
const bodyText = sourceCode.getText().slice(openBrace.range[1], closeBrace.range[0]);
return [fixer.insertTextBefore(statementNode, `interface ${interfaceName} {${bodyText}}\n`), fixer.replaceText(literal, interfaceName)];
}
});
}
};
/**
* Check a parameter node for type annotations
* Handles both direct parameters and parameters with default values (AssignmentPattern).
*/
const checkParameter = (param) => {
let nodeToCheck = param;
if (param.type === AST_NODE_TYPES.AssignmentPattern) nodeToCheck = param.left;
if ((nodeToCheck.type === AST_NODE_TYPES.Identifier || nodeToCheck.type === AST_NODE_TYPES.ArrayPattern || nodeToCheck.type === AST_NODE_TYPES.ObjectPattern || nodeToCheck.type === AST_NODE_TYPES.RestElement) && nodeToCheck.typeAnnotation) reportTypeAnnotation(nodeToCheck.typeAnnotation.typeAnnotation);
};
return {
VariableDeclarator(node) {
if (isInsideClass(node)) return;
if (node.id.typeAnnotation) reportTypeAnnotation(node.id.typeAnnotation.typeAnnotation);
},
FunctionDeclaration(node) {
if (isInsideClass(node)) return;
node.params.forEach((param) => {
checkParameter(param);
});
if (checkReturnTypes && node.returnType) reportTypeAnnotation(node.returnType.typeAnnotation);
},
FunctionExpression(node) {
if (isInsideClass(node)) return;
node.params.forEach((param) => {
checkParameter(param);
});
if (checkReturnTypes && node.returnType) reportTypeAnnotation(node.returnType.typeAnnotation);
},
ArrowFunctionExpression(node) {
if (isInsideClass(node)) return;
node.params.forEach((param) => {
checkParameter(param);
});
if (checkReturnTypes && node.returnType) reportTypeAnnotation(node.returnType.typeAnnotation);
}
};
}
});
//#endregion
//#region src/rules/no-redundant-jsx-prop-usage.ts
const RULE_NAME$2 = "no-redundant-jsx-prop-usage";
const SKIP = Symbol("SKIP");
/**
* Extracts a primitive literal value from a TypeScript AST expression node.
* Returns SKIP if the expression is not a primitive literal we can compare.
*/
const extractPrimitiveLiteral = (node) => {
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
if (ts.isNumericLiteral(node)) return Number(node.text);
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
if (ts.isIdentifier(node) && node.text === "undefined") return;
if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand)) return -Number(node.operand.text);
return SKIP;
};
/**
* If the call expression is React.forwardRef(...) or forwardRef(...),
* returns the inner function (the actual component). Otherwise returns undefined.
*/
const unwrapForwardRef = (callExpr) => {
const callee = callExpr.expression;
let funcName;
if (ts.isIdentifier(callee)) funcName = callee.text;
else if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) funcName = callee.name.text;
if (funcName === "forwardRef") {
const firstArg = callExpr.arguments[0];
if (ts.isArrowFunction(firstArg) || ts.isFunctionExpression(firstArg)) return firstArg;
}
};
/**
* Given a TypeScript declaration node, attempts to find the function-like node
* that represents the React component. Returns a Map of prop name -\> default value,
* or null if the declaration cannot be analyzed.
*/
const extractDefaultsFromDeclaration = (decl) => {
let functionNode;
if (ts.isFunctionDeclaration(decl) || ts.isFunctionExpression(decl) || ts.isArrowFunction(decl)) functionNode = decl;
else if (ts.isVariableDeclaration(decl)) {
const init = decl.initializer;
if (!init) return null;
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) functionNode = init;
else if (ts.isCallExpression(init)) {
const inner = unwrapForwardRef(init);
if (inner) functionNode = inner;
}
}
if (!functionNode) return null;
const firstParam = functionNode.parameters[0];
if (!firstParam) return null;
const bindingPattern = firstParam.name;
if (!ts.isObjectBindingPattern(bindingPattern)) return null;
const defaults = /* @__PURE__ */ new Map();
for (const element of bindingPattern.elements) {
if (!element.initializer) continue;
let propName;
if (element.propertyName) {
if (ts.isIdentifier(element.propertyName)) propName = element.propertyName.text;
} else if (ts.isIdentifier(element.name)) propName = element.name.text;
if (!propName) continue;
const defaultValue = extractPrimitiveLiteral(element.initializer);
if (defaultValue !== SKIP) defaults.set(propName, defaultValue);
}
return defaults;
};
/**
* Extracts a primitive value from a JSX attribute value (ESTree side).
* Returns SKIP if the value is not a comparable primitive literal.
*/
const getJSXAttributePrimitiveValue = (attr) => {
const { value } = attr;
if (value === null) return true;
if (value.type === AST_NODE_TYPES.Literal) {
const v = value.value;
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean" || v === null) return v;
return SKIP;
}
if (value.type === AST_NODE_TYPES.JSXExpressionContainer) {
const expr = value.expression;
if (expr.type === AST_NODE_TYPES.Literal) {
const v = expr.value;
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean" || v === null) return v;
}
if (expr.type === AST_NODE_TYPES.Identifier && expr.name === "undefined") return;
if (expr.type === AST_NODE_TYPES.UnaryExpression && expr.operator === "-" && expr.argument.type === AST_NODE_TYPES.Literal && typeof expr.argument.value === "number") return -expr.argument.value;
}
return SKIP;
};
var no_redundant_jsx_prop_usage_default = createEslintRule({
name: RULE_NAME$2,
meta: {
type: "suggestion",
docs: {
description: "disallow passing a JSX prop whose value matches the component's destructuring default for that prop",
url: "https://github.com/AndreaPontrandolfo/eslint-plugin-fsecond/blob/master/docs/rules/no-redundant-jsx-prop-usage.md",
recommended: false,
requiresTypeChecking: true
},
fixable: "code",
schema: [],
messages: { noRedundantJsxPropUsage: "Prop \"{{propName}}\" is redundant — it matches the default value ({{defaultValue}})." }
},
defaultOptions: [],
create(context) {
const services = ESLintUtils.getParserServices(context);
const checker = services.program.getTypeChecker();
const defaultsCache = /* @__PURE__ */ new Map();
const getDefaultsForSymbol = (symbol) => {
if (defaultsCache.has(symbol)) return defaultsCache.get(symbol) ?? null;
const { declarations } = symbol;
if (!declarations || declarations.length === 0) {
defaultsCache.set(symbol, null);
return null;
}
for (const declaration of declarations) {
const defaults = extractDefaultsFromDeclaration(declaration);
if (defaults !== null) {
defaultsCache.set(symbol, defaults);
return defaults;
}
}
defaultsCache.set(symbol, null);
return null;
};
return { JSXOpeningElement(node) {
const nameNode = node.name;
if (nameNode.type === AST_NODE_TYPES.JSXIdentifier && (nameNode.name.length === 0 || nameNode.name.startsWith(nameNode.name[0].toLowerCase()))) return;
const symbol = services.getSymbolAtLocation(nameNode);
if (!symbol) return;
const defaults = getDefaultsForSymbol(symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol);
if (!defaults || defaults.size === 0) return;
for (const attr of node.attributes) {
if (attr.type !== AST_NODE_TYPES.JSXAttribute) continue;
const propNameNode = attr.name;
if (propNameNode.type !== AST_NODE_TYPES.JSXIdentifier) continue;
const propName = propNameNode.name;
if (!defaults.has(propName)) continue;
const attrValue = getJSXAttributePrimitiveValue(attr);
if (attrValue === SKIP) continue;
const defaultValue = defaults.get(propName);
if (attrValue === defaultValue) context.report({
node: attr,
messageId: "noRedundantJsxPropUsage",
data: {
propName,
defaultValue: String(defaultValue)
},
fix(fixer) {
const tokenBefore = context.sourceCode.getTokenBefore(attr);
const start = tokenBefore ? tokenBefore.range[1] : attr.range[0];
return fixer.removeRange([start, attr.range[1]]);
}
});
}
} };
}
});
//#endregion
//#region src/rules/prefer-destructured-optionals.ts
const RULE_NAME$1 = "prefer-destructured-optionals";
var prefer_destructured_optionals_default = createEslintRule({
name: RULE_NAME$1,
meta: {
type: "suggestion",
docs: {
description: "enforce placing optional parameters on a destructured object instead of the function signature itself",
recommended: true,
url: "https://github.com/AndreaPontrandolfo/eslint-plugin-fsecond/blob/master/docs/rules/prefer-destructured-optionals.md"
},
schema: [],
messages: { noNonDestructuredOptional: "Convert this optional parameter to a destructured parameter." }
},
defaultOptions: [],
create(context) {
const checkParameters = (params) => {
let isParamObjectInTheMiddle = false;
params.forEach((param) => {
if (param.type === AST_NODE_TYPES.AssignmentPattern && param.left.type !== AST_NODE_TYPES.ObjectPattern || param.type === AST_NODE_TYPES.Identifier && param.optional) context.report({
node: param,
messageId: "noNonDestructuredOptional"
});
if (isParamObjectInTheMiddle && param.type !== AST_NODE_TYPES.ObjectPattern) context.report({
node: param,
messageId: "noNonDestructuredOptional"
});
if (param.type === AST_NODE_TYPES.ObjectPattern || param.type === AST_NODE_TYPES.AssignmentPattern && param.left.type === AST_NODE_TYPES.ObjectPattern) isParamObjectInTheMiddle = true;
});
};
return {
FunctionDeclaration(node) {
checkParameters(node.params);
},
FunctionExpression(node) {
checkParameters(node.params);
},
ArrowFunctionExpression(node) {
checkParameters(node.params);
}
};
}
});
//#endregion
//#region src/rules/valid-event-listener.ts
const RULE_NAME = "valid-event-listener";
/**
* Helper function: Check if a node is a method call to addEventListener or removeEventListener.
*/
const isEventListenerMethodCall = (node, methodName) => {
let currentNode = node;
if (currentNode instanceof Object && "type" in currentNode && currentNode.type === AST_NODE_TYPES.ChainExpression) currentNode = currentNode.expression;
if (!(currentNode instanceof Object) || !("type" in currentNode) || currentNode.type !== AST_NODE_TYPES.CallExpression || !("callee" in currentNode)) return false;
let { callee } = currentNode;
if (callee instanceof Object && callee.type === AST_NODE_TYPES.ChainExpression) callee = callee.expression;
return callee instanceof Object && callee.type === AST_NODE_TYPES.MemberExpression && callee.property instanceof Object && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === methodName;
};
/**
* Helper function: Check if node is an addEventListener call.
*/
const isAddEventListenerCall = (node) => {
return isEventListenerMethodCall(node, "addEventListener");
};
/**
* Helper function: Check if node is a removeEventListener call.
*/
const isRemoveEventListenerCall = (node) => {
return isEventListenerMethodCall(node, "removeEventListener");
};
/**
* Helper function: Check if a call expression is useEffect or useLayoutEffect.
*/
const isUseEffectOrUseLayoutEffectCall = (node) => {
const { callee } = node;
if (callee.type === AST_NODE_TYPES.Identifier) return callee.name === "useEffect" || callee.name === "useLayoutEffect";
if (callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed) return callee.property.type === AST_NODE_TYPES.Identifier && (callee.property.name === "useEffect" || callee.property.name === "useLayoutEffect");
return false;
};
/**
* Helper function: Check if an expression statement contains a conditional addEventListener.
*/
const isConditionalAddEventListener = (node) => {
if (node.type !== AST_NODE_TYPES.IfStatement) return false;
if (node.consequent.type === AST_NODE_TYPES.BlockStatement) {
for (const statement of node.consequent.body) if (statement.type === AST_NODE_TYPES.ExpressionStatement && isAddEventListenerCall(statement.expression)) return true;
}
if (node.consequent.type === AST_NODE_TYPES.ExpressionStatement && isAddEventListenerCall(node.consequent.expression)) return true;
if (node.alternate?.type === AST_NODE_TYPES.BlockStatement) {
for (const statement of node.alternate.body) if (statement.type === AST_NODE_TYPES.ExpressionStatement && isAddEventListenerCall(statement.expression)) return true;
}
return node.alternate?.type === AST_NODE_TYPES.ExpressionStatement && isAddEventListenerCall(node.alternate.expression);
};
/**
* Helper function: Check if expression has conditional addEventListener (logical or ternary).
*/
const isConditionalExpression = (expression) => {
if (expression.type === AST_NODE_TYPES.LogicalExpression && expression.operator === "&&" && isAddEventListenerCall(expression.right)) return true;
if (expression.type === AST_NODE_TYPES.ConditionalExpression) {
if (isAddEventListenerCall(expression.consequent)) return true;
if (isAddEventListenerCall(expression.alternate)) return true;
}
return false;
};
/**
* Helper function: Search block for addEventListener, return true if found.
*/
const findAddEventListenerInBlock = (statements) => {
for (const statement of statements) if (statement.type === AST_NODE_TYPES.ExpressionStatement) {
if (isAddEventListenerCall(statement.expression)) return true;
if (isConditionalExpression(statement.expression)) return true;
}
return false;
};
/**
* Helper function: Search block for removeEventListener, return true if found.
*/
const findRemoveEventListenerInBlock = (statements) => {
for (const statement of statements) if (statement.type === AST_NODE_TYPES.ExpressionStatement && isRemoveEventListenerCall(statement.expression)) return true;
return false;
};
/**
* Helper function: Search cleanup function (return statement) for removeEventListener.
*/
const findRemoveEventListenerInCleanup = (statements) => {
for (const statement of statements) {
if (statement.type !== AST_NODE_TYPES.ReturnStatement) continue;
const cleanupFunction = statement.argument;
if (cleanupFunction && (cleanupFunction.type === AST_NODE_TYPES.ArrowFunctionExpression || cleanupFunction.type === AST_NODE_TYPES.FunctionExpression) && cleanupFunction.body?.type === AST_NODE_TYPES.BlockStatement && cleanupFunction.body.body && findRemoveEventListenerInBlock(cleanupFunction.body.body)) return true;
if (cleanupFunction?.type === AST_NODE_TYPES.ArrowFunctionExpression && cleanupFunction.body && isRemoveEventListenerCall(cleanupFunction.body)) return true;
}
return false;
};
/**
* Helper function: Check if there's a return statement with a cleanup function.
*/
const hasReturnStatement = (statements) => {
for (const statement of statements) if (statement.type === AST_NODE_TYPES.ReturnStatement && statement.argument && (statement.argument.type === AST_NODE_TYPES.ArrowFunctionExpression || statement.argument.type === AST_NODE_TYPES.FunctionExpression)) return true;
return false;
};
/**
* Helper function: Check if useEffect body has conditional addEventListener.
*/
const hasConditionalAddEventListener = (statements) => {
for (const statement of statements) {
if (isConditionalAddEventListener(statement)) return true;
if (statement.type === AST_NODE_TYPES.ExpressionStatement && isConditionalExpression(statement.expression)) return true;
}
return false;
};
/**
* Helper function: Unwrap ChainExpression to get the actual CallExpression.
*/
const unwrapToCallExpression = (node) => {
let currentNode = node;
if (currentNode instanceof Object && "type" in currentNode && currentNode.type === AST_NODE_TYPES.ChainExpression) currentNode = currentNode.expression;
if (!(currentNode instanceof Object) || !("type" in currentNode) || currentNode.type !== AST_NODE_TYPES.CallExpression) return null;
return currentNode;
};
/**
* Helper function: Check if addEventListener call has \{ once: true \} option.
*/
const hasOnceOption = (call) => {
if (!isEventListenerMethodCall(call, "addEventListener")) return false;
const callExpression = unwrapToCallExpression(call);
if (!callExpression) return false;
const optionsArgument = callExpression.arguments[2];
if (optionsArgument?.type === AST_NODE_TYPES.ObjectExpression) {
for (const property of optionsArgument.properties) if (property.type === AST_NODE_TYPES.Property && property.key.type === AST_NODE_TYPES.Identifier && property.key.name === "once" && property.value.type === AST_NODE_TYPES.Literal && property.value.value === true) return true;
}
return false;
};
/**
* Helper function: Check if there's any addEventListener in block that is not once.
* Returns true if at least one addEventListener does NOT have \{ once: true \}.
*/
const hasNonOnceAddEventListenerInBlock = (statements) => {
for (const statement of statements) if (statement.type === AST_NODE_TYPES.ExpressionStatement && isEventListenerMethodCall(statement.expression, "addEventListener") && !hasOnceOption(statement.expression)) return true;
return false;
};
var valid_event_listener_default = createEslintRule({
name: RULE_NAME,
meta: {
type: "problem",
docs: {
description: "enforces best practices around addEventListener method in React components.",
url: "https://github.com/AndreaPontrandolfo/eslint-plugin-fsecond/blob/master/docs/rules/valid-event-listener.md",
recommended: true
},
schema: [{
type: "object",
additionalProperties: false,
properties: { requireUseEventListenerHook: {
description: "Require the use of a useEventListener hook",
type: "boolean"
} }
}],
defaultOptions: [{ requireUseEventListenerHook: true }],
messages: {
"required-cleanup": "Missing a cleanup function for the addEventListener in React useEffect.",
"required-remove-eventListener": "Missing a matching removeEventListener in the React useEffect cleanup.",
"no-conditional-addeventlistener": "Don't wrap addEventListener in a condition in React components.",
"require-use-event-listener-hook": "Use a useEventListener hook from a React hooks library instead of manually adding and removing event listeners."
}
},
defaultOptions: [{ requireUseEventListenerHook: true }],
create(context) {
const { requireUseEventListenerHook } = {
requireUseEventListenerHook: true,
...context.options[0] ?? {}
};
return { ExpressionStatement(node) {
const expression = node?.expression;
if (expression.type !== AST_NODE_TYPES.CallExpression) return;
if (!isUseEffectOrUseLayoutEffectCall(expression)) return;
const firstArgument = expression.arguments[0];
const isFunctionExpression = firstArgument?.type === AST_NODE_TYPES.ArrowFunctionExpression || firstArgument?.type === AST_NODE_TYPES.FunctionExpression;
const useEffectBodyInternalItems = expression?.arguments && expression.arguments.length > 0 && isFunctionExpression && firstArgument.body?.type === AST_NODE_TYPES.BlockStatement && firstArgument.body.body;
if (!useEffectBodyInternalItems || useEffectBodyInternalItems.length === 0) return;
if (hasConditionalAddEventListener(useEffectBodyInternalItems)) {
context.report({
node,
messageId: "no-conditional-addeventlistener"
});
return;
}
if (!findAddEventListenerInBlock(useEffectBodyInternalItems)) return;
if (requireUseEventListenerHook) {
context.report({
node,
messageId: "require-use-event-listener-hook"
});
return;
}
const hasReturnStmt = hasReturnStatement(useEffectBodyInternalItems);
if (!hasNonOnceAddEventListenerInBlock(useEffectBodyInternalItems)) return;
if (!findRemoveEventListenerInCleanup(useEffectBodyInternalItems)) if (hasReturnStmt) context.report({
node,
messageId: "required-remove-eventListener"
});
else context.report({
node,
messageId: "required-cleanup"
});
} };
}
});
//#endregion
//#region src/index.ts
const eslintPluginShortName = "fsecond";
const plugin = {
meta: {
name,
version
},
configs: {},
rules: {
"prefer-destructured-optionals": prefer_destructured_optionals_default,
"valid-event-listener": valid_event_listener_default,
"no-inline-interfaces": no_inline_interfaces_default,
"no-redundant-jsx-prop-usage": no_redundant_jsx_prop_usage_default
}
};
plugin.configs = {
recommended: [{
plugins: { fsecond: plugin },
rules: {
[`${eslintPluginShortName}/prefer-destructured-optionals`]: 2,
[`${eslintPluginShortName}/valid-event-listener`]: 2,
[`${eslintPluginShortName}/no-inline-interfaces`]: [2, {
checkGenericTypes: false,
checkReturnTypes: true
}]
}
}],
recommendedTypeChecked: [{
plugins: { fsecond: plugin },
rules: {
[`${eslintPluginShortName}/prefer-destructured-optionals`]: 2,
[`${eslintPluginShortName}/valid-event-listener`]: 2,
[`${eslintPluginShortName}/no-inline-interfaces`]: [2, {
checkGenericTypes: false,
checkReturnTypes: true
}],
[`${eslintPluginShortName}/no-redundant-jsx-prop-usage`]: 2
}
}]
};
//#endregion
export { plugin as default };
//# sourceMappingURL=index.mjs.map