diff --git a/eslint.config.ts b/eslint.config.ts index 89dae45c8..197ac78b4 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -42,6 +42,7 @@ export default defineConfig( { name: 'eslint overrides', rules: { + curly: ['error', 'all'], eqeqeq: ['error', 'always', { null: 'ignore' }], 'logical-assignment-operators': 'error', 'no-else-return': 'error', @@ -265,6 +266,192 @@ export default defineConfig( '@typescript-eslint/no-throw-literal': 'off', 'unicorn/no-array-reduce': 'off', }, - } + }, //#endregion + + { + name: "custom", + plugins: { + custom: { + rules: { + "no-arrow-parameter-types": { + meta: { + fixable: "code", + hasSuggestions: true, + type: "suggestion", + dialects: ["typescript"], + schema: [ + { + type: "object", + properties: { + allowOptional: { + type: "boolean", + default: false, + description: + "Allow type annotations when the parameter is optional. Sometimes useful for overloaded functions.", + }, + }, + }, + ], + defaultOptions: [ + { + allowOptional: false, + }, + ], + }, + create(context) { + const options = context.options[0] as { allowOptional: boolean }; + + return { + ArrowFunctionExpression(node) { + const paramsWithTypeAnnotation = node.params.filter( + ( + // @ts-expect-error: will be inferred when moved into an official plugin + param, + ) => param.typeAnnotation !== undefined, + ); + + const isCatchClause = + node.parent.callee?.property?.name === "catch"; + + if (paramsWithTypeAnnotation.length > 0 && !isCatchClause) { + for (const param of paramsWithTypeAnnotation) { + if (param.optional && options.allowOptional) { + continue; + } + + context.report({ + node: param, + message: + "Arrow function parameters should not have type annotations. Instead the Object where the operation is used should be typed correctly.", + fix(fixer) { + if (param.optional) { + return null; + } + + if ( + node.parent.type === "VariableDeclarator" && + !node.parent.id.typeAnnotation + ) { + const variableDeclarationNode = node.parent; + + const isAsyncFunction: boolean = node.async; + + const isBodyBlockStatement = + node.body.type === "BlockStatement"; + + const isBodyJSXElement = + node.body.type === "JSXElement"; + + const hasReturnType = node.returnType !== undefined; + + const lastParam = node.params.at(-1); + + const paramIdDifferentLine = + lastParam.loc.start.line !== + variableDeclarationNode.id.loc.end.line; + + const paramBlockDifferentLine = + lastParam.loc.end.line !== + node.body.loc.start.line; + + const behindClosingParenthesis = hasReturnType + ? (node.returnType.range[1] as number) + : (lastParam.range[1] as number) + ")".length; + + const fixes = [ + // Removes `=> ` + fixer.replaceTextRange( + [ + behindClosingParenthesis, + node.body.range[0] as number, + ], + !hasReturnType && + paramBlockDifferentLine && + paramIdDifferentLine + ? ")" + : "", + ), + // Removes ` = ` or ` = async ` + fixer.replaceTextRange( + [ + variableDeclarationNode.id.range[1] as number, + (variableDeclarationNode.init + .range[0] as number) + + (isAsyncFunction ? "async ".length : 0), + ], + "", + ), + // Replaces `const ` with `function ` or `async function ` + fixer.replaceTextRange( + [ + variableDeclarationNode.parent + .range[0] as number, + variableDeclarationNode.range[0] as number, + ], + isAsyncFunction + ? "async function " + : "function ", + ), + ]; + + // If the body is not a BlockStatement, we need to wrap it in curly braces + if (!isBodyBlockStatement) { + fixes.push( + fixer.insertTextBefore( + node.body, + `{return ${isBodyJSXElement ? "(" : ""}`, + ), + fixer.insertTextAfter( + node.body, + `${isBodyJSXElement ? ")" : ""}}`, + ), + ); + + if (isBodyJSXElement) { + fixes.push( + fixer.removeRange([ + node.body.range[1] as number, + node.range[1] as number, + ]), + ); + } + } + + return fixes; + } + + return fixer.removeRange( + param.typeAnnotation.range as [number, number], + ); + }, + suggest: [ + { + desc: "Remove type annotation", + fix(fixer) { + if (param.optional) { + return fixer.removeRange([ + (param.typeAnnotation.range[0] as number) - 1, // Remove the `?` before the type annotation + param.typeAnnotation.range[1] as number, + ]); + } + + return null; + }, + }, + ], + }); + } + } + }, + }; + }, + }, + }, + }, + }, + rules: { + "custom/no-arrow-parameter-types": ["error", { allowOptional: true }], + }, + }, ); diff --git a/src/db.ts b/src/db.ts index bc973915a..2b4b18570 100644 --- a/src/db.ts +++ b/src/db.ts @@ -106,7 +106,7 @@ export function db( }); const query: DBConnection['query'] = async ( - queryTextOrConfig: string | QueryConfig | QueryArrayConfig, + queryTextOrConfig, values?: any[] ): Promise => { await createConnection(); @@ -147,7 +147,7 @@ ${error} }; const select: DBConnection['select'] = async ( - queryTextOrConfig: string | QueryConfig | QueryArrayConfig, + queryTextOrConfig, values?: any[] ) => { const { rows } = await query(queryTextOrConfig, values); @@ -155,12 +155,15 @@ ${error} }; const column: DBConnection['column'] = async ( - columnName: string, - queryTextOrConfig: string | QueryConfig | QueryArrayConfig, + columnName, + queryTextOrConfig, values?: any[] ) => { - const rows = await select(queryTextOrConfig, values); - return rows.map((r: { [key: string]: any }) => r[columnName]); + const rows: Array<{ [key: string]: any }> = await select( + queryTextOrConfig, + values + ); + return rows.map((r) => r[columnName]); }; return { diff --git a/src/migrationBuilder.ts b/src/migrationBuilder.ts index d992f784d..2ed7d2f6c 100644 --- a/src/migrationBuilder.ts +++ b/src/migrationBuilder.ts @@ -825,9 +825,11 @@ export class MigrationBuilder { // This function wraps each operation within a function that either calls // the operation or its reverse, and appends the result // (array of sql statements) to the steps array - const wrap = - (operation: TOperation) => - (...args: Parameters) => { + const wrap: ( + operation: TOperation + ) => (...args: Parameters) => void = + (operation) => + (...args) => { if (this._REVERSE_MODE) { if (typeof operation.reverse !== 'function') { const name = `pgm.${operation.name}()`; @@ -970,9 +972,11 @@ export class MigrationBuilder { // Expose DB so we can access database within transaction /* eslint-disable @typescript-eslint/no-explicit-any */ - const wrapDB = - (operation: (...args: T) => TResult) => - (...args: T) => { + const wrapDB: ( + operation: (...args: T) => TResult + ) => (...args: T) => TResult = + (operation) => + (...args) => { if (this._REVERSE_MODE) { throw new Error('Impossible to automatically infer down migration'); } diff --git a/src/operations/operators/addToOperatorFamily.ts b/src/operations/operators/addToOperatorFamily.ts index 6e436a355..4d72f8e1f 100644 --- a/src/operations/operators/addToOperatorFamily.ts +++ b/src/operations/operators/addToOperatorFamily.ts @@ -12,9 +12,9 @@ export type AddToOperatorFamilyFn = ( export type AddToOperatorFamily = Reversible; -export const addToOperatorFamily = ( +export const addToOperatorFamily: ( mOptions: MigrationOptions -): AddToOperatorFamily => { +) => AddToOperatorFamily = (mOptions) => { const method: AddToOperatorFamily = ( operatorFamilyName, indexMethod, diff --git a/src/operations/operators/removeFromOperatorFamily.ts b/src/operations/operators/removeFromOperatorFamily.ts index ba3ca2240..0084fd7ee 100644 --- a/src/operations/operators/removeFromOperatorFamily.ts +++ b/src/operations/operators/removeFromOperatorFamily.ts @@ -9,9 +9,9 @@ export type RemoveFromOperatorFamily = ( operatorList: OperatorListDefinition[] ) => string; -export const removeFromOperatorFamily = ( +export const removeFromOperatorFamily: ( mOptions: MigrationOptions -): RemoveFromOperatorFamily => { +) => RemoveFromOperatorFamily = (mOptions) => { const method: RemoveFromOperatorFamily = ( operatorFamilyName, indexMethod, diff --git a/src/operations/schemas/createSchema.ts b/src/operations/schemas/createSchema.ts index 498aef8fd..f95efc18d 100644 --- a/src/operations/schemas/createSchema.ts +++ b/src/operations/schemas/createSchema.ts @@ -15,7 +15,7 @@ export type CreateSchemaFn = ( export type CreateSchema = Reversible; export function createSchema(mOptions: MigrationOptions): CreateSchema { - const _create: CreateSchema = (schemaName: string, options = {}) => { + const _create: CreateSchema = (schemaName, options = {}) => { const { ifNotExists = false, authorization } = options; const ifNotExistsStr = ifNotExists ? ' IF NOT EXISTS' : ''; diff --git a/src/operations/tables/shared.ts b/src/operations/tables/shared.ts index c9f16e55f..ffefdd635 100644 --- a/src/operations/tables/shared.ts +++ b/src/operations/tables/shared.ts @@ -439,10 +439,10 @@ export function parseLike( like: Name | { table: Name; options?: LikeOptions }, literal: Literal ): string { - const formatOptions = ( + const formatOptions: ( name: 'INCLUDING' | 'EXCLUDING', options?: Like | Like[] - ) => + ) => string = (name, options) => toArray(options) .filter((option): option is Like => option !== undefined) .map((option) => ` ${name} ${option}`) diff --git a/src/operations/triggers/createTrigger.ts b/src/operations/triggers/createTrigger.ts index 691489b2c..f7e88bd19 100644 --- a/src/operations/triggers/createTrigger.ts +++ b/src/operations/triggers/createTrigger.ts @@ -26,12 +26,14 @@ export type CreateTrigger = Reversible; export function createTrigger(mOptions: MigrationOptions): CreateTrigger { const _create: CreateTrigger = ( + /* eslint-disable custom/no-arrow-parameter-types */ tableName: Name, triggerName: string, triggerOptions: | (TriggerOptions & DropOptions) | (TriggerOptions & FunctionOptions & DropOptions), definition?: Value + /* eslint-enable custom/no-arrow-parameter-types */ ) => { const { constraint = false, diff --git a/test/ts/customRunner.ts b/test/ts/customRunner.ts index cbb2f2c04..14c12988c 100644 --- a/test/ts/customRunner.ts +++ b/test/ts/customRunner.ts @@ -13,7 +13,7 @@ type Options = | ({ databaseUrl: string } & TestOptions) | ({ dbClient: Client } & TestOptions); -export const run = async (options: Options): Promise => { +export const run: (options: Options) => Promise = async (options) => { const opts: Omit & Options = { migrationsTable: 'migrations', dir: resolve(import.meta.dirname, 'migrations'),