Skip to content

feat: add slonik plugin#455

Open
Newbie012 wants to merge 1 commit intomainfrom
feat-slonik-plugin
Open

feat: add slonik plugin#455
Newbie012 wants to merge 1 commit intomainfrom
feat-slonik-plugin

Conversation

@Newbie012
Copy link
Collaborator

@Newbie012 Newbie012 commented Mar 22, 2026

closes #295
closes #286
closes #285

Summary by CodeRabbit

  • New Features

    • Added an experimental Slonik plugin with broad Slonik helper support, fragment inlining, Zod-based result validation, and a runnable demo workspace.
  • Bug Fixes

    • Improved SQL error positioning, sourcemap-aware error mapping, and refined plugin suggestion vs. auto-fix behavior.
  • Documentation

    • Expanded Slonik compatibility docs with setup, examples, a support matrix, and guidance on suggestion vs. fix modes.
  • Tests

    • Added extensive unit and integration tests covering plugin translation, inlining, sourcemaps, and lint behavior.
  • Chores

    • Updated build/tooling configs, dependency bumps, new package manifests, and a changeset for publishing.

@changeset-bot
Copy link

changeset-bot bot commented Mar 22, 2026

🦋 Changeset detected

Latest commit: 554251b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 24 packages
Name Type
@ts-safeql/eslint-plugin Patch
@ts-safeql/plugin-utils Patch
@ts-safeql/zod-annotator Patch
@ts-safeql/plugin-slonik Minor
@ts-safeql-demos/basic-flat-config Patch
@ts-safeql-demos/basic-migrations-raw Patch
@ts-safeql-demos/basic-transform-type Patch
@ts-safeql-demos/basic Patch
@ts-safeql-demos/big-project Patch
@ts-safeql-demos/config-file-flat-config Patch
@ts-safeql-demos/from-config-file Patch
@ts-safeql-demos/multi-connections Patch
@ts-safeql-demos/playground Patch
@ts-safeql-demos/plugin-aws-iam Patch
@ts-safeql-demos/plugin-slonik Patch
@ts-safeql-demos/postgresjs-custom-types Patch
@ts-safeql-demos/postgresjs-demo Patch
@ts-safeql-demos/vercel-postgres Patch
@ts-safeql/connection-manager Patch
@ts-safeql/plugin-auth-aws Patch
@ts-safeql/generate Patch
@ts-safeql/shared Patch
@ts-safeql/sql-ast Patch
@ts-safeql/test-utils Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Mar 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
safeql Ready Ready Preview, Comment Mar 23, 2026 9:43pm

@coderabbitai
Copy link

coderabbitai bot commented Mar 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds an experimental Slonik plugin package and demo, integrates plugin-driven expression mapping into the ESLint rule pipeline, improves sourcemap-aware error mapping/reporting, switches plugin resolution to synchronous loading, and expands tests, docs, and build configs.

Changes

