eslint-plugin-fsecond
Version:
ESLint plugin with some opinionated and stylistic rules.
310 lines (303 loc) • 13.2 kB
JavaScript
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 };