Skip to content

Commit add31fc

Browse files
committed
feat: add parameter-prefix rule
1 parent c15cb9e commit add31fc

File tree

6 files changed

+152
-0
lines changed

6 files changed

+152
-0
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,21 @@ const user = db
133133
.all();
134134
```
135135
136+
### parameter-prefix
137+
138+
Enforce that all queries use the same prefix for named parameters.
139+
Can be configured to one of `:` (default), `@`, or `$`.
140+
141+
```ts
142+
// Bad
143+
/* eslint "sqlite/parameter-prefix": ["error", ":"] */
144+
db.prepare("SELECT * FROM users WHERE id = @id");
145+
146+
// Good
147+
/* eslint "sqlite/parameter-prefix": ["error", ":"] */
148+
db.prepare("SELECT * FROM users WHERE id = :id");
149+
```
150+
136151
## License
137152
138153
eslint-plugin-sqlite is licensed under the [MIT License](LICENSE) and

parser/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod nullable;
22
pub mod parameters;
3+
pub mod prefix;
34
pub mod query;

parser/src/prefix.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use fallible_iterator::FallibleIterator;
2+
use sqlite3_parser::{
3+
ast::{fmt::ToTokens, ParameterInfo},
4+
lexer::sql::Parser,
5+
};
6+
use wasm_bindgen::prelude::*;
7+
8+
#[wasm_bindgen]
9+
pub fn does_all_named_parameters_start_with_prefix(query: &str, prefix: char) -> Option<bool> {
10+
let mut parser = Parser::new(query.as_bytes());
11+
let cmd = parser.next().ok()??;
12+
13+
let mut parameters = ParameterInfo::default();
14+
15+
cmd.to_tokens(&mut parameters).ok()?;
16+
17+
Some(parameters.names.iter().all(|n| n.starts_with(prefix)))
18+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { TSESLint } from "@typescript-eslint/utils";
44
import SQLite from "better-sqlite3";
55

66
import { GetDatabaseOptions, RuleOptions } from "./ruleOptions.js";
7+
import { parameterPrefixRule } from "./rules/parameter-prefix.js";
78
import { typedInputRule } from "./rules/typed-input.js";
89
import { createTypedResultRule } from "./rules/typed-result.js";
910
import { createValidQueryRule } from "./rules/valid-query.js";
@@ -67,6 +68,7 @@ export function createSqlitePlugin(options: CreatePluginOptions) {
6768
"valid-query": createValidQueryRule(ruleOptions),
6869
"typed-result": createTypedResultRule(ruleOptions),
6970
"typed-input": typedInputRule,
71+
"parameter-prefix": parameterPrefixRule,
7072
},
7173
} satisfies TSESLint.FlatConfig.Plugin;
7274

src/rules/parameter-prefix.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
2+
3+
import { does_all_named_parameters_start_with_prefix } from "../parser/parser.js";
4+
import { getQueryValue } from "../utils.js";
5+
6+
export const parameterPrefixRule = ESLintUtils.RuleCreator.withoutDocs({
7+
create(context, options) {
8+
const expectedPrefix = options[0];
9+
10+
return {
11+
'CallExpression[callee.type=MemberExpression][callee.property.name="prepare"][arguments.length=1]'(
12+
node: Omit<TSESTree.CallExpression, "arguments" | "callee"> & {
13+
arguments: [TSESTree.CallExpression["arguments"][0]];
14+
callee: TSESTree.MemberExpression;
15+
},
16+
) {
17+
const arg = node.arguments[0];
18+
19+
const val = getQueryValue(arg, context.sourceCode.getScope(arg));
20+
21+
if (typeof val?.value !== "string") {
22+
return;
23+
}
24+
25+
const result = does_all_named_parameters_start_with_prefix(
26+
val.value,
27+
expectedPrefix,
28+
);
29+
30+
if (result === false) {
31+
context.report({
32+
node: arg,
33+
messageId: "incorrectPrefixUsed",
34+
data: {
35+
prefix: expectedPrefix,
36+
},
37+
});
38+
}
39+
},
40+
};
41+
},
42+
meta: {
43+
messages: {
44+
incorrectPrefixUsed:
45+
"Query uses a named parameter prefix that isn't permitted, use '{{prefix}}' instead.",
46+
},
47+
schema: [
48+
{
49+
type: "string",
50+
enum: [":", "@", "$"],
51+
},
52+
],
53+
type: "problem",
54+
},
55+
defaultOptions: [":"],
56+
});

tests/rules/parameter-prefix.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { parameterPrefixRule } from "../../src/rules/parameter-prefix.js";
2+
import { ruleTester } from "./rule-tester.js";
3+
4+
ruleTester.run("parameter-prefix", parameterPrefixRule, {
5+
valid: [
6+
{
7+
code: "db.prepare('SELECT * FROM foo WHERE id = :id')",
8+
},
9+
{
10+
code: "db.prepare('SELECT * FROM foo WHERE id = :id')",
11+
options: [":"],
12+
},
13+
{
14+
code: "db.prepare('SELECT * FROM foo WHERE id = @id')",
15+
options: ["@"],
16+
},
17+
{
18+
code: "db.prepare('SELECT * FROM foo WHERE id = $id')",
19+
options: ["$"],
20+
},
21+
],
22+
invalid: [
23+
{
24+
code: 'db.prepare("SELECT * FROM foo WHERE id = :id")',
25+
options: ["@"],
26+
errors: [
27+
{
28+
messageId: "incorrectPrefixUsed",
29+
data: {
30+
prefix: "@",
31+
},
32+
},
33+
],
34+
},
35+
{
36+
code: 'db.prepare("SELECT * FROM foo WHERE id = @id")',
37+
options: ["$"],
38+
errors: [
39+
{
40+
messageId: "incorrectPrefixUsed",
41+
data: {
42+
prefix: "$",
43+
},
44+
},
45+
],
46+
},
47+
{
48+
code: 'db.prepare("SELECT * FROM foo WHERE id = $id")',
49+
options: [":"],
50+
errors: [
51+
{
52+
messageId: "incorrectPrefixUsed",
53+
data: {
54+
prefix: ":",
55+
},
56+
},
57+
],
58+
},
59+
],
60+
});

0 commit comments

Comments
 (0)