add custom eslint rule to prefer countBy over findBy

This commit is contained in:
Johann150 2023-01-02 20:58:33 +01:00
parent 7bf4d4426a
commit 29714d1ae0
Signed by untrusted user: Johann150
GPG key ID: 9EE6577A2A06F8F1
4 changed files with 138 additions and 1 deletions

View file

@ -6,7 +6,11 @@ module.exports = {
extends: [
'../shared/.eslintrc.js',
],
plugins: [
'foundkey-custom-rules',
]
rules: {
'foundkey-custom-rules/typeorm-prefer-count': 'error',
'import/order': ['warn', {
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'pathGroups': [

View file

@ -174,6 +174,7 @@
"execa": "6.1.0",
"form-data": "^4.0.0",
"sinon": "^14.0.2",
"typescript": "^4.9.4"
"typescript": "^4.9.4",
"eslint-plugin-foundkey-custom-rules": "file:../shared/custom-rules"
}
}

View file

@ -0,0 +1,12 @@
const fs = require("fs");
const path = require("path");
const ruleFiles = fs
.readdirSync(__dirname)
.filter((file) => file !== "index.js" && !file.endsWith("test.js"));
const rules = Object.fromEntries(
ruleFiles.map((file) => [path.basename(file, ".js"), require("./" + file)])
);
module.exports = { rules };

View file

@ -0,0 +1,120 @@
const dbFunctions = ["find", "findBy", "findOne", "findOneBy", "findOneOrFail", "findOneByOrFail"];
module.exports = {
meta: {
type: "suggestion",
},
create(context) {
return {
"Program:exit"(programNode) {
const isNull = (node) => node.type === "Literal" && node.value === null;
const scopes = [context.getScope()];
while (scopes.length > 0) {
const s = scopes.pop();
s.childScopes.forEach(x => scopes.push(x));
const variables = s.variables
.filter(x =>
x.references
.filter((ref) => ref.isRead())
.length > 0
&& x.defs.length > 0
);
for (const v of variables) {
let findValueAssign = null;
// if the find value was not read, there will already be an unused variable lint
// or maybe this was inside an if/else so we dont want to cause a false positive
let read = false;
for (const ref of v.references) {
if (
ref.isWrite()
) {
if (!ref.writeExpr) continue;
let writeExpr = ref.writeExpr;
// unwrap write expression, order matters to correctly unwrap
// something like "await Promise.all([a, b])".
// Basically a poor mans data flow analysis.
if (writeExpr.type === "AwaitExpression")
writeExpr = writeExpr.argument;
if (
writeExpr.type === "CallExpression"
&& writeExpr.callee.type === "MemberExpression"
&& writeExpr.callee.object.type === "Identifier"
&& writeExpr.callee.object.name === "Promise"
&& writeExpr.callee.property.type === "Identifier"
&& writeExpr.callee.property.name === "all"
)
writeExpr = writeExpr.arguments[0];
if (
writeExpr.type === "ArrayExpression"
&& ref.identifier.parent.type === "ArrayPattern"
) {
// use same index as the index of the identifier in the array pattern
const index = ref.identifier.parent.elements.indexOf(ref.identifier);
if (index > -1)
writeExpr = writeExpr.elements[index];
}
// check if this is a DB find thingy
if (
writeExpr.type === "CallExpression"
&& writeExpr.callee.type === "MemberExpression"
&& writeExpr.callee.property.type === "Identifier"
&& dbFunctions.includes(writeExpr.callee.property.name)
) {
if (findValueAssign && read) {
context.report({
node: findValueAssign,
message: "The returned object(s) are not read from, use `count` or `countBy` instead."
});
}
findValueAssign = writeExpr;
read = false;
}
}
// must be a read
else if (findValueAssign) {
const node = ref.identifier;
if(!(
(
// explicit null check
node.parent.type === "BinaryExpression" &&
["==", "==="].includes(node.parent.operator) &&
(
(isNull(node.parent.left) && node.parent.right === node)
||
(isNull(node.parent.right) && node.parent.left === node)
)
) || (
// implicit null check
["IfStatement", "ConditionalExpression"].includes(node.parent.type)
) || (
// different implicit null check
node.parent.type === "UnaryExpression" &&
node.parent.operator === "!"
) || (
// length check
node.parent.type === "MemberExpression" &&
node.parent.object === node &&
node.parent.property.type === "Identifier" &&
node.parent.property.name === "length"
)
)) {
// other use, dont report
findValueAssign = null;
}
}
}
if (findValueAssign) {
context.report({
node: findValueAssign,
message: "The returned object(s) are not read from, use `count` or `countBy` instead."
});
}
}
}
}
};
}
};