UNPKG

eslint-plugin-fsecond

Version:
310 lines (303 loc) 13.2 kB
import { readPackageUpSync } from 'read-package-up'; import { minimatch } from 'minimatch'; import { ESLintUtils } from '@typescript-eslint/utils'; const createEslintRule = ESLintUtils.RuleCreator((ruleName) => ruleName); const RULE_NAME$2 = "prefer-aliased-path"; const preferAliasedPath = createEslintRule({ name: RULE_NAME$2, meta: { type: "suggestion", docs: { description: "Suggest using a package.json alias subpath import instead of a relative import." }, schema: [ { type: "object", additionalProperties: false, properties: {} } ], messages: { preferAliasedPath: "Use an aliased path instead of a relative path for '{{ importPath }}'." } }, defaultOptions: [], create(context) { let packageJson; const sourceCode = context.getSourceCode(); sourceCode.ast.body.some((node) => { if (node.type === "ImportDeclaration") { const packagePath = node.source.value; if (packagePath.startsWith(".")) { const currentLocalDirectory = new URL(".", import.meta.url); packageJson = readPackageUpSync({ cwd: currentLocalDirectory }); } } if (packageJson) { return true; } }); if (!packageJson?.packageJson?.imports) { return {}; } return { ImportDeclaration(node) { if (node.type === "ImportDeclaration") { const packagePath = node.source.value; if (packagePath.startsWith(".")) { const importAliases = Object.values( packageJson.packageJson.imports ); for (const importAlias of importAliases) { if (!importAlias || typeof importAlias !== "string") { continue; } const capturedExtension = importAlias.endsWith(".ts") ? ".ts" : importAlias.endsWith(".js") ? ".js" : ""; const isPathMatched = minimatch( `${packagePath}${capturedExtension}`, importAlias ); if (isPathMatched) { context.report({ node, messageId: "preferAliasedPath", data: { importPath: packagePath } }); } } } } } }; } }); const RULE_NAME$1 = "prefer-destructured-optionals"; const preferDestructuredOptionals = createEslintRule({ name: RULE_NAME$1, meta: { type: "problem", docs: { description: "Enforce placing optional parameters on a destructured object instead of the function signature itself" }, schema: [], messages: { noNonDestructuredOptional: "Move this optional parameter to a destructured parameter." } }, defaultOptions: [], create(context) { function checkParameters(params) { let isParamObjectInTheMiddle = false; params.forEach((param) => { if (param.type === "ObjectPattern") { isParamObjectInTheMiddle = true; } if (param.left?.type !== "ObjectPattern" && (param.optional || param.type === "AssignmentPattern")) { context.report({ node: param, messageId: "noNonDestructuredOptional" }); } if (isParamObjectInTheMiddle && param.type !== "ObjectPattern") { context.report({ node: param, messageId: "noNonDestructuredOptional" }); } }); } return { FunctionDeclaration(node) { checkParameters(node.params); }, FunctionExpression(node) { checkParameters(node.params); }, ArrowFunctionExpression(node) { checkParameters(node.params); } }; } }); const RULE_NAME = "valid-event-listener"; const validEventListener = createEslintRule({ name: RULE_NAME, meta: { type: "problem", docs: { description: "Enforces best practices around addEventListener method." }, schema: [ { type: "object", additionalProperties: false, properties: { requireUseEventListenerHook: { type: "boolean", default: true } } } ], messages: { "required-cleanup": "Missing a cleanup function for the addEventListener.", "required-remove-eventListener": "Missing a matching removeEventListener.", "no-conditional-addeventlistener": "Don't wrap a addEventListener in a condition.", "require-use-event-listener-hook": "Use a useEventListener hook from a hooks library instead of manually adding and removing event listeners." } }, defaultOptions: [ { requireUseEventListenerHook: true } ], create(context) { const providedFirstOption = context.options[0]; const { requireUseEventListenerHook } = { requireUseEventListenerHook: true, ...providedFirstOption }; return { ExpressionStatement(node) { let hasAddEventListener = false; let hasReturnStatement = false; let hasRemoveEventListener = false; let hasAddEventListenerInCondition = false; const expression = node?.expression; if (expression.type !== "CallExpression") { return; } const calleeName = expression?.callee && expression.callee.type === "Identifier" && expression.callee.name; if (calleeName === "useEffect") { const useEffectBodyInternalItems = expression?.arguments && expression.arguments.length > 0 && expression.arguments[0].type === "ArrowFunctionExpression" && expression.arguments[0].body && expression.arguments[0].body.type === "BlockStatement" && expression.arguments[0].body.body; if (useEffectBodyInternalItems && useEffectBodyInternalItems.length > 0) { useEffectBodyInternalItems.every((element) => { const elementType = element.type; if (elementType === "IfStatement") { if (element.consequent.type === "BlockStatement") { element.consequent.body.forEach((ifStatementBodyElement) => { const ifStatementExpression = ifStatementBodyElement.type === "ExpressionStatement" && ifStatementBodyElement.expression; const ifStatementExpressionCalle = ifStatementExpression.type === "CallExpression" && ifStatementExpression.callee; if (ifStatementExpressionCalle.type === "MemberExpression" && ifStatementExpressionCalle.property.type === "Identifier" && ifStatementExpressionCalle.property.name === "addEventListener") { hasAddEventListenerInCondition = true; } return hasAddEventListenerInCondition; }); } if (element.alternate?.type === "BlockStatement") { element.alternate.body.forEach( (ifStatementAlternateBodyElement) => { const ifStatementAlternateExpression = ifStatementAlternateBodyElement.type === "ExpressionStatement" && ifStatementAlternateBodyElement.expression; const ifStatementAlternateExpressionCalle = ifStatementAlternateExpression.type === "CallExpression" && ifStatementAlternateExpression.callee; if (ifStatementAlternateExpressionCalle.type === "MemberExpression" && ifStatementAlternateExpressionCalle.property.type === "Identifier" && ifStatementAlternateExpressionCalle.property.name === "addEventListener") { hasAddEventListenerInCondition = true; } return true; } ); } if (element.consequent.type === "ExpressionStatement") { const ifStatementExpression = element.consequent.expression; if (ifStatementExpression.type === "CallExpression" && ifStatementExpression.callee.type === "MemberExpression" && ifStatementExpression.callee.property.type === "Identifier" && ifStatementExpression.callee.property.name === "addEventListener") { hasAddEventListenerInCondition = true; return true; } } } if (hasRemoveEventListener) { return false; } if (elementType === "ExpressionStatement") { const internalExpression = element.expression; if (internalExpression.type === "LogicalExpression" && internalExpression.operator === "&&" && internalExpression.right.type === "CallExpression" && internalExpression.right.callee.type === "MemberExpression" && internalExpression.right.callee.property.type === "Identifier" && internalExpression.right.callee.property.name === "addEventListener") { hasAddEventListenerInCondition = true; return true; } if (internalExpression.type === "ConditionalExpression" && internalExpression.consequent.type === "CallExpression" && internalExpression.consequent.callee.type === "MemberExpression" && internalExpression.consequent.callee.property.type === "Identifier" && internalExpression.consequent.callee.property.name === "addEventListener" || internalExpression.type === "ConditionalExpression" && internalExpression.alternate.type === "CallExpression" && internalExpression.alternate.callee.type === "MemberExpression" && internalExpression.alternate.callee.property.type === "Identifier" && internalExpression.alternate.callee.property.name === "addEventListener") { hasAddEventListenerInCondition = true; return true; } if (internalExpression.type !== "CallExpression") { return true; } const internalExpressionCallee = internalExpression.callee; if (internalExpressionCallee.type !== "MemberExpression") { return true; } const internalExpressionCalleeProperty = internalExpressionCallee.property; if (internalExpressionCalleeProperty.type !== "Identifier") { return true; } const internalExpressionCalleePropertyName = internalExpressionCalleeProperty.name; if (internalExpressionCalleePropertyName === "addEventListener") { hasAddEventListener = true; return true; } } if (hasAddEventListener && elementType === "ReturnStatement") { hasReturnStatement = true; const returnBlockBody = element.argument && element.argument.type === "ArrowFunctionExpression" && element.argument.body && element.argument.body.type === "BlockStatement" && element.argument.body.body; if (returnBlockBody && returnBlockBody.length > 0) { returnBlockBody.every((returnElement) => { if (hasRemoveEventListener) { return false; } const returnElementCallee = returnElement.type === "ExpressionStatement" && returnElement.expression && returnElement.expression.type === "CallExpression" && returnElement.expression.callee; const returnElementCalleeProperty = returnElementCallee && returnElementCallee.type === "MemberExpression" && returnElementCallee.property && returnElementCallee.property.type === "Identifier" && returnElementCallee.property.name; if (returnElementCalleeProperty === "removeEventListener") { hasRemoveEventListener = true; } return true; }); } } return true; }); } if (hasAddEventListener && requireUseEventListenerHook) { context.report({ node, messageId: "require-use-event-listener-hook" }); } if (hasAddEventListenerInCondition) { context.report({ node, messageId: "no-conditional-addeventlistener" }); } else { if (hasAddEventListener && !hasRemoveEventListener) { if (hasReturnStatement) { context.report({ node, messageId: "required-remove-eventListener" }); } else { context.report({ node, messageId: "required-cleanup" }); } } } } } }; } }); const plugin = { meta: { name: "eslint-plugin-fsecond", version: "1.1.0" }, rules: { "prefer-destructured-optionals": preferDestructuredOptionals, "valid-event-listener": validEventListener, "prefer-aliased-path": preferAliasedPath } }; export { plugin as default };