Skip to content

Commit

Permalink
feat: support variable input parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
merceyz committed Sep 1, 2024
1 parent ed88d9f commit adfaab4
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 11 deletions.
4 changes: 2 additions & 2 deletions src/rules/typed-input.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ESLintUtils, TSESTree, ASTUtils } from "@typescript-eslint/utils";
import { RuleOptions } from "../ruleOptions.js";
import { stringifyNode } from "../utils.js";
import { getQueryValue, stringifyNode } from "../utils.js";
import { inferQueryInput, QueryInput } from "../inferQueryInput.js";

export function createTypedInputRule(options: RuleOptions) {
Expand All @@ -13,7 +13,7 @@ export function createTypedInputRule(options: RuleOptions) {
callee: TSESTree.MemberExpression;
},
) {
const val = ASTUtils.getStaticValue(
const val = getQueryValue(
node.arguments[0],
context.sourceCode.getScope(node.arguments[0]),
);
Expand Down
4 changes: 2 additions & 2 deletions src/rules/typed-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
inferQueryResult,
} from "../inferQueryResult.js";
import { RuleOptions } from "../ruleOptions.js";
import { stringifyNode } from "../utils.js";
import { getQueryValue, stringifyNode } from "../utils.js";

type ColumnInfoWithUserType = ColumnInfo & { userTSTypeAnnotation?: string };

Expand All @@ -19,7 +19,7 @@ export function createTypedResultRule(options: RuleOptions) {
callee: TSESTree.MemberExpression;
},
) {
const val = ASTUtils.getStaticValue(
const val = getQueryValue(
node.arguments[0],
context.sourceCode.getScope(node.arguments[0]),
);
Expand Down
9 changes: 3 additions & 6 deletions src/rules/valid-query.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ESLintUtils, TSESTree, ASTUtils } from "@typescript-eslint/utils";
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
import { RuleOptions } from "../ruleOptions.js";
import { stringifyNode } from "../utils.js";
import { getQueryValue, stringifyNode } from "../utils.js";

export function createValidQueryRule(options: RuleOptions) {
return ESLintUtils.RuleCreator.withoutDocs({
Expand All @@ -14,10 +14,7 @@ export function createValidQueryRule(options: RuleOptions) {
) {
const arg = node.arguments[0];

const val = ASTUtils.getStaticValue(
arg,
context.sourceCode.getScope(arg),
);
const val = getQueryValue(arg, context.sourceCode.getScope(arg));

if (!val) {
context.report({
Expand Down
102 changes: 101 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TSESTree } from "@typescript-eslint/utils";
import { ASTUtils, TSESLint, TSESTree } from "@typescript-eslint/utils";

export function stringifyNode(
node: TSESTree.Expression | TSESTree.PrivateIdentifier,
Expand All @@ -21,3 +21,103 @@ export function stringifyNode(
}
}
}

/**
* Attempts to get a query value from a given CallExpression argument.
*/
export function getQueryValue(
arg: TSESTree.CallExpression["arguments"][0],
scope: TSESLint.Scope.Scope,
): { value: unknown } | null {
const value = ASTUtils.getStaticValue(arg, scope);
if (value) {
return value;
}

if (arg.type === TSESTree.AST_NODE_TYPES.TemplateLiteral) {
if (arg.expressions.every((expr) => isVariableParameterExpression(expr))) {
return {
value: arg.quasis.map((quasi) => quasi.value.cooked).join("?"),
};
}

return null;
}

if (arg.type === TSESTree.AST_NODE_TYPES.Identifier) {
const variable = ASTUtils.findVariable(scope, arg);
const def = variable?.defs[0];
if (
variable?.defs.length === 1 &&
def?.type === TSESLint.Scope.DefinitionType.Variable &&
def.node.id.type === TSESTree.AST_NODE_TYPES.Identifier &&
def.parent.kind === "const" &&
def.node.init
) {
return getQueryValue(def.node.init, scope);
}
return null;
}

return null;
}

/**
* Checks if the expression looks like `foo.map(() => '?').join(',')`
*/
function isVariableParameterExpression(expr: TSESTree.Expression) {
if (expr.type !== TSESTree.AST_NODE_TYPES.CallExpression || expr.optional) {
return false;
}

if (expr.callee.type !== TSESTree.AST_NODE_TYPES.MemberExpression) {
return false;
}

if (!expr.arguments[0]) {
return false;
}

if (ASTUtils.getPropertyName(expr.callee) !== "join") {
return false;
}

const joinValue = ASTUtils.getStaticValue(expr.arguments[0]);
if (typeof joinValue?.value !== "string" || joinValue.value.trim() !== ",") {
return false;
}

if (
expr.callee.object.type !== TSESTree.AST_NODE_TYPES.CallExpression ||
expr.callee.object.optional
) {
return false;
}

const maybeMapExpr = expr.callee.object;

if (maybeMapExpr.callee.type !== TSESTree.AST_NODE_TYPES.MemberExpression) {
return false;
}

if (ASTUtils.getPropertyName(maybeMapExpr.callee) !== "map") {
return false;
}

if (!maybeMapExpr.arguments[0]) {
return false;
}

const maybeCallback = maybeMapExpr.arguments[0];

if (maybeCallback.type !== TSESTree.AST_NODE_TYPES.ArrowFunctionExpression) {
return false;
}

const mapValue = ASTUtils.getStaticValue(maybeCallback.body);
if (typeof mapValue?.value !== "string" || mapValue.value.trim() !== "?") {
return false;
}

return true;
}
8 changes: 8 additions & 0 deletions tests/rules/typed-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ ruleTester.run("typed-result", rule, {
'db.prepare<[unknown, {"userID": unknown}]>("SELECT * FROM users WHERE id = :userID or name = ?")',
'db.prepare<[unknown, {"userID": string}]>("SELECT * FROM users WHERE id = :userID or name = ?")',
'db.prepare<{"userID": string}>("SELECT * FROM users WHERE id = :userID")',
'db.prepare<[unknown]>(`SELECT * FROM users WHERE id IN (${foo.map(() => "?").join(",")})`)',
],
invalid: [
// No parameters
Expand Down Expand Up @@ -151,5 +152,12 @@ ruleTester.run("typed-result", rule, {
'db.prepare<{"userID": string}>("SELECT * FROM users WHERE id = :userID")',
errors: [{ messageId: "incorrectInputType" }],
},
// Variable input parameters
{
code: 'db.prepare(`SELECT * FROM users WHERE id IN (${foo.map(() => "?").join(",")})`)',
output:
'db.prepare<[unknown]>(`SELECT * FROM users WHERE id IN (${foo.map(() => "?").join(",")})`)',
errors: [{ messageId: "missingInputType" }],
},
],
});
8 changes: 8 additions & 0 deletions tests/rules/typed-result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ ruleTester.run("typed-result", rule, {
`db.prepare<[], {"name": number | string | Buffer | null}>("SELECT name FROM test")`,
`db.prepare<[]>("DELETE FROM foo")`,
`db.prepare<[], {"random()": (foo | number), "id": number}>("SELECT random(), id FROM users")`,
'db.prepare<[], {"name": string}>(`SELECT name FROM users WHERE id IN (${foo.map(() => "?").join(",")})`)',
],
invalid: [
// Query as string Literal
Expand Down Expand Up @@ -189,5 +190,12 @@ ruleTester.run("typed-result", rule, {
errors: [{ messageId: "incorrectResultType" }],
output: `db.prepare<[], {"random()": (foo | number), "id": number}>("SELECT random(), id FROM users")`,
},
// Variable input parameters
{
code: 'db.prepare(`SELECT name FROM users WHERE id IN (${foo.map(() => "?").join(",")})`)',
output:
'db.prepare<[], {"name": string}>(`SELECT name FROM users WHERE id IN (${foo.map(() => "?").join(",")})`)',
errors: [{ messageId: "missingResultType" }],
},
],
});
24 changes: 24 additions & 0 deletions tests/rules/valid-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ ruleTester.run("valid-query", rule, {
"db_users.prepare('SELECT * FROM users')",
"nested.db.users.prepare('SELECT * FROM users')",
"db.prepare('DELETE FROM foo')",
"db_users.prepare(`SELECT * FROM users WHERE id IN (${ids.map(() => '?').join(',')})`);",
"const query = `SELECT * FROM users WHERE id IN (${ids.map(() => '?').join(',')})`;db_users.prepare(query);",
],
invalid: [
{
Expand Down Expand Up @@ -88,5 +90,27 @@ ruleTester.run("valid-query", rule, {
},
],
},
{
code: "db_users.prepare(`SELECT * FROM user WHERE id IN (${ids.map(() => '?').join(',')})`);",
errors: [
{
messageId: "invalidQuery",
data: {
message: `no such table: user`,
},
},
],
},
{
code: "const query = `SELECT * FROM user WHERE id IN (${ids.map(() => '?').join(',')})`;db_users.prepare(query);",
errors: [
{
messageId: "invalidQuery",
data: {
message: `no such table: user`,
},
},
],
},
],
});

0 comments on commit adfaab4

Please sign in to comment.