feat: add new biome plugins for enforcing select and where clauses, remove deprecated kysely rules

This commit is contained in:
Marco Pedone 2025-09-01 09:04:02 +02:00
parent 82dd4d19cb
commit 1860bce2a3
10 changed files with 39 additions and 347 deletions

View file

@ -100,6 +100,7 @@
} }
} }
}, },
"plugins": ["biome_plugins/missing.grit", "biome_plugins/null.grit"],
"vcs": { "vcs": {
"clientKind": "git", "clientKind": "git",
"enabled": true, "enabled": true,

View file

@ -0,0 +1,25 @@
language js
`$fn($_)` where {
if ($fn <: contains `selectFrom`) {
or {
$fn <: contains `execute`,
$fn <: contains `executeTakeFirst`,
$fn <: contains `executeTakeFirstOrThrow`
},
! $fn <: contains `select`,
! $fn <: contains `selectAll`,
register_diagnostic(span=$fn, message="select statement must contain select() or selectAll()", severity="error")
} else if (or {
$fn <: contains `updateTable`,
$fn <: contains `deleteFrom`
}) {
or {
$fn <: contains `execute`,
$fn <: contains `executeTakeFirst`,
$fn <: contains `executeTakeFirstOrThrow`
},
! $fn <: contains `where`,
register_diagnostic(span=$fn, message="where or delete statements must contain where()", severity="error")
}
}

View file

@ -0,0 +1,13 @@
language js
`$fn($a)` where {
$fn <: contains `where`,
or {
$a <: contains `'='`,
$a <: contains `'=='`,
$a <: contains `'!='`,
$a <: contains `'!=='`
},
$a <: contains `null`,
register_diagnostic(span=$a, message=$a, severity="error")
}

View file

@ -1,15 +0,0 @@
const plugin = {
meta: {
name: "kyselyRules",
version: "1.0.0",
},
configs: {},
rules: {
"enforce-select": require("./rules/select"),
"enforce-where": require("./rules/where"),
"enforce-null": require("./rules/null"),
},
processors: {},
};
module.exports = plugin;

View file

@ -1,107 +0,0 @@
//@ts-nocheck
module.exports = {
meta: {
type: "problem",
docs: {
description:
"Ensure that all where condition that thest for null values utilize the correct operators.",
},
fixable: "code",
schema: [], // no options
},
create(context) {
const sourceCode = context.sourceCode;
const reportedNodes = new Set();
if (!/\.where\(/.test(sourceCode.text)) {
// Skip
return {};
}
return {
ExpressionStatement(node) {
evaluate(node.expression, sourceCode.getTokens(node));
},
AwaitExpression(node) {
evaluate(node.argument, sourceCode.getTokens(node));
},
};
function Check(expression, operator) {
const forbiddenOperators = [
"'='",
"'=='",
"'!='",
"'!=='",
'"="',
'"=="',
'"!="',
'"!=="',
];
if (forbiddenOperators.includes(operator.value)) {
const message = `The where condition is testing for null using the ${operator.value} operator. Ensure this is intentional and uses the correct operator.`;
context.report({
node: expression,
message,
fix: (fixer) => {
const correctOperator =
operator.value === "'='" ||
operator.value === "'=='" ||
operator.value === '"="' ||
operator.value === '"=="'
? "'is'"
: "'is not'";
return fixer.replaceText(operator, correctOperator);
},
});
}
}
function findNodes(expression, node, nodes) {
const nullNodes = nodes.filter((token) => token.value === "null");
if (nullNodes.length === 0) {
return;
}
for (let i = 0; i < nullNodes.length; i++) {
const nullNode = nullNodes[i];
// Avoid duplicate reports
if (reportedNodes.has(nullNode)) {
continue;
}
reportedNodes.add(nullNode);
const operator = sourceCode.getTokensBefore(nullNode, 2)[0];
if (!operator) {
return;
}
Check(expression, operator);
}
}
function evaluate(expression, nodes) {
if (!expression || expression.type !== "CallExpression") {
return true;
}
const whereNodes = nodes.filter((token) => token.value === "where");
if (whereNodes.length === 0 || !whereNodes) {
return;
}
if (whereNodes.length === 1) {
findNodes(expression, whereNodes[0], nodes);
return;
}
for (let i = 0; i < whereNodes.length; i++) {
const whereNode = whereNodes[i];
findNodes(expression, whereNode, nodes);
}
return;
}
},
};

View file

@ -1,55 +0,0 @@
const { RuleTester } = require("eslint");
const rule = require("./null");
const ruleTester = new RuleTester({
languageOptions: { ecmaVersion: 2015 },
});
ruleTester.run(
"enforce-null", // rule name
rule,
{
valid: [
{
code: "trx.selectFrom('name').select(['field']).where('something', 'is', null).execute()",
},
{
code: "trx.selectFrom('name').select(['field']).where('something', 'is not', null).execute()",
},
],
invalid: [
{
code: "trx.selectFrom('name').select(['field']).where('something', '=', null).execute()",
errors: 1,
output:
"trx.selectFrom('name').select(['field']).where('something', 'is', null).execute()",
},
{
code: "trx.selectFrom('name').select(['field']).where('something', '==', null).execute()",
errors: 1,
output:
"trx.selectFrom('name').select(['field']).where('something', 'is', null).execute()",
},
{
code: "trx.selectFrom('name').select(['field']).where('something', '!=', null).execute()",
errors: 1,
output:
"trx.selectFrom('name').select(['field']).where('something', 'is not', null).execute()",
},
{
code: "trx.selectFrom('name').select(['field']).where('something', '!==', null).execute()",
errors: 1,
output:
"trx.selectFrom('name').select(['field']).where('something', 'is not', null).execute()",
},
{
code: "trx.selectFrom('name').select(['field']).where('something', 'is', null).where('something2', '=', null).execute()",
errors: 1,
output:
"trx.selectFrom('name').select(['field']).where('something', 'is', null).where('something2', 'is', null).execute()",
},
],
},
);
console.log("EnforceNull: All tests passed!");

View file

@ -1,61 +0,0 @@
//@ts-nocheck
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Description of the rule",
},
fixable: "code",
schema: [], // no options
},
create(context) {
const sourceCode = context.sourceCode;
if (!/\.selectFrom\(/.test(sourceCode.text)) {
// Skip
return {};
}
function evaluate(expression, nodes) {
if (!expression || expression.type !== "CallExpression") {
return true;
}
const hasSelectFrom = nodes.some((token) => token.value === "selectFrom");
if (hasSelectFrom) {
const hasSelect = nodes.some(
(token) => token.value === "select" || token.value === "selectAll",
);
if (!hasSelect) {
let closingParenthesisNode = null;
const selectNodeIndex = nodes.findIndex(
(token) => token.value === "selectFrom",
);
if (selectNodeIndex !== -1) {
closingParenthesisNode = nodes
.slice(selectNodeIndex)
.find((token) => token.value === ")");
}
context.report({
node: expression,
message: `Call chains containing "selectFrom" must also include a "select" or "selectAll" clause.`,
fix: (fixer) => {
return fixer.insertTextAfter(
closingParenthesisNode || nodes[6],
".selectAll()",
);
},
});
}
}
}
return {
ExpressionStatement(node) {
evaluate(node.expression, sourceCode.getTokens(node));
},
AwaitExpression(node) {
evaluate(node.argument, sourceCode.getTokens(node));
},
};
},
};

View file

@ -1,31 +0,0 @@
const { RuleTester } = require("eslint");
const rule = require("./select");
const ruleTester = new RuleTester({
languageOptions: { ecmaVersion: 2015 },
});
ruleTester.run(
"enforce-select", // rule name
rule,
{
valid: [
{
code: "trx.selectFrom('name').select(['field']).where('something', '=', something).execute()",
},
{
code: "trx.selectFrom('name').selectAll().where('something', '=', something).execute()",
},
],
invalid: [
{
code: "trx.selectFrom('name').where('something', '=', something).execute()",
errors: 1,
output:
"trx.selectFrom('name').selectAll().where('something', '=', something).execute()",
},
],
},
);
console.log("EnforceSelect: All tests passed!");

View file

@ -1,52 +0,0 @@
//@ts-nocheck
module.exports = {
meta: {
type: "problem",
docs: {
description:
"Ensures that any call chain which affects a table also includes a 'where' clause.",
},
fixable: "code",
schema: [], // no options
},
create(context) {
const sourceCode = context.sourceCode;
if (!/\.updateTable\(|\.deleteFrom\(/.test(sourceCode.text)) {
// Skip
return {};
}
return {
ExpressionStatement(node) {
evaluate(node.expression, sourceCode.getTokens(node));
},
AwaitExpression(node) {
evaluate(node.argument, sourceCode.getTokens(node));
},
};
function evaluate(expression, nodes) {
if (!expression || expression.type !== "CallExpression") {
return true;
}
const hasUpdateOrDelete = nodes.some(
(token) =>
token.value === "updateTable" || token.value === "deleteFrom",
);
if (hasUpdateOrDelete) {
const hasWhere = nodes.some((token) => token.value === "where");
if (!hasWhere) {
const message = `Call chains containing "updateTable" or "deleteFrom" must also include a "where" clause.`;
context.report({
node: expression,
message,
});
}
}
}
},
};

View file

@ -1,26 +0,0 @@
const { RuleTester } = require("eslint");
const rule = require("./where");
const ruleTester = new RuleTester({
languageOptions: { ecmaVersion: 2015 },
});
ruleTester.run(
"enforce-where", // rule name
rule,
{
valid: [
{
code: "trx.updateTable('name').set({foo:bar}).where('something', '=', something).execute()",
},
],
invalid: [
{
code: "trx.updateTable('name').set({foo:bar}).execute()",
errors: 1,
},
],
},
);
console.log("EnforceWhere: All tests passed!");