Skip to content

Commit 78cea08

Browse files
committed
fix(scripts): audit drizzle sql tags bound through a dynamic import
1 parent d828a11 commit 78cea08

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

scripts/check-sql-date-binding.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,36 @@ describe('sql Date binding audit', () => {
255255

256256
expect(violations.map((violation) => violation.expression)).toEqual(['since'])
257257
})
258+
259+
test('resolves a drizzle tag imported dynamically', () => {
260+
const violations = findSqlDateBindingViolations(`
261+
import { and } from 'drizzle-orm'
262+
async function scan() {
263+
const { sql } = await import('drizzle-orm')
264+
const { sql: raw } = await import('drizzle-orm')
265+
const namespace = await import('drizzle-orm')
266+
const now = new Date()
267+
return [
268+
sql\`a < \${now}\`,
269+
raw\`b < \${now}\`,
270+
namespace.sql\`c < \${now}\`,
271+
]
272+
}
273+
`)
274+
275+
expect(violations.map((violation) => violation.expression)).toEqual(['now', 'now', 'now'])
276+
})
277+
278+
test('ignores a postgres-js client built from a dynamic import', () => {
279+
const violations = findSqlDateBindingViolations(`
280+
async function scan() {
281+
const { default: postgres } = await import('postgres')
282+
const sql = postgres(process.env.DATABASE_URL as string)
283+
const now = new Date()
284+
return sql\`a < \${now}\`
285+
}
286+
`)
287+
288+
expect(violations).toEqual([])
289+
})
258290
})

scripts/check-sql-date-binding.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,12 +240,68 @@ function collectSqlBindings(program: SyntaxNode): SqlBindings {
240240
}
241241
}
242242
}
243+
if (node.type === 'VariableDeclarator' && isDrizzleImportCall(node.init))
244+
bindDynamicImport(node.id, bindings)
245+
243246
for (const child of getChildNodes(node)) visit(child)
244247
}
245248
visit(program)
246249
return bindings
247250
}
248251

252+
/**
253+
* `import('drizzle-orm')`, with or without an `await`.
254+
*
255+
* Babel parses a dynamic import as a `CallExpression` whose callee is `Import`;
256+
* the `ImportExpression` spelling is accepted too so a parser upgrade cannot
257+
* silently reopen the hole this closes.
258+
*/
259+
function isDrizzleImportCall(node: unknown): boolean {
260+
if (!isSyntaxNode(node)) return false
261+
const current = node.type === 'AwaitExpression' ? unwrapAwait(node) : node
262+
if (!isSyntaxNode(current)) return false
263+
const isImport =
264+
current.type === 'ImportExpression' ||
265+
(current.type === 'CallExpression' &&
266+
isSyntaxNode(current.callee) &&
267+
current.callee.type === 'Import')
268+
if (!isImport) return false
269+
const args = Array.isArray(current.arguments) ? current.arguments : []
270+
const source = isSyntaxNode(current.source) ? current.source : args.find(isSyntaxNode)
271+
const value = source?.value
272+
return (
273+
typeof value === 'string' &&
274+
(value === DRIZZLE_MODULE || value.startsWith(`${DRIZZLE_MODULE}/`))
275+
)
276+
}
277+
278+
const unwrapAwait = (node: SyntaxNode): unknown =>
279+
isSyntaxNode(node.argument) ? node.argument : undefined
280+
281+
/**
282+
* Binds `const { sql } = await import('drizzle-orm')` and its namespace form.
283+
*
284+
* Without this a file importing the tag dynamically resolves no tag at all, so
285+
* the whole file is skipped rather than audited — a silent hole, not a warning.
286+
*/
287+
function bindDynamicImport(target: unknown, bindings: SqlBindings): void {
288+
if (!isSyntaxNode(target)) return
289+
if (target.type === 'Identifier' && typeof target.name === 'string') {
290+
bindings.namespaces.add(target.name)
291+
return
292+
}
293+
if (target.type !== 'ObjectPattern' || !Array.isArray(target.properties)) return
294+
for (const property of target.properties) {
295+
if (!isSyntaxNode(property) || property.type !== 'ObjectProperty') continue
296+
const key = isSyntaxNode(property.key) ? property.key.name : undefined
297+
if (key !== 'sql') continue
298+
const raw = isSyntaxNode(property.value) ? property.value : undefined
299+
const local = raw?.type === 'AssignmentPattern' && isSyntaxNode(raw.left) ? raw.left : raw
300+
if (local?.type === 'Identifier' && typeof local.name === 'string')
301+
bindings.tags.add(local.name)
302+
}
303+
}
304+
249305
/** `sql`, an aliased import of it, or `namespace.sql`. */
250306
function isSqlReference(node: unknown, bindings: SqlBindings): boolean {
251307
if (!isSyntaxNode(node)) return false

0 commit comments

Comments
 (0)