Cohort / File(s) Summary
Slonik Plugin Core
packages/plugins/slonik/src/plugin.ts, packages/plugins/slonik/src/index.ts
New @ts-safeql/plugin-slonik with onTarget/onExpression handlers, Slonik helper→SQL mappings, fragment inlining, Zod validation hooks, and enforceType default suggest.
Slonik Package & Build
packages/plugins/slonik/package.json, packages/plugins/slonik/build.config.ts, packages/plugins/slonik/tsconfig.json, packages/plugins/slonik/src/ts-fixture/*
New package manifest, Unbuild config, tsconfig, and ts-fixture support for plugin package and tests.
Plugin Tests
packages/plugins/slonik/src/plugin.test.ts, packages/plugins/slonik/src/plugin.integration.test.ts
New unit and DB-backed integration tests exercising translation, fragment embedding, sql.type/typeAlias, enforce/fix behavior, and many Slonik helpers.
ESLint Rule & Mapping
packages/eslint-plugin/src/rules/check-sql.rule.ts, packages/eslint-plugin/src/utils/ts-pg.utils.ts, packages/eslint-plugin/src/rules/check-sql.utils.ts
Propagate plugin context through checking/reporting, accept plugins in mapTemplateLiteralToQueryText, call plugins for expression mapping, add plugin suggestion message, and improve sourcemap-aware error position mapping.
RuleTester & Fixtures
packages/eslint-plugin/src/rules/check-sql.test.ts, packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts, packages/eslint-plugin/src/rules/ts-fixture/skip-target-plugin.ts
Test harness updates (timeouts, Vitest wiring) and new fixture plugins (sourcemap, skip-target) plus test helpers for plugin scenarios.
Plugin Loader / Utils
packages/plugin-utils/src/resolve.ts, packages/plugin-utils/package.json, packages/plugin-utils/build.config.ts
Switch to synchronous module loading via loadModuleSync, add tsx handling for local requires, and mark tsx as external in build config.
Demos
demos/plugin-slonik/package.json, demos/plugin-slonik/eslint.config.js, demos/plugin-slonik/tsconfig.json, demos/plugin-slonik/src/index.ts
New demo workspace with ESLint flat config, tsconfig, build/lint scripts, and a comprehensive Slonik demo exercising plugin behaviors.
Docs & Minor fixes
docs/compatibility/slonik.md, docs/compatibility/postgres.js.md
Expanded Slonik compatibility docs with plugin usage, support matrix, enforcement notes; minor formatting fix in postgres.js.md.
Utility & Tests
packages/eslint-plugin/src/utils/ts-pg.utils.test.ts, packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts
Add tests for plugin placeholder replacement and new sourcemap test plugin.
Dependency bumps & Changeset
packages/eslint-plugin/package.json, packages/zod-annotator/package.json, .changeset/slonik-plugin-public.md
Bump zod devDependency to ^4.3.6 where applicable and add a changeset for publishing the Slonik plugin.
Build config updates
packages/plugin-utils/build.config.ts, packages/plugin-utils/package.json
Mark tsx external in build config and add tsx to plugin-utils dependencies.

Sequence Diagram(s)

sequenceDiagram
    participant ESLintRule as "ESLint Rule"
    participant ExprMapper as "Expression Mapper\n(mapTemplateLiteralToQueryText)"
    participant SlonikPlugin as "Slonik Plugin"
    participant Checker as "TypeChecker / DB (Zod/PG)"

    ESLintRule->>ExprMapper: mapTemplateLiteralToQueryText(template, plugins, sourceCode)
    activate ExprMapper
    loop each template expression
        ExprMapper->>SlonikPlugin: resolvePluginExpression(expr, tsNode, tsType, precedingSQL)
        activate SlonikPlugin
        alt plugin returns SQL string
            SlonikPlugin-->>ExprMapper: SQL fragment with $N placeholders
        else plugin returns false
            SlonikPlugin-->>ExprMapper: signal to skip validation
        else no plugin result
            SlonikPlugin-->>ExprMapper: undefined (continue default mapping)
        end
        deactivate SlonikPlugin
    end
    ExprMapper-->>ESLintRule: final queryText + sourcemap
    deactivate ExprMapper

    ESLintRule->>Checker: run type-check / zod validation (enforceType: fix|suggest)
    Checker-->>ESLintRule: errors, fixes or suggestions
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I nibble tokens, stitch them tight,

Fragments hum and types take flight,
Zod and Slonik hop in tune,
Suggestions whisper, fixes croon,
The rabbit stamps — the queries light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add slonik plugin' directly and accurately describes the primary change—adding a new Slonik plugin to the codebase.
Linked Issues check ✅ Passed The PR successfully addresses all three linked issues: #295 (Zod validator generation via sql.type with createZodAnnotator), #286 (conditional expression skipping via enforceType default 'suggest'), and #285 (fragment escape hatch via onTarget returning false for sql.fragment).
Out of Scope Changes check ✅ Passed All changes are scoped to Slonik plugin implementation, supporting infrastructure, and documentation updates. No unrelated refactoring or scope creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-slonik-plugin

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (9)
packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts (1)

17-19: Escape embedded quotes in generated identifier text.

return \"${argument.value}"`;can produce invalid SQL when the literal contains"`. Escaping makes this fixture safer and more representative.

Proposed tweak
-          if (argument?.type === "Literal" && typeof argument.value === "string") {
-            return `"${argument.value}"`;
+          if (argument?.type === "Literal" && typeof argument.value === "string") {
+            return `"${argument.value.replaceAll('"', '""')}"`;
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts` around lines
17 - 19, The current literal-handling branch returns `"${argument.value}"` which
can produce invalid SQL if the string contains double quotes; update the branch
that checks `argument?.type === "Literal" && typeof argument.value === "string"`
to escape embedded double quotes in `argument.value` before wrapping it in
quotes (e.g., replace `"` with `\"` or use a safe serializer such as
JSON.stringify(argument.value)) so the generated identifier text is valid and
safe.
packages/plugin-utils/src/resolve.ts (1)

74-87: resolveOne is now effectively synchronous; consider simplifying.

The async method now uses loadModuleSync and no longer performs any asynchronous operations. Consider either:

  1. Removing the async wrapper if callers can be updated
  2. Adding a comment explaining why the async signature is retained (e.g., backward compatibility)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-utils/src/resolve.ts` around lines 74 - 87, The resolveOne
method is marked async but performs only synchronous work (getCacheKey,
pluginCache.get/set, loadModuleSync, extractPlugin); remove the async wrapper
and change the signature to synchronous (drop async and return Promise/Sync type
as appropriate) or, if you must keep async for compatibility, add a concise
comment above resolveOne stating it intentionally remains async for backward
compatibility; update any callers if you choose to make it synchronous. Ensure
to reference and adjust usages of resolveOne, loadModuleSync, getCacheKey,
extractPlugin, and pluginCache to match the new synchronous signature or retain
the async wrapper with the explanatory comment.
packages/plugins/slonik/package.json (1)

39-41: Wide peer dependency range may need testing coverage.

The peer dependency specifies slonik >=38.0.0, but the dev dependency uses ^48.13.2. This means the plugin claims compatibility with slonik versions 38-47 that aren't being tested.

Consider either narrowing the peer dependency range to versions you've actually tested, or documenting known compatibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugins/slonik/package.json` around lines 39 - 41, The peer
dependency in package.json currently lists "slonik": ">=38.0.0" while
devDependencies only test "^48.13.2", which claims unsupported ranges; update
package.json to either narrow "peerDependencies.slonik" to the tested range
(e.g., "^48.13.2" or a compatible semver range you validated) or add testing
coverage/documentation for older 38–47 versions; ensure you modify the
package.json "peerDependencies" entry and, if choosing to support older
versions, add CI matrix entries and update README/compatibility notes
referencing "slonik" to reflect the validated versions.
demos/plugin-slonik/src/index.ts (1)

5-5: Empty connection string may cause runtime issues.

The pool is created with "postgres://" which has no host, port, or database specified. While this demo may never be executed (only linted), if someone tries to run it, it will fail.

Consider using a placeholder that's more obviously incomplete or adding a comment.

♻️ Suggested improvement
-const pool = await createPool("postgres://", { driverFactory: createPgDriverFactory() });
+// Demo file - not meant to be executed. Configure a real connection string if running.
+const pool = await createPool("postgres://user:pass@localhost:5432/db", { driverFactory: createPgDriverFactory() });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demos/plugin-slonik/src/index.ts` at line 5, The demo uses an empty
connection string ("postgres://") when constructing the pool via createPool with
createPgDriverFactory (variable pool), which will fail at runtime; replace the
empty URI with a clear placeholder (e.g. include host/port/db or a comment) or
add an inline comment next to the createPool call clarifying it’s intentionally
non-functional and must be replaced before running so readers won’t attempt to
run with an invalid connection string.
packages/plugins/slonik/src/plugin.ts (3)

91-98: sql.literalValue fallback returns empty string literal.

When sql.literalValue(...) is called with a non-string literal argument (e.g., a variable), the function returns '' (empty string literal). This may produce unexpected SQL behavior.

Consider returning false to skip the query when the literal value cannot be statically determined, similar to how sql.unnest handles dynamic cases.

♻️ Suggested fix
           if (isCallTo(node, "literalValue")) {
             const arg = node.arguments[0];
             if (arg?.type === "Literal" && typeof arg.value === "string") return `'${arg.value}'`;
-            return "''";
+            return false; // Cannot statically determine literal value
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugins/slonik/src/plugin.ts` around lines 91 - 98, The fallback for
sql.literalValue currently returns the empty string literal "''" which can
produce bad SQL when the argument isn't a static string; in the isCallTo(node,
"literalValue") branch (in package plugins/slonik/src/plugin.ts) change the
non-string/default return to false (matching how sql.unnest handles dynamic
cases) so callers skip or treat the value as dynamic instead of injecting an
empty literal; ensure isCallTo(..., "join") behavior remains unchanged.

181-196: Fragment variable lookup only searches immediate scope.

The findVariableInit function walks up to find the nearest Program or BlockStatement, then searches for variable declarations. This may miss variables declared in:

  • Parent function scopes
  • Module-level declarations when inside nested blocks
  • Destructured variables

For the common case of const where = sql.fragment\...`; sql.unsafe`... ${where}``, this works, but more complex patterns may fail silently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugins/slonik/src/plugin.ts` around lines 181 - 196,
findVariableInit only inspects declarations in the first Program or
BlockStatement it finds, missing variables in enclosing function/module scopes
and destructured declarators; update findVariableInit to walk ancestors up to
the Program node and at each ancestor that can contain statements (Program,
BlockStatement, FunctionDeclaration/FunctionExpression/ArrowFunction) examine
all statement lists (body, body.body or function.body) for VariableDeclaration
nodes, and for each VariableDeclarator handle id types Identifier, ObjectPattern
and ArrayPattern (recursively) to match the target id.name and return decl.init
when found; keep the function signature (findVariableInit(id:
TSESTree.Identifier)) and only return undefined if no matching declarator/init
is found after checking all ancestor scopes.

144-150: Module detection relies on path pattern matching that may fail across different environments.

The isSymbolFromModule function checks if sourceFile.fileName contains /${moduleName}/ using string matching. This has potential issues:

  • On Windows, TypeScript returns backslash-separated paths, so the forward-slash pattern won't match
  • Symlinked packages may resolve to unexpected paths depending on TypeScript's resolution strategy

Consider using path normalization (e.g., path.normalize() or path.sep) or a more robust comparison approach like checking resolved module paths directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugins/slonik/src/plugin.ts` around lines 144 - 150, The module
detection in isSymbolFromModule currently checks
sourceFile.fileName.includes(`/${moduleName}/`), which fails on Windows and
symlinked paths; update isSymbolFromModule to normalize paths and perform a
robust comparison: import path, call path.normalize(sourceFile.fileName) (or
split by path.sep) and then check for a segment equal to moduleName or use
path.join(path.sep, moduleName, path.sep) with path.sep, and also fallback to
comparing path.basename or resolved module paths if declarations provide them;
update the loop that iterates resolved.declarations and replace the string
literal `/${moduleName}/` check with the normalized/segmented comparison so it
works across platforms and symlinked resolutions.
packages/eslint-plugin/src/utils/ts-pg.utils.ts (1)

193-221: Last-plugin-wins semantics may be surprising.

The resolvePluginExpression function iterates through all plugins and keeps overwriting result whenever a plugin returns a non-undefined value. This means the last plugin to return a value "wins", which differs from typical "first match wins" plugin patterns.

Consider documenting this behavior or implementing early return when a plugin definitively handles an expression.

♻️ Alternative: first-plugin-wins semantics
 function resolvePluginExpression(params: {
   expression: TSESTree.Expression;
   checker: ts.TypeChecker;
   plugins: SafeQLPlugin[];
   precedingSQL: string;
   tsNode: ts.Node;
   tsType: ts.Type;
 }): string | false | undefined {
-  let result: string | false | undefined;
-
   for (const plugin of params.plugins) {
     const pluginResult = plugin.onExpression?.({
       node: params.expression,
       context: {
         precedingSQL: params.precedingSQL,
         checker: params.checker,
         tsNode: params.tsNode,
         tsType: params.tsType,
         tsTypeText: params.checker.typeToString(params.tsType),
       },
     });

     if (pluginResult !== undefined) {
-      result = pluginResult;
+      return pluginResult; // First plugin to handle wins
     }
   }

-  return result;
+  return undefined;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/eslint-plugin/src/utils/ts-pg.utils.ts` around lines 193 - 221, The
current resolvePluginExpression iterates all plugins and lets the last
non-undefined pluginResult win; change it to first-plugin-wins by returning
immediately when a plugin's onExpression returns a non-undefined value (i.e.,
inside resolvePluginExpression, after calling plugin.onExpression for each
plugin, if pluginResult !== undefined then return pluginResult). Make sure to
reference resolvePluginExpression and plugin.onExpression in the change and
preserve semantics for explicit false (still return false immediately).
packages/plugins/slonik/src/plugin.test.ts (1)

150-156: Consider simplifying the assertion branching.

The current logic checks for "skipped" in c.output first, then checks if c.output.sql.includes("/* skipped */"). However, when "skipped" in c.output is true, c.output is { skipped: true } and accessing .sql would be invalid. The current order prevents this, but the condition at line 152 could be clearer.

♻️ Suggested refactor for clarity
       // ASSERT
