eslint-plugin-fsecond
Version:
ESLint plugin with some opinionated and stylistic rules.
313 lines (305 loc) • 13.5 kB
JavaScript
'use strict';
const readPackageUp = require('read-package-up');
const minimatch = require('minimatch');
const utils = require('@typescript-eslint/utils');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
const createEslintRule = utils.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(".", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
packageJson = readPackageUp.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.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
}
};
module.exports = plugin;