Skip to content

Commit

Permalink
Ensure prepare has unique name
Browse files Browse the repository at this point in the history
  • Loading branch information
oscarhermoso committed Mar 2, 2024
1 parent fed7bde commit 21aa5a4
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 26 deletions.
18 changes: 18 additions & 0 deletions eslint-plugin-drizzle/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,21 @@ const db = drizzle(...)
// ---> Will be triggered by ESLint Rule
db.update()
```


**enforce-prepare-has-unique-name**: Enforce using a unique name for the `.prepare()` statement. This is useful when you have multiple `.prepare()` statements in your codebase and want to avoid accidental runtime errors.

```json
"rules": {
"drizzle/enforce-prepare-has-unique-name": ["error"]
}
```

```ts
const db = drizzle(...)

db.select().from(table).prepare('query1');

// ---> Will be triggered by ESLint Rule
db.select().from(table).prepare('query1');
```
25 changes: 13 additions & 12 deletions eslint-plugin-drizzle/src/configs/all.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
export default {
env: {
es2024: true,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['drizzle'],
rules: {
'drizzle/enforce-delete-with-where': 'error',
'drizzle/enforce-update-with-where': 'error',
},
env: {
es2024: true,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['drizzle'],
rules: {
'drizzle/enforce-delete-with-where': 'error',
'drizzle/enforce-update-with-where': 'error',
'drizzle/enforce-prepare-has-unique-name': 'error',
},
};
25 changes: 13 additions & 12 deletions eslint-plugin-drizzle/src/configs/recommended.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
export default {
env: {
es2024: true,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['drizzle'],
rules: {
'drizzle/enforce-delete-with-where': 'error',
'drizzle/enforce-update-with-where': 'error',
},
env: {
es2024: true,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['drizzle'],
rules: {
'drizzle/enforce-delete-with-where': 'error',
'drizzle/enforce-update-with-where': 'error',
'drizzle/enforce-prepare-has-unique-name': 'error',
},
};
86 changes: 86 additions & 0 deletions eslint-plugin-drizzle/src/enforce-prepare-has-unique-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';
import type { Options } from './utils/options';

const createRule = ESLintUtils.RuleCreator(
() => 'https://github.com/drizzle-team/eslint-plugin-drizzle'
);
type MessageIds = 'enforcePrepareHasUniqueName';

interface QueryLocation {
preparedName: string;
node: TSESTree.CallExpression;
filePath: string;
line: number;
}

const updateRule = createRule<Options, MessageIds>({
defaultOptions: [{ drizzleObjectName: [] }],
name: 'enforce-prepare-has-unique-name',
meta: {
type: 'problem',
docs: {
description:
'Enforce that `prepare` method is called with a unique `name` to avoid a runtime error',
},
fixable: 'code',
messages: {
enforcePrepareHasUniqueName:
'Prepared statements `.prepare(...)` require a unique name. The name "{{preparedName}}" is also used at {{location}}',
},
schema: [
{
type: 'object',
properties: {
drizzleObjectName: {
type: ['string', 'array'],
},
},
additionalProperties: false,
},
],
},
create(context, _options) {
const preparedStatementNames = new Map<string, QueryLocation[]>();

return {
CallExpression(node) {
if (
node.callee.type === 'MemberExpression' &&
node.callee.property &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'prepare' &&
node.arguments.length === 1 &&
node.arguments[0]?.type === 'Literal'
) {
const preparedName = node.arguments[0].value as string;
const filePath = context.getFilename();
const line = node.loc ? node.loc.start.line : 0;

const collidingLocation = preparedStatementNames.get(preparedName);

if (collidingLocation) {
for (const location of collidingLocation) {
const messageData = {
location: `${location.filePath}:${location.line}`,
preparedName,
};
context.report({
node,
messageId: 'enforcePrepareHasUniqueName',
data: messageData,
});
}
collidingLocation.push({ preparedName, node, filePath, line });
} else {
preparedStatementNames.set(preparedName, [
{ preparedName, node, filePath, line },
]);
}
}
return;
},
};
},
});

export default updateRule;
6 changes: 4 additions & 2 deletions eslint-plugin-drizzle/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import all from './configs/all';
import recommended from './configs/recommended';
import deleteRule from './enforce-delete-with-where';
import updateRule from './enforce-update-with-where';
import prepareRule from './enforce-prepare-has-unique-name';
import type { Options } from './utils/options';

export const rules = {
'enforce-delete-with-where': deleteRule,
'enforce-update-with-where': updateRule,
'enforce-delete-with-where': deleteRule,
'enforce-update-with-where': updateRule,
'enforce-prepare-has-unique-name': prepareRule,
} satisfies Record<string, TSESLint.RuleModule<string, Options>>;

export const configs = { all, recommended };
Expand Down
96 changes: 96 additions & 0 deletions eslint-plugin-drizzle/tests/prepare.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// @ts-ignore
import { RuleTester } from '@typescript-eslint/rule-tester';

import rule from '../src/enforce-prepare-has-unique-name';

const ruleTester = new RuleTester();

ruleTester.run('enforce-prepare-has-unique-name', rule, {
valid: [
{
code: `
drizzle.select().from(table).prepare('query1');
drizzle.select().from(table).prepare('query2');
`,
},
{
code: `
db.select().from(table).prepare('query1');
db.select().from(table).prepare('query2');
`,
options: [{ drizzleObjectName: ['db'] }],
},
{
code: `
drizzle.select().from(table).prepare('query1');
db.select().from(table).prepare('query2');
`,
options: [{ drizzleObjectName: ['drizzle', 'db'] }],
},
],
invalid: [
{
code: `
drizzle.select().from(table).prepare('query1');
drizzle.select().from(table).prepare('query1');
`,
// test without options
errors: [
{
messageId: 'enforcePrepareHasUniqueName',
data: {
preparedName: 'query1',
location: 'file.ts:2',
},
},
],
},
{
code: `
drizzle.select().from(table).prepare('query1');
drizzle.select().from(table).prepare('query1');
`,
options: [{ drizzleObjectName: ['drizzle'] }],
errors: [
{
messageId: 'enforcePrepareHasUniqueName',
data: {
preparedName: 'query1',
location: 'file.ts:2',
},
},
],
},
{
code: `
db.select().from(table).prepare('query1');
db.select().from(table).prepare('query2');
db.select().from(table).prepare('query1');
`,
errors: [
{
messageId: 'enforcePrepareHasUniqueName',
data: {
preparedName: 'query1',
location: 'file.ts:2',
},
},
],
},
{
code: `
drizzle.select().from(table).prepare('query1');
db.select().from(table).prepare('query1');
`,
errors: [
{
messageId: 'enforcePrepareHasUniqueName',
data: {
preparedName: 'query1',
location: 'file.ts:2',
},
},
],
},
],
});

0 comments on commit 21aa5a4

Please sign in to comment.