-      if ("skipped" in c.output) {
+      if ("skipped" in c.output && c.output.skipped === true) {
         expect(result).toEqual({ skipped: true });
-      } else if (c.output.sql.includes("/* skipped */")) {
+      } else if ("sql" in c.output && c.output.sql.includes("/* skipped */")) {
         expect(result).toMatchObject({ sql: expect.stringContaining("/* skipped */") });
       } else {
         expect(result).toEqual(c.output);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugins/slonik/src/plugin.test.ts` around lines 150 - 156, The
branching in the test should explicitly guard access to c.output.sql; update the
second condition to check that c.output has an sql property before calling
includes (e.g. use "if ('sql' in c.output && c.output.sql.includes('/* skipped
*/'))" or use optional chaining c.output.sql?.includes(...)) so the assertions
for result (checking { skipped: true }, sql containing "/* skipped */", or full
equality to c.output) remain correct and clear when running the test in
plugin.test.ts where c.output and result are compared.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/plugin-utils/package.json`:
- Around line 40-41: The package.json adds "tsx" as a runtime dependency but the
build config's externals list (in build.config.ts) doesn't include "tsx", so the
bundler will package it; update the externals array (the externals variable or
export where `@typescript-eslint/parser`, `@typescript-eslint/utils`, and typescript
are listed) to include "tsx" so it is treated as an external dependency and not
bundled into the dist output.

In `@packages/plugin-utils/src/resolve.ts`:
- Around line 93-97: The tsx.require call in the isLocalPath branch is using
pluginPath as both module ID and parent filename which is incorrect; update the
call in resolve.ts where you create tsx with
createRequire(import.meta.url)(`tsx/cjs/api`) so that you call
tsx.require(pluginPath, import.meta.url) instead of tsx.require(pluginPath,
pluginPath) to provide the correct ESM resolution context (use import.meta.url
as the second argument).

In `@packages/plugins/slonik/package.json`:
- Around line 2-4: Remove the "private": true field from the package.json for
the `@ts-safeql/plugin-slonik` package so it can be published; locate the
package.json entry containing "name": "@ts-safeql/plugin-slonik" and delete the
"private": true property, ensuring the version and other fields (version,
exports, types) remain unchanged so the package can be released via the monorepo
changesets workflow.

---

Nitpick comments:
In `@demos/plugin-slonik/src/index.ts`:
- Line 5: The demo uses an empty connection string ("postgres://") when
constructing the pool via createPool with createPgDriverFactory (variable pool),
which will fail at runtime; replace the empty URI with a clear placeholder (e.g.
include host/port/db or a comment) or add an inline comment next to the
createPool call clarifying it’s intentionally non-functional and must be
replaced before running so readers won’t attempt to run with an invalid
connection string.

In `@packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts`:
- Around line 17-19: The current literal-handling branch returns
`"${argument.value}"` which can produce invalid SQL if the string contains
double quotes; update the branch that checks `argument?.type === "Literal" &&
typeof argument.value === "string"` to escape embedded double quotes in
`argument.value` before wrapping it in quotes (e.g., replace `"` with `\"` or
use a safe serializer such as JSON.stringify(argument.value)) so the generated
identifier text is valid and safe.

In `@packages/eslint-plugin/src/utils/ts-pg.utils.ts`:
- Around line 193-221: The current resolvePluginExpression iterates all plugins
and lets the last non-undefined pluginResult win; change it to first-plugin-wins
by returning immediately when a plugin's onExpression returns a non-undefined
value (i.e., inside resolvePluginExpression, after calling plugin.onExpression
for each plugin, if pluginResult !== undefined then return pluginResult). Make
sure to reference resolvePluginExpression and plugin.onExpression in the change
and preserve semantics for explicit false (still return false immediately).

In `@packages/plugin-utils/src/resolve.ts`:
- Around line 74-87: The resolveOne method is marked async but performs only
synchronous work (getCacheKey, pluginCache.get/set, loadModuleSync,
extractPlugin); remove the async wrapper and change the signature to synchronous
(drop async and return Promise/Sync type as appropriate) or, if you must keep
async for compatibility, add a concise comment above resolveOne stating it
intentionally remains async for backward compatibility; update any callers if
you choose to make it synchronous. Ensure to reference and adjust usages of
resolveOne, loadModuleSync, getCacheKey, extractPlugin, and pluginCache to match
the new synchronous signature or retain the async wrapper with the explanatory
comment.

In `@packages/plugins/slonik/package.json`:
- Around line 39-41: The peer dependency in package.json currently lists
"slonik": ">=38.0.0" while devDependencies only test "^48.13.2", which claims
unsupported ranges; update package.json to either narrow
"peerDependencies.slonik" to the tested range (e.g., "^48.13.2" or a compatible
semver range you validated) or add testing coverage/documentation for older
38–47 versions; ensure you modify the package.json "peerDependencies" entry and,
if choosing to support older versions, add CI matrix entries and update
README/compatibility notes referencing "slonik" to reflect the validated
versions.

In `@packages/plugins/slonik/src/plugin.test.ts`:
- Around line 150-156: The branching in the test should explicitly guard access
to c.output.sql; update the second condition to check that c.output has an sql
property before calling includes (e.g. use "if ('sql' in c.output &&
c.output.sql.includes('/* skipped */'))" or use optional chaining
c.output.sql?.includes(...)) so the assertions for result (checking { skipped:
true }, sql containing "/* skipped */", or full equality to c.output) remain
correct and clear when running the test in plugin.test.ts where c.output and
result are compared.

In `@packages/plugins/slonik/src/plugin.ts`:
- Around line 91-98: The fallback for sql.literalValue currently returns the
empty string literal "''" which can produce bad SQL when the argument isn't a
static string; in the isCallTo(node, "literalValue") branch (in package
plugins/slonik/src/plugin.ts) change the non-string/default return to false
(matching how sql.unnest handles dynamic cases) so callers skip or treat the
value as dynamic instead of injecting an empty literal; ensure isCallTo(...,
"join") behavior remains unchanged.
- Around line 181-196: findVariableInit only inspects declarations in the first
Program or BlockStatement it finds, missing variables in enclosing
function/module scopes and destructured declarators; update findVariableInit to
walk ancestors up to the Program node and at each ancestor that can contain
statements (Program, BlockStatement,
FunctionDeclaration/FunctionExpression/ArrowFunction) examine all statement
lists (body, body.body or function.body) for VariableDeclaration nodes, and for
each VariableDeclarator handle id types Identifier, ObjectPattern and
ArrayPattern (recursively) to match the target id.name and return decl.init when
found; keep the function signature (findVariableInit(id: TSESTree.Identifier))
and only return undefined if no matching declarator/init is found after checking
all ancestor scopes.
- Around line 144-150: The module detection in isSymbolFromModule currently
checks sourceFile.fileName.includes(`/${moduleName}/`), which fails on Windows
and symlinked paths; update isSymbolFromModule to normalize paths and perform a
robust comparison: import path, call path.normalize(sourceFile.fileName) (or
split by path.sep) and then check for a segment equal to moduleName or use
path.join(path.sep, moduleName, path.sep) with path.sep, and also fallback to
comparing path.basename or resolved module paths if declarations provide them;
update the loop that iterates resolved.declarations and replace the string
literal `/${moduleName}/` check with the normalized/segmented comparison so it
works across platforms and symlinked resolutions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c7d9b820-bb1b-41a0-bc3b-79d65eb893bc

📥 Commits

Reviewing files that changed from the base of the PR and between 00b9904 and 741a4c3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (24)
  • demos/plugin-slonik/eslint.config.js
  • demos/plugin-slonik/package.json
  • demos/plugin-slonik/src/index.ts
  • demos/plugin-slonik/tsconfig.json
  • docs/compatibility/postgres.js.md
  • docs/compatibility/slonik.md
  • packages/eslint-plugin/package.json
  • packages/eslint-plugin/src/rules/check-sql.rule.ts
  • packages/eslint-plugin/src/rules/check-sql.test.ts
  • packages/eslint-plugin/src/rules/check-sql.utils.ts
  • packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts
  • packages/eslint-plugin/src/utils/ts-pg.utils.ts
  • packages/plugin-utils/package.json
  • packages/plugin-utils/src/resolve.ts
  • packages/plugins/slonik/build.config.ts
  • packages/plugins/slonik/package.json
  • packages/plugins/slonik/src/index.ts
  • packages/plugins/slonik/src/plugin.integration.test.ts
  • packages/plugins/slonik/src/plugin.test.ts
  • packages/plugins/slonik/src/plugin.ts
  • packages/plugins/slonik/src/ts-fixture/file.ts
  • packages/plugins/slonik/src/ts-fixture/tsconfig.json
  • packages/plugins/slonik/tsconfig.json
  • packages/zod-annotator/package.json

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts (1)

36-50: Consider simplifying the type guard.

The isCall function is defensively checking properties on unknown, but in the onExpression context, node is already typed as TSESTree.Expression. A more concise implementation could leverage this:

♻️ Simplified implementation using Expression type
-function isCall(node: unknown, name: string): node is TSESTree.CallExpression {
-  return (
-    typeof node === "object" &&
-    node !== null &&
-    "type" in node &&
-    node.type === "CallExpression" &&
-    "callee" in node &&
-    typeof node.callee === "object" &&
-    node.callee !== null &&
-    "type" in node.callee &&
-    node.callee.type === "Identifier" &&
-    "name" in node.callee &&
-    node.callee.name === name
-  );
+function isCall(node: TSESTree.Expression, name: string): node is TSESTree.CallExpression & { callee: TSESTree.Identifier } {
+  return (
+    node.type === "CallExpression" &&
+    node.callee.type === "Identifier" &&
+    node.callee.name === name
+  );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts` around lines
36 - 50, The isCall type-guard is overly defensive for callers that already pass
a TSESTree.Expression (e.g., onExpression); change the isCall signature to
accept node: TSESTree.Expression (or narrow inside callers) and simplify the
check to assert node.type === "CallExpression" && node.callee.type ===
"Identifier" && node.callee.name === name, returning the appropriate type
predicate (node is TSESTree.CallExpression) so callers like onExpression can
rely on the simpler guard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts`:
- Around line 15-20: The ident() handling currently returns the raw string
interpolation for argument.value which breaks if the value contains double
quotes; update the logic in the isCall(node, "ident") branch to escape embedded
double quotes by replacing each " with "" before wrapping the result in double
quotes so the returned value is a valid SQL identifier (locate the isCall(node,
"ident") block and the use of argument.value and change the string construction
to perform the quote-doubling escape).

---

Nitpick comments:
In `@packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts`:
- Around line 36-50: The isCall type-guard is overly defensive for callers that
already pass a TSESTree.Expression (e.g., onExpression); change the isCall
signature to accept node: TSESTree.Expression (or narrow inside callers) and
simplify the check to assert node.type === "CallExpression" && node.callee.type
=== "Identifier" && node.callee.name === name, returning the appropriate type
predicate (node is TSESTree.CallExpression) so callers like onExpression can
rely on the simpler guard.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3f192108-e3d2-4c92-b789-0f8764ef7b5b

📥 Commits

Reviewing files that changed from the base of the PR and between 741a4c3 and f7bd6cf.

📒 Files selected for processing (1)
  • packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts

@Newbie012 Newbie012 marked this pull request as draft March 22, 2026 22:13
@Newbie012 Newbie012 force-pushed the feat-slonik-plugin branch 3 times, most recently from b99d787 to a74de57 Compare March 23, 2026 00:19
@Newbie012 Newbie012 marked this pull request as ready for review March 23, 2026 00:20
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
packages/plugins/slonik/src/plugin.ts (2)

99-100: Clarify behavior when sql.array has a fragment type argument.

When the second argument is not a string literal (e.g., sql.fragment\int[]`), the code returns "$N"(untyped placeholder). The test at line 103-104 inplugin.test.ts` confirms this is intentional, but consider adding a comment explaining why fragment types can't be resolved statically.

📝 Suggested clarification
     case "array":
+      // String literal type → typed array cast; fragment type → untyped placeholder (can't resolve statically)
       return node.arguments[1]?.type === "Literal" ? `$N::${node.arguments[1].value}[]` : "$N";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugins/slonik/src/plugin.ts` around lines 99 - 100, Add a short
explanatory comment inside the switch case for "array" (around the code that
inspects node.arguments[1]) stating that when the second argument is not a
Literal (e.g., a sql.fragment like `sql.fragment\`int[]\``) we cannot resolve
the SQL type statically at compile time, so we intentionally fall back to the
untyped placeholder "$N"; reference the "array" case, node.arguments[1], and the
related test in plugin.test.ts to make the rationale clear for future readers.

159-175: Fragment detection heuristic is reasonable but may have edge cases.

The isFragmentTokenType function uses two approaches:

  1. Type text matching (FragmentSqlToken, SqlFragmentToken)
  2. Duck typing (has sql, values, type but not parser)

This covers common cases but could potentially match non-Slonik types with similar shapes. Consider documenting this limitation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugins/slonik/src/plugin.ts` around lines 159 - 175,
isFragmentTokenType currently identifies fragment tokens by matching tsTypeText
for FragmentSqlToken/SqlFragmentToken and by duck-typing the tsType for
properties "sql", "values", "type" and absence of "parser", which can produce
false positives for similarly-shaped non-Slonik types; update the code by adding
a clear explanatory comment above the isFragmentTokenType function that
documents this heuristic, lists the checked symbols (FragmentSqlToken,
SqlFragmentToken) and property-based checks ("sql", "values", "type", not
"parser"), and explicitly states the potential edge-case limitation and that
future changes should tighten detection if needed (e.g., using fully-qualified
type checks or additional distinctive markers).
packages/plugin-utils/src/resolve.ts (1)

62-84: resolveOne and resolveOneSync are now identical — consider consolidating.

Both methods perform the same synchronous logic after the change. resolveOne could simply delegate to resolveOneSync, or one of them could be removed entirely.

♻️ Suggested consolidation
-  private resolveOne(descriptor: PluginDescriptor, projectDir: string): SafeQLPlugin {
-    const key = this.getCacheKey(descriptor);
-    const cached = this.pluginCache.get(key);
-    if (cached) return cached;
-
-    const mod = this.loadModuleSync(descriptor.package, projectDir);
-
-    const plugin = this.extractPlugin(descriptor, mod);
-    this.pluginCache.set(key, plugin);
-    return plugin;
-  }
+  private resolveOne(descriptor: PluginDescriptor, projectDir: string): SafeQLPlugin {
+    return this.resolveOneSync(descriptor, projectDir);
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-utils/src/resolve.ts` around lines 62 - 84, Both resolveOne
and resolveOneSync contain identical synchronous logic; consolidate by having
one delegate to the other to avoid duplication. Modify the
resolveOne(descriptor, projectDir) implementation to simply return
this.resolveOneSync(descriptor, projectDir) (or vice versa), keeping
getCacheKey, loadModuleSync, extractPlugin, and pluginCache usage unchanged, and
remove the duplicated body so only one implementation contains the actual logic.
demos/plugin-slonik/src/index.ts (2)

5-5: Consider using a placeholder that clearly indicates this is a demo.

The empty connection string "postgres://" may cause confusion or runtime errors if someone tries to execute this file. Consider using a more explicit placeholder with a comment.

📝 Suggested improvement
-const pool = await createPool("postgres://", { driverFactory: createPgDriverFactory() });
+// Demo file — connection URL should be replaced with a real database for execution
+const pool = await createPool("postgres://user:password@localhost:5432/db", { driverFactory: createPgDriverFactory() });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demos/plugin-slonik/src/index.ts` at line 5, The connection string passed to
createPool (const pool = await createPool("postgres://", { driverFactory:
createPgDriverFactory() });) is an ambiguous empty URL; replace it with a clear
demo placeholder (e.g., "postgres://user:pass@localhost:5432/dbname" or
"postgres://<USER>:<PASS>@<HOST>:<PORT>/<DB>") and add a short inline comment
indicating it must be replaced in real usage so users don’t run the demo with an
invalid URI.

201-206: The section and example helper functions are no-ops.

These functions execute their callbacks but don't provide any reporting or grouping. If this is intentional for static analysis purposes only (ESLint checking), consider adding a comment to clarify.

📝 Suggested clarification
+// These helpers exist solely to organize code for ESLint/SafeQL static analysis.
+// They execute their callbacks immediately without additional runtime behavior.
 function section(_: string, fn: () => void) {
   fn();
 }
 function example(_: string, fn: () => void) {
   fn();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demos/plugin-slonik/src/index.ts` around lines 201 - 206, The helpers section
and example are currently no-ops (they simply call the callbacks) which is
confusing; either make them meaningful or explicitly document their no-op
intent: update the functions section and example to either implement simple
grouping/reporting (e.g., call console.group/console.groupEnd or forward to your
test/reporting utility around fn()) or add a clear explanatory comment/JSDoc
above each function stating they intentionally exist only for static
analysis/ESLint/test-structure and must not perform runtime behavior. Ensure you
reference the exact function names section and example when making the change so
reviewers can see the intended behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/eslint-plugin/src/utils/ts-pg.utils.ts`:
- Around line 56-68: The sourcemap entry pushed into sourcemaps uses
original.end: expression.range[1] - quasi.range[0] but the extracted text
includes the trailing '}' (sourceCode.text.slice uses expression.range[1] + 1),
so update the original.end calculation to expression.range[1] - quasi.range[0] +
1 to match the actual slice; locate the sourcemaps.push call that builds the
original object (references to sourcemaps, expression.range, quasi.range, and
the text slice) and adjust original.end accordingly so the sourcemap spans the
same characters as the extracted text.

---

Nitpick comments:
In `@demos/plugin-slonik/src/index.ts`:
- Line 5: The connection string passed to createPool (const pool = await
createPool("postgres://", { driverFactory: createPgDriverFactory() });) is an
ambiguous empty URL; replace it with a clear demo placeholder (e.g.,
"postgres://user:pass@localhost:5432/dbname" or
"postgres://<USER>:<PASS>@<HOST>:<PORT>/<DB>") and add a short inline comment
indicating it must be replaced in real usage so users don’t run the demo with an
invalid URI.
- Around line 201-206: The helpers section and example are currently no-ops
(they simply call the callbacks) which is confusing; either make them meaningful
or explicitly document their no-op intent: update the functions section and
example to either implement simple grouping/reporting (e.g., call
console.group/console.groupEnd or forward to your test/reporting utility around
fn()) or add a clear explanatory comment/JSDoc above each function stating they
intentionally exist only for static analysis/ESLint/test-structure and must not
perform runtime behavior. Ensure you reference the exact function names section
and example when making the change so reviewers can see the intended behavior.

In `@packages/plugin-utils/src/resolve.ts`:
- Around line 62-84: Both resolveOne and resolveOneSync contain identical
synchronous logic; consolidate by having one delegate to the other to avoid
duplication. Modify the resolveOne(descriptor, projectDir) implementation to
simply return this.resolveOneSync(descriptor, projectDir) (or vice versa),
keeping getCacheKey, loadModuleSync, extractPlugin, and pluginCache usage
unchanged, and remove the duplicated body so only one implementation contains
the actual logic.

In `@packages/plugins/slonik/src/plugin.ts`:
- Around line 99-100: Add a short explanatory comment inside the switch case for
"array" (around the code that inspects node.arguments[1]) stating that when the
second argument is not a Literal (e.g., a sql.fragment like
`sql.fragment\`int[]\``) we cannot resolve the SQL type statically at compile
time, so we intentionally fall back to the untyped placeholder "$N"; reference
the "array" case, node.arguments[1], and the related test in plugin.test.ts to
make the rationale clear for future readers.
- Around line 159-175: isFragmentTokenType currently identifies fragment tokens
by matching tsTypeText for FragmentSqlToken/SqlFragmentToken and by duck-typing
the tsType for properties "sql", "values", "type" and absence of "parser", which
can produce false positives for similarly-shaped non-Slonik types; update the
code by adding a clear explanatory comment above the isFragmentTokenType
function that documents this heuristic, lists the checked symbols
(FragmentSqlToken, SqlFragmentToken) and property-based checks ("sql", "values",
"type", not "parser"), and explicitly states the potential edge-case limitation
and that future changes should tighten detection if needed (e.g., using
fully-qualified type checks or additional distinctive markers).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0dc389cf-89fb-48fb-8584-a5391e77315b

📥 Commits

Reviewing files that changed from the base of the PR and between f7bd6cf and a74de57.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (26)
  • .changeset/slonik-plugin-public.md
  • demos/plugin-slonik/eslint.config.js
  • demos/plugin-slonik/package.json
  • demos/plugin-slonik/src/index.ts
  • demos/plugin-slonik/tsconfig.json
  • docs/compatibility/postgres.js.md
  • docs/compatibility/slonik.md
  • packages/eslint-plugin/package.json
  • packages/eslint-plugin/src/rules/check-sql.rule.ts
  • packages/eslint-plugin/src/rules/check-sql.test.ts
  • packages/eslint-plugin/src/rules/check-sql.utils.ts
  • packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts
  • packages/eslint-plugin/src/utils/ts-pg.utils.ts
  • packages/plugin-utils/build.config.ts
  • packages/plugin-utils/package.json
  • packages/plugin-utils/src/resolve.ts
  • packages/plugins/slonik/build.config.ts
  • packages/plugins/slonik/package.json
  • packages/plugins/slonik/src/index.ts
  • packages/plugins/slonik/src/plugin.integration.test.ts
  • packages/plugins/slonik/src/plugin.test.ts
  • packages/plugins/slonik/src/plugin.ts
  • packages/plugins/slonik/src/ts-fixture/file.ts
  • packages/plugins/slonik/src/ts-fixture/tsconfig.json
  • packages/plugins/slonik/tsconfig.json
  • packages/zod-annotator/package.json
✅ Files skipped from review due to trivial changes (15)
  • packages/plugins/slonik/src/index.ts
  • demos/plugin-slonik/tsconfig.json
  • .changeset/slonik-plugin-public.md
  • packages/plugin-utils/package.json
  • packages/zod-annotator/package.json
  • packages/plugins/slonik/src/ts-fixture/file.ts
  • packages/plugins/slonik/tsconfig.json
  • packages/plugin-utils/build.config.ts
  • packages/eslint-plugin/package.json
  • docs/compatibility/postgres.js.md
  • packages/plugins/slonik/build.config.ts
  • packages/plugins/slonik/src/ts-fixture/tsconfig.json
  • demos/plugin-slonik/package.json
  • packages/plugins/slonik/package.json
  • docs/compatibility/slonik.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • demos/plugin-slonik/eslint.config.js
  • packages/eslint-plugin/src/rules/check-sql.rule.ts
  • packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts
  • packages/plugins/slonik/src/plugin.integration.test.ts
  • packages/eslint-plugin/src/rules/check-sql.utils.ts

@greptile-apps
Copy link

greptile-apps bot commented Mar 23, 2026

Greptile Summary

This PR introduces the @ts-safeql/plugin-slonik package — a first-party SafeQL plugin that teaches the ESLint rule how to handle Slonik's SQL helper methods (sql.json, sql.jsonb, sql.unnest, sql.identifier, sql.literalValue, sql.join, sql.fragment, sql.type, etc.) and fragment inlining. It also adds an enforceType: "fix" | "suggest" connection option so that plugin-generated type fixes can be surfaced as auto-fixes or as editor suggestions (the Slonik plugin defaults to "suggest"). Underlying infrastructure changes include switching plugin loading from async import() to synchronous tsx.require for local TypeScript plugin files, and a rewrite of the source-map error-position logic for more precise error highlighting when plugin expressions expand to differently-sized SQL.

  • New @ts-safeql/plugin-slonik package — translates the full Slonik expression surface (json, jsonb, binary, date, timestamp, interval, uuid, array, unnest, identifier, literalValue, join, fragments) to SQL placeholders, and integrates @ts-safeql/zod-annotator for sql.type schema validation.
  • enforceType: "fix" | "suggest" — new connection-level option that routes plugin type errors to ESLint auto-fixes or editor suggestions respectively; the Slonik plugin sets "suggest" as its default via connectionDefaults.
  • Plugin loading now synchronousresolve.ts switches from await import() to require()/tsx.require() and exposes resolvePluginsSync; local .ts plugin paths are loaded via tsx/cjs/api (added as a plugin-utils dependency).
  • Improved error position mappingcheck-sql.utils.ts rewrites getQueryErrorPosition with a cleaner source-map lookup, a separate getSourceRange helper for positions between mapped ranges, and findNearestMatchIndex for syntax-error token highlighting.
  • loadModuleSync swallows the original error — any failure (missing package, TypeScript compile error, runtime crash) surfaces as "could not be loaded. Is it installed?". Preserving the cause with { cause } would significantly ease plugin debugging (P1).
  • Suggestion messageId reuses "pluginError" — ESLint displays this string as the suggestion action label in editors, so the quick-fix button reads like an error description rather than an action prompt. A dedicated messageId would improve the editor UX (P2).
  • Inconsistent plugin-resolution priorityresolvePluginExpression uses first-plugin-wins while resolvePluginTargetMatch uses last-plugin-wins; this inconsistency becomes observable when two plugins are active (P2).

Confidence Score: 4/5

  • PR is on the happy path to merge; one targeted fix to the error-swallowing in loadModuleSync would round it out.
  • The core plugin logic is well-structured and thoroughly tested with both unit and integration tests covering the full Slonik surface area. The enforceType branching logic and source-map offset arithmetic are correct. The only P1 issue (error cause swallowed in loadModuleSync) is a one-liner fix that does not affect correctness, only debuggability. The P2 items are cosmetic or only observable in multi-plugin setups.
  • packages/plugin-utils/src/resolve.ts — the loadModuleSync catch block should preserve the original error via { cause }.

Important Files Changed

Filename Overview
packages/plugins/slonik/src/plugin.ts New Slonik plugin implementing onTarget and onExpression hooks; translates Slonik helper calls (sql.json, sql.unnest, sql.identifier, etc.) to safe SQL placeholders, inlines fragments, and integrates Zod schema validation via sql.type. Symbol detection uses two heuristics (import source + path segment), and buildFragmentTemplateSql can recurse safely under TypeScript's initialiser constraints.
packages/plugin-utils/src/resolve.ts Moved from async dynamic import to synchronous require/tsx.require for plugin loading; adds isLocalPath helper to route local .ts plugin files through tsx/cjs/api. The single catch block swallows the original error, replacing it with a generic message that hides compile/runtime errors in plugin files (flagged as P1).
packages/eslint-plugin/src/rules/check-sql.rule.ts Adds `enforceType: "fix"
packages/eslint-plugin/src/rules/check-sql.utils.ts Rewrites getQueryErrorPosition with cleaner matchingSourceMap lookup and separate getSourceRange/getSourceLocation/findNearestMatchIndex helpers; adds syntax-error token search for more precise error highlighting. Logic for offset accumulation in getSourceRange is correct for ordered source maps.
packages/eslint-plugin/src/utils/ts-pg.utils.ts Integrates plugin expression resolution into the template-literal-to-query-text pipeline; adds resolvePluginExpression with first-wins semantics, inconsistent with resolvePluginTargetMatch's last-wins semantics in check-sql.rule.ts (flagged P2). Source-map entries for plugin-translated expressions are computed correctly.
packages/plugins/slonik/src/plugin.integration.test.ts Comprehensive integration tests covering sql.unsafe, sql.type, sql.fragment, expression helpers (json, jsonb, binary, date, timestamp, interval, uuid, array, unnest, identifier, literalValue, join), fragment embedding, and both suggest and fix enforcement modes. Good coverage of Slonik-specific scenarios.
packages/plugins/slonik/src/plugin.test.ts Unit tests for SQL translation using PluginTestDriver; covers the full set of Slonik helper call-to-SQL translations, fragment inlining (including nested and destructured fragments), aliased imports, and non-slonik tag rejection. Good negative test coverage.
packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts New test-only plugin demonstrating local TypeScript plugin loading via tsx. Implements ident, jsonb, and unnest2 translations to drive the new plugin position test cases in check-sql.test.ts.

Sequence Diagram

sequenceDiagram
    participant ESLint
    participant CheckSQLRule as check-sql.rule
    participant PluginManager
    participant SlonikPlugin as plugin-slonik
    participant TsPgUtils as ts-pg.utils
    participant DB as PostgreSQL

    ESLint->>CheckSQLRule: TaggedTemplateExpression node
    CheckSQLRule->>PluginManager: resolvePluginsSync(descriptors, projectDir)
    PluginManager->>SlonikPlugin: loadModuleSync / tsx.require
    SlonikPlugin-->>PluginManager: SafeQLPlugin { onTarget, onExpression, connectionDefaults }
    PluginManager-->>CheckSQLRule: SafeQLPlugin[]

    CheckSQLRule->>CheckSQLRule: applyPluginDefaults(connection, plugins)
    Note over CheckSQLRule: Merges connectionDefaults.enforceType="suggest"

    CheckSQLRule->>SlonikPlugin: onTarget({ node, context })
    SlonikPlugin-->>CheckSQLRule: { skipTypeAnnotations } | { typeCheck: zodTypeCheck } | false | undefined

    alt target not skipped
        CheckSQLRule->>TsPgUtils: mapTemplateLiteralToQueryText(quasi, checker, connection, plugins)
        loop each template expression
            TsPgUtils->>SlonikPlugin: onExpression({ node, context })
            SlonikPlugin-->>TsPgUtils: "$N::jsonb" | '"identifier"' | false | undefined
        end
        TsPgUtils-->>CheckSQLRule: { query, sourcemaps }

        CheckSQLRule->>DB: executeQuery(query)
        DB-->>CheckSQLRule: result or error

        alt Plugin typeCheck (sql.type)
            CheckSQLRule->>SlonikPlugin: typeCheck (zodTypeCheck)
            SlonikPlugin-->>CheckSQLRule: { message, fix } | undefined
            alt enforceType === "fix"
                CheckSQLRule->>ESLint: report({ fix })
            else enforceType === "suggest"
                CheckSQLRule->>ESLint: report({ suggest: [{ fix }] })
            end
        else SQL error
            CheckSQLRule->>ESLint: report({ loc: mappedPosition })
        end
    end
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/plugin-utils/src/resolve.ts
Line: 78-89

Comment:
**Original error swallowed in plugin loading**

The `catch` block discards the original error, replacing it with a generic message. This makes debugging very painful: if a local `.ts` plugin file has a TypeScript syntax error, a missing export, or a runtime crash, the user only sees "could not be loaded. Is it installed?" with no clue about what actually went wrong.

Use `{ cause }` to preserve the original error so it appears in stack traces:

```suggestion
  private loadModuleSync(packageName: string, projectDir: string): unknown {
    const projectRequire = createRequire(path.resolve(projectDir, "package.json"));

    try {
      if (isLocalPath(packageName)) {
        const pluginPath = path.resolve(projectDir, packageName);
        const tsx = createRequire(import.meta.url)(`tsx/cjs/api`);
        return tsx.require(pluginPath, import.meta.url);
      }

      return projectRequire(packageName);
    } catch (cause) {
      throw new Error(`SafeQL plugin "${packageName}" could not be loaded. Is it installed?`, {
        cause,
      });
    }
  }
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/eslint-plugin/src/rules/check-sql.rule.ts
Line: 291-302

Comment:
**Suggestion description reuses error `messageId`**

ESLint displays a suggestion's `messageId` text as the label for the quick-fix action button in editors (e.g., "💡 Expected { id: number, name: string }"). Reusing `"pluginError"` means the action label reads like an error description rather than an actionable prompt, which is confusing for users.

A dedicated suggestion message (e.g., `"pluginSuggestion"``"Apply suggested type fix"`) would make the IDE experience clearer. The rule `messages` map and the `RuleTester` expectations would need a matching update.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/eslint-plugin/src/utils/ts-pg.utils.ts
Line: 193-217

Comment:
**First-plugin-wins in `resolvePluginExpression` vs last-plugin-wins in `resolvePluginTargetMatch`**

`resolvePluginExpression` (this file) returns as soon as the first plugin returns a non-`undefined` result, while `resolvePluginTargetMatch` in `check-sql.rule.ts` keeps iterating and returns the *last* non-`undefined` result.

This inconsistency means adding a second plugin produces different override semantics depending on whether you're configuring a target or an expression. A deliberate, uniform priority model (always first-wins or always last-wins) with a comment explaining the design choice would make multi-plugin behaviour predictable.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: add Slonik plugin with Zod validat..." | Re-trigger Greptile

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/eslint-plugin/src/utils/ts-pg.utils.ts`:
- Around line 52-53: The current global replace on pluginExpression (producing
generatedText using pluginExpression.replace(/\$N/g, ...)) wrongly rewrites
literal SQL like sql.literalValue("$N"); change this to a token-aware
replacement that only renumbers real plugin placeholders: either (a) switch to a
sentinel that cannot occur in emitted SQL (e.g., a unique token inserted by the
plugin) and replace that sentinel, or (b) parse/scan pluginExpression for bare
$N tokens that are not inside string literals (use a simple tokenizer or regex
that tracks quote boundaries) and only replace those occurrences with
`$${++$idx}`. Update the logic around pluginExpression/generateText and keep
$idx increment behavior the same so only true plugin placeholders are renumbered
(ensure sql.literalValue("$N") remains untouched).

In `@packages/plugins/slonik/src/plugin.ts`:
- Around line 223-250: resolveIdentifierFragmentSql() and resolveFragmentSql()
can recurse indefinitely on alias cycles (e.g., const a = b; const b = a;); add
a visited Set<ts.Node | string> passed as an extra parameter to both functions,
mark each node before descending, and check the set to immediately return
false/undefined when a node is already visited; update all calls to
resolveFragmentSql(...) and resolveIdentifierFragmentSql(...) to pass the same
visited set so cycles are detected and recursion stops.
- Around line 59-61: translateCallExpression is currently dispatching on method
names alone and can rewrite non-Slonik helpers into tokens; to fix, update the
onExpression call sites (including the block in onExpression and the similar
block covering lines ~87–114) to pass context.tsNode and context.checker into
translateCallExpression, and inside translateCallExpression perform an early
guard using isSlonikMethod(tsNode, checker, node) (or equivalent) to ensure the
target object is a Slonik symbol before switching on method names; also mirror
the same Slonik symbol check used by resolveFragmentFactoryCall so only true
Slonik methods (e.g., jsonb, identifier, array) are translated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0ec97956-2eff-4d44-9c26-7fbc7500c39b

📥 Commits

Reviewing files that changed from the base of the PR and between a74de57 and ee7d695.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (27)
  • .changeset/slonik-plugin-public.md
  • demos/plugin-slonik/eslint.config.js
  • demos/plugin-slonik/package.json
  • demos/plugin-slonik/src/index.ts
  • demos/plugin-slonik/tsconfig.json
  • docs/compatibility/postgres.js.md
  • docs/compatibility/slonik.md
  • packages/eslint-plugin/package.json
  • packages/eslint-plugin/src/rules/check-sql.rule.ts
  • packages/eslint-plugin/src/rules/check-sql.test.ts
  • packages/eslint-plugin/src/rules/check-sql.utils.ts
  • packages/eslint-plugin/src/rules/ts-fixture/skip-target-plugin.ts
  • packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts
  • packages/eslint-plugin/src/utils/ts-pg.utils.ts
  • packages/plugin-utils/build.config.ts
  • packages/plugin-utils/package.json
  • packages/plugin-utils/src/resolve.ts
  • packages/plugins/slonik/build.config.ts
  • packages/plugins/slonik/package.json
  • packages/plugins/slonik/src/index.ts
  • packages/plugins/slonik/src/plugin.integration.test.ts
  • packages/plugins/slonik/src/plugin.test.ts
  • packages/plugins/slonik/src/plugin.ts
  • packages/plugins/slonik/src/ts-fixture/file.ts
  • packages/plugins/slonik/src/ts-fixture/tsconfig.json
  • packages/plugins/slonik/tsconfig.json
  • packages/zod-annotator/package.json
✅ Files skipped from review due to trivial changes (18)
  • demos/plugin-slonik/tsconfig.json
  • packages/zod-annotator/package.json
  • packages/plugins/slonik/src/ts-fixture/tsconfig.json
  • docs/compatibility/postgres.js.md
  • packages/plugins/slonik/src/ts-fixture/file.ts
  • packages/plugin-utils/package.json
  • packages/plugins/slonik/src/index.ts
  • packages/eslint-plugin/package.json
  • .changeset/slonik-plugin-public.md
  • packages/plugin-utils/build.config.ts
  • packages/plugins/slonik/build.config.ts
  • demos/plugin-slonik/package.json
  • demos/plugin-slonik/eslint.config.js
  • packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts
  • packages/plugins/slonik/src/plugin.test.ts
  • packages/plugins/slonik/tsconfig.json
  • packages/plugins/slonik/package.json
  • packages/plugins/slonik/src/plugin.integration.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/plugin-utils/src/resolve.ts
  • docs/compatibility/slonik.md
  • demos/plugin-slonik/src/index.ts
  • packages/eslint-plugin/src/rules/check-sql.rule.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/plugin-utils/src/resolve.ts (1)

62-68: ⚠️ Potential issue | 🟠 Major

Include the resolved local path in the cache key.

Local plugins are loaded relative to projectDir, but the cache key is still built from the raw descriptor.package string. In a monorepo, two packages that both use ./safeql-plugin.ts with the same config can reuse the first loaded module instead of resolving against their own package root. Because the ESLint rule keeps a module-scoped PluginManager, this leak survives across files in the same lint run.

🧩 One way to fix it
-  private getCacheKey(descriptor: PluginDescriptor): string {
-    return `${descriptor.package}:${stableStringify(descriptor.config ?? {})}`;
+  private getCacheKey(descriptor: PluginDescriptor, projectDir?: string): string {
+    const packageKey =
+      projectDir !== undefined && isLocalPath(descriptor.package)
+        ? path.resolve(projectDir, descriptor.package)
+        : descriptor.package;
+    return `${packageKey}:${stableStringify(descriptor.config ?? {})}`;
   }
-    const key = this.getCacheKey(descriptor);
+    const key = this.getCacheKey(descriptor, projectDir);

Apply the same normalization anywhere else that deletes from this cache.

Also applies to: 78-85

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-utils/src/resolve.ts` around lines 62 - 68, The cache key for
plugins must include the resolved local path so locally referenced plugins
loaded relative to different projectDir values don’t collide; in resolveOneSync
(and similarly resolveOneAsync / any delete-from-cache sites), call the resolver
that computes the actual module path (the same logic used by loadModuleSync or
loadModuleAsync) before constructing/looking up the cache key, and include that
resolved absolute path (or a normalized form of it) alongside
descriptor.package/config when calling getCacheKey or building the key; also
update any cache deletion code to use the same normalized resolved path so
add/remove use identical keys (refer to resolveOneSync, loadModuleSync and other
resolveOne* methods and the places that delete from pluginCache).
packages/eslint-plugin/src/rules/check-sql.rule.ts (1)

157-172: ⚠️ Potential issue | 🟠 Major

Don’t short-circuit every configured target once onTarget() matches.

This branch reports immediately for each configured target whenever a plugin returns a TargetMatch. With multiple targets, the same query gets checked more than once, and transforms/field transforms from non-matching targets can leak into the diagnostic.

🧭 One way to keep plugin context without duplicating reports
-  if (params.pluginCtx.targetMatch !== undefined) {
-    const target: ConnectionTarget = {
-      ...params.target,
-      skipTypeAnnotations:
-        params.pluginCtx.targetMatch.skipTypeAnnotations ?? params.target.skipTypeAnnotations,
-    };
-    return reportCheck({
-      context: params.context,
-      tag: params.tag,
-      connection: params.connection,
-      target,
-      projectDir: params.projectDir,
-      baseNode: params.tag.tag,
-      typeParameter: params.tag.typeArguments,
-      pluginCtx: params.pluginCtx,
-    });
-  }
+  const target: ConnectionTarget =
+    params.pluginCtx.targetMatch !== undefined
+      ? {
+          ...params.target,
+          skipTypeAnnotations:
+            params.pluginCtx.targetMatch.skipTypeAnnotations ?? params.target.skipTypeAnnotations,
+        }
+      : params.target;
 
-  if ("tag" in params.target) {
-    return checkConnectionByTagExpression({ ...params, target: params.target });
+  if ("tag" in target) {
+    return checkConnectionByTagExpression({ ...params, target });
   }
 
-  if ("wrapper" in params.target) {
-    return checkConnectionByWrapperExpression({ ...params, target: params.target });
+  if ("wrapper" in target) {
+    return checkConnectionByWrapperExpression({ ...params, target });
   }

Keep the synthetic no-target path in check(...) for plugin-only connections.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/eslint-plugin/src/rules/check-sql.rule.ts` around lines 157 - 172,
The current branch returns early when params.pluginCtx.targetMatch is set,
causing reportCheck to run immediately and duplicate checks across configured
targets; instead, do not return—merge the matched target info into the plugin
context or create a synthetic "no-target" ConnectionTarget and let the existing
check(...) flow handle reporting once. Concretely: in the block that currently
builds target from params.target and params.pluginCtx.targetMatch, update
params.pluginCtx (or a local pluginCtx copy) to include the resolved
targetMatch/skipTypeAnnotations and then call check(...) (or fall through to the
existing check path) rather than calling and returning reportCheck directly;
keep reportCheck only where the original no-target/reporting logic expects it so
transforms from non-matching targets cannot leak into diagnostics (refer to
params.pluginCtx.targetMatch, ConnectionTarget construction, reportCheck, and
the check(...) flow).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@demos/plugin-slonik/src/index.ts`:
- Around line 103-106: The schema for the "typed array" example is wrong: the
query SELECT ${sql.array([1, 2, 3], "int4")} AS a returns an array but the
validator uses z.number().nullable(). Update the schema in the example("typed
array") call to expect an array of numbers (e.g., change z.object({ a:
z.number().nullable() }) to z.object({ a: z.array(z.number()).nullable() }) or
z.object({ a: z.array(z.number()) }) as appropriate), leaving the pool.one,
sql.type, and sql.array(...) usage unchanged.
- Line 5: The current usage of pool (const pool = await createPool(...)) fires
queries with pool.one()/pool.query() inside section() and example() without
awaiting their async results, causing unhandled rejections and leaving the pool
open; update the section() and example() functions to accept and await async
callbacks (i.e., call await callback()), change all top-level
section()/example() invocations to be awaited via top-level await or an (async
() => { ... })() IIFE, and ensure you call await pool.end() in a finally block
after all examples complete so the connection pool is closed cleanly.

In `@packages/plugins/slonik/src/plugin.ts`:
- Around line 245-266: The bug is that the shared Set named visited is mutated
and reused across independent fragment resolutions (e.g., resolveFragmentSql and
resolveIdentifierFragmentSql), causing false cyclic detection when the same
fragment is referenced more than once; fix it by scoping the cycle-guard Set to
each independent resolution path: when calling resolveIdentifierFragmentSql or
recursing via resolveFragmentSql, either create a fresh Set for that top-level
resolution or push/pop markers (clone or copy the Set) before descending and
restore it afterwards so each descent has its own local visited context; update
calls in resolveFragmentSql, resolveIdentifierFragmentSql, and the code paths
handling VariableDeclaration/BindingElement initializers to pass or derive a
local visited instance instead of mutating a shared one.
- Around line 103-107: The translation currently accepts partially static token
arguments (in the "array" and "unnest" cases and in
translateIdentifierCall/translateUnnestCall), which can produce SQL that Slonik
will not execute; change these helpers to fail closed by returning false
whenever any required argument segment is not a static string or the exact
expected literal type — e.g., in the "array" case only accept when
node.arguments[1] is a Literal with a string type name and otherwise return
false, in the "unnest" case only accept when every type in the provided list is
a Literal string before calling translateUnnestCall, and in
translateIdentifierCall ensure every identifier path segment is a static string
(return false if any segment is dynamic); apply the same strict checks to the
other affected ranges (lines ~121-141 and ~419-425) so partially static
arguments are rejected rather than rewritten.
- Around line 206-207: joinIdentifierPath currently wraps each part in double
quotes but doesn't escape embedded double quotes, producing invalid SQL for
inputs like foo"bar; update joinIdentifierPath (function
joinIdentifierPath(parts: readonly string[])) to escape any " inside each part
by replacing " with "" (SQL standard doubling) before wrapping with surrounding
quotes, preserving the existing behavior of returning false for empty arrays.

---

Outside diff comments:
In `@packages/eslint-plugin/src/rules/check-sql.rule.ts`:
- Around line 157-172: The current branch returns early when
params.pluginCtx.targetMatch is set, causing reportCheck to run immediately and
duplicate checks across configured targets; instead, do not return—merge the
matched target info into the plugin context or create a synthetic "no-target"
ConnectionTarget and let the existing check(...) flow handle reporting once.
Concretely: in the block that currently builds target from params.target and
params.pluginCtx.targetMatch, update params.pluginCtx (or a local pluginCtx
copy) to include the resolved targetMatch/skipTypeAnnotations and then call
check(...) (or fall through to the existing check path) rather than calling and
returning reportCheck directly; keep reportCheck only where the original
no-target/reporting logic expects it so transforms from non-matching targets
cannot leak into diagnostics (refer to params.pluginCtx.targetMatch,
ConnectionTarget construction, reportCheck, and the check(...) flow).

In `@packages/plugin-utils/src/resolve.ts`:
- Around line 62-68: The cache key for plugins must include the resolved local
path so locally referenced plugins loaded relative to different projectDir
values don’t collide; in resolveOneSync (and similarly resolveOneAsync / any
delete-from-cache sites), call the resolver that computes the actual module path
(the same logic used by loadModuleSync or loadModuleAsync) before
constructing/looking up the cache key, and include that resolved absolute path
(or a normalized form of it) alongside descriptor.package/config when calling
getCacheKey or building the key; also update any cache deletion code to use the
same normalized resolved path so add/remove use identical keys (refer to
resolveOneSync, loadModuleSync and other resolveOne* methods and the places that
delete from pluginCache).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: adcbd1a5-a926-4616-99a0-9a2d2a77ac34

📥 Commits

Reviewing files that changed from the base of the PR and between ee7d695 and aa7f14c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • .changeset/slonik-plugin-public.md
  • demos/plugin-slonik/eslint.config.js
  • demos/plugin-slonik/package.json
  • demos/plugin-slonik/src/index.ts
  • demos/plugin-slonik/tsconfig.json
  • docs/compatibility/postgres.js.md
  • docs/compatibility/slonik.md
  • packages/eslint-plugin/package.json
  • packages/eslint-plugin/src/rules/check-sql.rule.ts
  • packages/eslint-plugin/src/rules/check-sql.test.ts
  • packages/eslint-plugin/src/rules/check-sql.utils.ts
  • packages/eslint-plugin/src/rules/ts-fixture/skip-target-plugin.ts
  • packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts
  • packages/eslint-plugin/src/utils/ts-pg.utils.test.ts
  • packages/eslint-plugin/src/utils/ts-pg.utils.ts
  • packages/plugin-utils/build.config.ts
  • packages/plugin-utils/package.json
  • packages/plugin-utils/src/resolve.ts
  • packages/plugins/slonik/build.config.ts
  • packages/plugins/slonik/package.json
  • packages/plugins/slonik/src/index.ts
  • packages/plugins/slonik/src/plugin.integration.test.ts
  • packages/plugins/slonik/src/plugin.test.ts
  • packages/plugins/slonik/src/plugin.ts
  • packages/plugins/slonik/src/ts-fixture/file.ts
  • packages/plugins/slonik/src/ts-fixture/tsconfig.json
  • packages/plugins/slonik/tsconfig.json
  • packages/zod-annotator/package.json
✅ Files skipped from review due to trivial changes (17)
  • docs/compatibility/postgres.js.md
  • demos/plugin-slonik/tsconfig.json
  • packages/plugins/slonik/src/ts-fixture/file.ts
  • packages/plugin-utils/package.json
  • packages/plugins/slonik/src/index.ts
  • packages/plugin-utils/build.config.ts
  • packages/plugins/slonik/tsconfig.json
  • packages/zod-annotator/package.json
  • packages/plugins/slonik/src/ts-fixture/tsconfig.json
  • demos/plugin-slonik/eslint.config.js
  • packages/plugins/slonik/build.config.ts
  • demos/plugin-slonik/package.json
  • .changeset/slonik-plugin-public.md
  • packages/plugins/slonik/package.json
  • packages/plugins/slonik/src/plugin.integration.test.ts
  • packages/eslint-plugin/package.json
  • packages/eslint-plugin/src/rules/check-sql.utils.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/eslint-plugin/src/rules/ts-fixture/skip-target-plugin.ts
  • packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/eslint-plugin/src/utils/ts-pg.utils.ts (1)

216-228: Consider PostgreSQL backslash-escape edge case in quoted literals.

The findQuotedLiteralEnd function handles SQL-standard doubled quotes ('', "") but not C-style backslash escapes (\'). When standard_conforming_strings is off (rare in modern PostgreSQL) or in E-prefixed strings (E'...'), a \' would prematurely end scanning.

This is a minor edge case since most codebases use standard-conforming strings, but worth noting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/eslint-plugin/src/utils/ts-pg.utils.ts` around lines 216 - 228, The
current quoted-literal scanning misses C-style backslash-escaped quotes (e.g.,
E'\\'') and will stop early; update the finder used by the callers
(findQuotedLiteralEnd) to treat a backslash before the quote character as an
escape by skipping the backslash and the escaped character instead of
terminating on the quote, i.e., when scanning inside findQuotedLiteralEnd handle
'\' followed by quoteChar (and '\\') as part of the literal, and ensure callers
(the text[cursor] checks) still call findQuotedLiteralEnd for both single and
double quotes so backslash-escaping is respected for E-prefixed/non-standard
strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/eslint-plugin/src/utils/ts-pg.utils.ts`:
- Around line 216-228: The current quoted-literal scanning misses C-style
backslash-escaped quotes (e.g., E'\\'') and will stop early; update the finder
used by the callers (findQuotedLiteralEnd) to treat a backslash before the quote
character as an escape by skipping the backslash and the escaped character
instead of terminating on the quote, i.e., when scanning inside
findQuotedLiteralEnd handle '\' followed by quoteChar (and '\\') as part of the
literal, and ensure callers (the text[cursor] checks) still call
findQuotedLiteralEnd for both single and double quotes so backslash-escaping is
respected for E-prefixed/non-standard strings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3f76bf11-4c2f-4643-8fee-62deb1273ab5

📥 Commits

Reviewing files that changed from the base of the PR and between ee7d695 and 554251b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • .changeset/slonik-plugin-public.md
  • demos/plugin-slonik/eslint.config.js
  • demos/plugin-slonik/package.json
  • demos/plugin-slonik/src/index.ts
  • demos/plugin-slonik/tsconfig.json
  • docs/compatibility/postgres.js.md
  • docs/compatibility/slonik.md
  • packages/eslint-plugin/package.json
  • packages/eslint-plugin/src/rules/check-sql.rule.ts
  • packages/eslint-plugin/src/rules/check-sql.test.ts
  • packages/eslint-plugin/src/rules/check-sql.utils.ts
  • packages/eslint-plugin/src/rules/ts-fixture/skip-target-plugin.ts
  • packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts
  • packages/eslint-plugin/src/utils/ts-pg.utils.test.ts
  • packages/eslint-plugin/src/utils/ts-pg.utils.ts
  • packages/plugin-utils/build.config.ts
  • packages/plugin-utils/package.json
  • packages/plugin-utils/src/resolve.ts
  • packages/plugins/slonik/build.config.ts
  • packages/plugins/slonik/package.json
  • packages/plugins/slonik/src/index.ts
  • packages/plugins/slonik/src/plugin.integration.test.ts
  • packages/plugins/slonik/src/plugin.test.ts
  • packages/plugins/slonik/src/plugin.ts
  • packages/plugins/slonik/src/ts-fixture/file.ts
  • packages/plugins/slonik/src/ts-fixture/tsconfig.json
  • packages/plugins/slonik/tsconfig.json
  • packages/zod-annotator/package.json
✅ Files skipped from review due to trivial changes (17)
  • packages/plugins/slonik/src/ts-fixture/file.ts
  • demos/plugin-slonik/tsconfig.json
  • packages/plugins/slonik/tsconfig.json
  • packages/plugin-utils/package.json
  • packages/eslint-plugin/package.json
  • packages/plugins/slonik/src/index.ts
  • packages/zod-annotator/package.json
  • docs/compatibility/postgres.js.md
  • packages/plugin-utils/build.config.ts
  • packages/plugins/slonik/src/ts-fixture/tsconfig.json
  • .changeset/slonik-plugin-public.md
  • demos/plugin-slonik/eslint.config.js
  • packages/plugins/slonik/build.config.ts
  • demos/plugin-slonik/package.json
  • docs/compatibility/slonik.md
  • packages/plugins/slonik/package.json
  • packages/plugin-utils/src/resolve.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/eslint-plugin/src/utils/ts-pg.utils.test.ts
  • packages/eslint-plugin/src/rules/ts-fixture/skip-target-plugin.ts
  • packages/eslint-plugin/src/rules/ts-fixture/sourcemap-plugin.ts
  • packages/eslint-plugin/src/rules/check-sql.rule.ts
  • packages/eslint-plugin/src/rules/check-sql.test.ts
  • packages/plugins/slonik/src/plugin.integration.test.ts
  • packages/eslint-plugin/src/rules/check-sql.utils.ts
  • packages/plugins/slonik/src/plugin.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant