@nitra/cursor 1.8.159 → 1.8.161
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/mdc/js-bun-db.mdc +55 -6
- package/package.json +1 -1
- package/scripts/check-js-bun-db.mjs +36 -13
- package/scripts/utils/ast-scan-utils.mjs +23 -0
- package/scripts/utils/bun-sql-scan.mjs +117 -19
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.8.161] - 2026-05-01
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `js-bun-db.mdc` (v1.5): нова секція «Прибирати pg-leftover виклики (`.connect()`, `.end()`)». У файлах з Bun SQL прапоруються `<obj>.connect(...)` і `<obj>.end(...)` як ручний lifecycle, який Bun SQL робить за тебе. Opt-out — маркер `// allow-pg-leftover: <причина>` (line- або block-коментар на тому ж рядку чи безпосередньо перед викликом).
|
|
12
|
+
- `bun-sql-scan.mjs`: новий сканер `findBunSqlPgLeftoverCallInText` (скоп — лише файли з `import { sql|SQL } from 'bun'`, щоб не давати false-positive на WebSocket/Stream `.end()`). Виділено спільний `hasMarkerCommentNear` для обох opt-in маркерів (`allow-unsafe`, `allow-pg-leftover`).
|
|
13
|
+
|
|
14
|
+
## [1.8.160] - 2026-05-01
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `js-bun-db.mdc` (v1.4): `sql.unsafe(...)` тепер заборонено за замовчуванням — допустимо лише для підстановки назви таблиці/колонки чи dynamic SQL/DDL з code-controlled значенням; інакше переробляємо на tagged template `sql\`...${value}...\``. Кожен легітимний виклик має супроводжуватись маркером `// allow-unsafe: <причина>` на тому ж рядку або рядком вище.
|
|
19
|
+
- `check-js-bun-db.mjs`: замість вузької перевірки `sql.unsafe(\`...${expr}...\`)` тепер сканер `findBunSqlUnsafeUseWithoutAllowMarkerInText` падає на будь-якому `<obj>.unsafe(...)` без маркера-коментаря з непорожньою причиною (line- або block-коментар на тому ж рядку чи безпосередньо перед викликом).
|
|
20
|
+
- `ast-scan-utils.mjs`: додано `parseProgramAndCommentsOrNull` — окремий вхід для перевірок, яким потрібні коментарі поряд з AST.
|
|
21
|
+
|
|
7
22
|
## [1.8.159] - 2026-05-01
|
|
8
23
|
|
|
9
24
|
### Added
|
package/mdc/js-bun-db.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Використання pg / mysql2 / Bun SQL у Node.js та Bun
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.5'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Підтримувані версії баз даних
|
|
@@ -116,22 +116,71 @@ await sql.begin(async tx => {
|
|
|
116
116
|
await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
##
|
|
119
|
+
## `sql.unsafe(...)` за замовчуванням заборонено
|
|
120
|
+
|
|
121
|
+
Будь-який виклик `sql.unsafe(...)` (так само `tx.unsafe(...)` всередині `sql.begin`) **заборонено**, окрім випадків, коли **обидві** умови виконані:
|
|
122
|
+
|
|
123
|
+
1. значення підставляється з **коду** — константа, конфіг, whitelist; **не з user input**;
|
|
124
|
+
2. треба підставити те, що **не можна параметризувати** через tagged template:
|
|
125
|
+
- назву **таблиці**,
|
|
126
|
+
- назву **колонки**,
|
|
127
|
+
- **dynamic SQL / DDL** (`CREATE`, `ALTER`, `DROP`, multi-statement migration, серверні `SET`/`SHOW` і подібне).
|
|
128
|
+
|
|
129
|
+
В усіх інших випадках — переробити на звичайний tagged template `sql\`...\${value}...\``: значення біндяться як параметри й injection не лишається.
|
|
120
130
|
|
|
121
|
-
|
|
131
|
+
Кожен легітимний `sql.unsafe(...)` має супроводжуватись **маркером-коментарем** з причиною — на тому ж рядку (trailing) або на рядку безпосередньо перед викликом. Маркер — opt-in для перевірки `js-bun-db` і слід для ревʼюера:
|
|
122
132
|
|
|
123
133
|
```javascript
|
|
124
|
-
//
|
|
134
|
+
// allow-unsafe: DDL — назву таблиці параметризувати не можна
|
|
135
|
+
await sql.unsafe(`CREATE TABLE ${tableName} (id int)`)
|
|
136
|
+
|
|
137
|
+
await sql.unsafe('SELECT pg_advisory_lock($1)', [lockId]) // allow-unsafe: pg_advisory_lock — окремий шлях, без tagged template
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Формат маркера: `allow-unsafe: <непорожня причина>` у line- або block-коментарі. Без причини (`// allow-unsafe:`) і без маркера взагалі — **fail** перевірки.
|
|
141
|
+
|
|
142
|
+
❌ Заборонені кейси (треба переробити на tagged template):
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
// ❌ дані від користувача — параметризуй через tagged template
|
|
125
146
|
await sql.unsafe(`SELECT * FROM users WHERE id = ${userId}`)
|
|
126
147
|
|
|
127
148
|
// ❌ навіть у tagged template — динамічний список через .join(',')
|
|
128
149
|
await sql`SELECT * FROM users WHERE id IN (${ids.join(',')})`
|
|
129
150
|
```
|
|
130
151
|
|
|
131
|
-
`sql.unsafe(text, params)` допустимий лише для **статичного** SQL без даних від користувача (наприклад, разовий DDL-скрипт), і обов'язково з масивом параметрів — ніяких `${...}` у самому рядку.
|
|
132
|
-
|
|
133
152
|
Для динамічних списків — `sql([...])` або `sql(rows, 'colA', 'colB')`, **не** `.join(',')`.
|
|
134
153
|
|
|
154
|
+
## Прибирати pg-leftover виклики (`.connect()`, `.end()`)
|
|
155
|
+
|
|
156
|
+
У файлах з Bun SQL (`import { sql, SQL } from 'bun'`) залишки від `pg` — `pool.connect()`, `client.end()`, `pool.end()` — мають бути видалені. Bun SQL пулом керує сам: на першому запиті підключається, idle/lifetime закриває за конфігом — окремий життєвий цикл вручну не потрібен.
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
// ❌ pg-leftover: ручний lifecycle, який Bun SQL робить за тебе
|
|
160
|
+
const client = await pool.connect()
|
|
161
|
+
try {
|
|
162
|
+
await client.query('...')
|
|
163
|
+
} finally {
|
|
164
|
+
await client.end()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ✅ Bun SQL — без явних .connect()/.end()
|
|
168
|
+
await sql`...`
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Якщо виклик дійсно потрібен (наприклад, `sql.end()` у graceful shutdown або `.connect()` на сторонньому об'єкті, що випадково ділить імʼя методу), додай маркер `// allow-pg-leftover: <причина>` на тому ж рядку (trailing) або на рядку безпосередньо перед викликом:
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
// allow-pg-leftover: graceful shutdown — закриваємо пул перед exit
|
|
175
|
+
await sql.end()
|
|
176
|
+
|
|
177
|
+
ws.connect(url) // allow-pg-leftover: WebSocket, не pg
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Формат маркера: `allow-pg-leftover: <непорожня причина>` у line- або block-коментарі. Без маркера й без причини — **fail** перевірки.
|
|
181
|
+
|
|
182
|
+
## Що НЕ робити
|
|
183
|
+
|
|
135
184
|
### Не створювати підключення на кожен запит
|
|
136
185
|
|
|
137
186
|
```javascript
|
package/package.json
CHANGED
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
* 2) Якщо в коді використовується Bun SQL (імпорт `sql`/`SQL` з `'bun'`), додатково
|
|
11
11
|
* перевіряє небезпечні патерни:
|
|
12
12
|
* - `new SQL(...)` всередині функції (пул має бути singleton на рівні модуля).
|
|
13
|
-
* -
|
|
13
|
+
* - Будь-який `<obj>.unsafe(...)` без маркера-коментаря `// allow-unsafe: <reason>`
|
|
14
|
+
* на тому ж рядку або рядком вище. `sql.unsafe` за замовчуванням заборонено;
|
|
15
|
+
* допустимий лише для підстановки назви таблиці/колонки чи dynamic SQL/DDL,
|
|
16
|
+
* коли значення контролюється кодом (не user input) — в інших випадках
|
|
17
|
+
* переробляємо на tagged template `sql\`...\${value}...\``.
|
|
18
|
+
* - pg-leftover виклики `<obj>.connect(...)` / `<obj>.end(...)` у файлах, що
|
|
19
|
+
* імпортують Bun SQL: пулом керує Bun, життєвий цикл вручну не потрібен.
|
|
20
|
+
* Opt-out — маркер `// allow-pg-leftover: <reason>`.
|
|
14
21
|
* - Динамічні SQL-списки через `.join(',')` у `IN (...)` / `VALUES (...)`
|
|
15
22
|
* (треба `sql([...])`).
|
|
16
23
|
*/
|
|
@@ -21,9 +28,10 @@ import { join, relative, sep } from 'node:path'
|
|
|
21
28
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
22
29
|
import {
|
|
23
30
|
findBunSqlPerRequestConnectionInText,
|
|
31
|
+
findBunSqlPgLeftoverCallInText,
|
|
32
|
+
findBunSqlUnsafeUseWithoutAllowMarkerInText,
|
|
24
33
|
findUnsafeBunSqlDynamicSqlListInText,
|
|
25
34
|
findUnsafeBunSqlInListMissingEmptyGuardInText,
|
|
26
|
-
findUnsafeBunSqlUnsafeCallInText,
|
|
27
35
|
isBunSqlScanSourceFile,
|
|
28
36
|
textHasBunSqlImport
|
|
29
37
|
} from './utils/bun-sql-scan.mjs'
|
|
@@ -124,7 +132,7 @@ async function checkForbiddenDependencies(pkgJsonPaths, repoRoot, reporter) {
|
|
|
124
132
|
*/
|
|
125
133
|
async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
|
|
126
134
|
const { fail } = reporter
|
|
127
|
-
const counts = { perRequest: 0, unsafeCall: 0, dynamicList: 0, inListGuard: 0 }
|
|
135
|
+
const counts = { perRequest: 0, unsafeCall: 0, dynamicList: 0, inListGuard: 0, pgLeftover: 0 }
|
|
128
136
|
let hasBunSqlImport = false
|
|
129
137
|
|
|
130
138
|
for (const absPath of sourcePaths) {
|
|
@@ -144,7 +152,7 @@ async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
|
|
|
144
152
|
* @param {string} content вміст файлу
|
|
145
153
|
* @param {string} rel posix-шлях відносно `repoRoot`
|
|
146
154
|
* @param {(msg: string) => void} fail callback при помилці
|
|
147
|
-
* @param {{ perRequest: number, unsafeCall: number, dynamicList: number, inListGuard: number }} counts акумулятори
|
|
155
|
+
* @param {{ perRequest: number, unsafeCall: number, dynamicList: number, inListGuard: number, pgLeftover: number }} counts акумулятори
|
|
148
156
|
* @returns {void}
|
|
149
157
|
*/
|
|
150
158
|
function scanFileForBunSqlPatterns(content, rel, fail, counts) {
|
|
@@ -155,11 +163,23 @@ function scanFileForBunSqlPatterns(content, rel, fail, counts) {
|
|
|
155
163
|
`тримай singleton на рівні модуля (js-bun-db.mdc): ${v.snippet}`
|
|
156
164
|
)
|
|
157
165
|
}
|
|
158
|
-
for (const v of
|
|
166
|
+
for (const v of findBunSqlUnsafeUseWithoutAllowMarkerInText(content, rel)) {
|
|
159
167
|
counts.unsafeCall++
|
|
160
168
|
fail(
|
|
161
|
-
`js-bun-db: ${rel}:${v.line} — sql.unsafe(
|
|
162
|
-
|
|
169
|
+
`js-bun-db: ${rel}:${v.line} — sql.unsafe(...) заборонено за замовчуванням; ` +
|
|
170
|
+
`допустимо лише для підстановки назви таблиці/колонки чи dynamic SQL/DDL з code-controlled значенням, ` +
|
|
171
|
+
`інакше переробити на tagged template sql\`...\${value}...\`. ` +
|
|
172
|
+
`Якщо випадок легітимний — додай маркер "// allow-unsafe: <причина>" на тому ж рядку або рядком вище ` +
|
|
173
|
+
`(js-bun-db.mdc): ${v.snippet}`
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
for (const v of findBunSqlPgLeftoverCallInText(content, rel)) {
|
|
177
|
+
counts.pgLeftover++
|
|
178
|
+
fail(
|
|
179
|
+
`js-bun-db: ${rel}:${v.line} — pg-leftover виклик .${v.methodName}(...): Bun SQL пулом керує сам, ` +
|
|
180
|
+
`видали зайвий .connect()/.end() або, якщо випадок легітимний (graceful shutdown тощо), ` +
|
|
181
|
+
`додай маркер "// allow-pg-leftover: <причина>" на тому ж рядку або рядком вище ` +
|
|
182
|
+
`(js-bun-db.mdc): ${v.snippet}`
|
|
163
183
|
)
|
|
164
184
|
}
|
|
165
185
|
for (const v of findUnsafeBunSqlDynamicSqlListInText(content, rel)) {
|
|
@@ -230,11 +250,8 @@ export async function check() {
|
|
|
230
250
|
return reporter.getExitCode()
|
|
231
251
|
}
|
|
232
252
|
|
|
233
|
-
const { hasBunSqlImport, perRequest, unsafeCall, dynamicList, inListGuard } =
|
|
234
|
-
sourcePaths,
|
|
235
|
-
repoRoot,
|
|
236
|
-
reporter
|
|
237
|
-
)
|
|
253
|
+
const { hasBunSqlImport, perRequest, unsafeCall, dynamicList, inListGuard, pgLeftover } =
|
|
254
|
+
await scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter)
|
|
238
255
|
|
|
239
256
|
if (!hasBunSqlImport) {
|
|
240
257
|
pass("js-bun-db: Bun SQL не використовується в коді (немає import { sql|SQL } from 'bun')")
|
|
@@ -245,7 +262,13 @@ export async function check() {
|
|
|
245
262
|
pass('js-bun-db: немає створення new SQL(...) всередині функцій (singleton на рівні модуля)')
|
|
246
263
|
}
|
|
247
264
|
if (unsafeCall === 0) {
|
|
248
|
-
pass('js-bun-db:
|
|
265
|
+
pass('js-bun-db: усі sql.unsafe(...) або відсутні, або супроводжуються маркером "// allow-unsafe: <причина>"')
|
|
266
|
+
}
|
|
267
|
+
if (pgLeftover === 0) {
|
|
268
|
+
pass(
|
|
269
|
+
'js-bun-db: немає pg-leftover викликів .connect()/.end() у файлах з Bun SQL ' +
|
|
270
|
+
'(або всі вони мають маркер "// allow-pg-leftover: <причина>")'
|
|
271
|
+
)
|
|
249
272
|
}
|
|
250
273
|
if (dynamicList === 0) {
|
|
251
274
|
pass("js-bun-db: немає небезпечних динамічних SQL-списків через .join(',') у IN/VALUES")
|
|
@@ -112,6 +112,29 @@ export function parseProgramOrNull(content, virtualPath) {
|
|
|
112
112
|
return result.program
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Парсить файл і повертає `{ program, comments }` або null. Окремий вхід для перевірок,
|
|
117
|
+
* яким потрібні коментарі (наприклад, маркер `// allow-unsafe: ...` біля виклику) —
|
|
118
|
+
* базовий `parseProgramOrNull` свідомо лишається без коментарів, щоб не змінювати API.
|
|
119
|
+
* @param {string} content вихідний код
|
|
120
|
+
* @param {string} virtualPath шлях для вибору `lang` (також для діагностики)
|
|
121
|
+
* @returns {{ program: unknown, comments: { type: 'Line' | 'Block', value: string, start: number, end: number }[] } | null}
|
|
122
|
+
*/
|
|
123
|
+
export function parseProgramAndCommentsOrNull(content, virtualPath) {
|
|
124
|
+
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
125
|
+
let result
|
|
126
|
+
try {
|
|
127
|
+
result = parseSync(virtualPath || 'scan.ts', content, { lang, sourceType: 'module' })
|
|
128
|
+
} catch {
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
if (result.errors?.length) return null
|
|
132
|
+
return {
|
|
133
|
+
program: result.program,
|
|
134
|
+
comments: Array.isArray(result.comments) ? result.comments : []
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
/**
|
|
116
139
|
* Чи це `.join(...)` виклик (типово для динамічних списків у SQL).
|
|
117
140
|
* @param {unknown} node AST node
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* Знаходить:
|
|
5
5
|
* - `new SQL(...)` всередині функції — пул має бути singleton на рівні модуля,
|
|
6
6
|
* а не на кожен виклик handler-а.
|
|
7
|
-
* -
|
|
8
|
-
* `sql.unsafe`
|
|
9
|
-
*
|
|
7
|
+
* - Будь-який виклик `<obj>.unsafe(...)` без маркера-коментаря `// allow-unsafe: <reason>`
|
|
8
|
+
* на тому ж рядку або рядком вище. `sql.unsafe` за замовчуванням заборонено: дозволено
|
|
9
|
+
* тільки якщо значення контролюється кодом (не user input) і потрібно підставити
|
|
10
|
+
* назву таблиці/колонки або dynamic SQL/DDL. Інакше — переробити на tagged template
|
|
11
|
+
* `sql\`...\${value}...\``. Маркер фіксує цю причину для ревʼюера.
|
|
10
12
|
* - Динамічні SQL-списки у tagged template `sql\`... IN (${arr.join(',')}) ...\``:
|
|
11
13
|
* навіть «через tagged template» у запит потрапляє готовий шматок SQL замість
|
|
12
14
|
* параметризованих значень — треба `sql([...])`.
|
|
@@ -21,6 +23,7 @@ import {
|
|
|
21
23
|
isSqlListContextTemplate,
|
|
22
24
|
normalizeSnippet,
|
|
23
25
|
offsetToLine,
|
|
26
|
+
parseProgramAndCommentsOrNull,
|
|
24
27
|
parseProgramOrNull,
|
|
25
28
|
walkAstWithAncestors
|
|
26
29
|
} from './ast-scan-utils.mjs'
|
|
@@ -28,6 +31,17 @@ import {
|
|
|
28
31
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
29
32
|
const BUN_SQL_IMPORT_RE = /\bimport\s*\{[\s\S]*?\b(sql|SQL)\b[\s\S]*?\}\s*from\s*["']bun["']/u
|
|
30
33
|
const IN_PLACEHOLDER_END_RE = /\bin\s*(\(\s*)?$/iu
|
|
34
|
+
// `// allow-unsafe: <reason>` — `allow-unsafe`, двокрапка, **непорожня** причина.
|
|
35
|
+
// Без причини маркер не приймається: ціль — лишити слід для ревʼюера, а не «німий» прапорець.
|
|
36
|
+
const ALLOW_UNSAFE_MARKER_RE = /\ballow-unsafe\s*:\s*\S+/u
|
|
37
|
+
// `// allow-pg-leftover: <reason>` — opt-in для виправданих `.connect()` / `.end()`
|
|
38
|
+
// у файлах з Bun SQL (наприклад, `sql.end()` у graceful shutdown або `.connect()`
|
|
39
|
+
// на сторонньому об'єкті, що випадково ділить імʼя методу з `pg`).
|
|
40
|
+
const ALLOW_PG_LEFTOVER_MARKER_RE = /\ballow-pg-leftover\s*:\s*\S+/u
|
|
41
|
+
// pg-API, які не потрібні з Bun SQL: pool/client життєвий цикл вручну.
|
|
42
|
+
// `release` не входить — Bun SQL такого методу не має, а `.connect()` / `.end()`
|
|
43
|
+
// формально існують і там, тому опт-аут маркером лишається доречним.
|
|
44
|
+
const PG_LEFTOVER_METHOD_NAMES = new Set(['connect', 'end'])
|
|
31
45
|
|
|
32
46
|
/**
|
|
33
47
|
* @param {unknown} node AST node
|
|
@@ -192,23 +206,65 @@ function isNewSqlConstructor(node) {
|
|
|
192
206
|
}
|
|
193
207
|
|
|
194
208
|
/**
|
|
195
|
-
* Чи це виклик `<obj>.unsafe(...)`
|
|
196
|
-
*
|
|
209
|
+
* Чи це виклик `<obj>.unsafe(...)` (будь-який обʼєкт, не тільки `sql`).
|
|
210
|
+
* Файл сканується лише якщо є `import { sql|SQL } from 'bun'`, тож у практиці це
|
|
211
|
+
* або `sql.unsafe`, або `tx.unsafe` всередині `sql.begin(async tx => ...)` —
|
|
212
|
+
* обидва однаково небезпечні, тому розрізняти імʼя обʼєкта не треба.
|
|
197
213
|
* @param {unknown} node AST node
|
|
198
|
-
* @returns {boolean} true для
|
|
214
|
+
* @returns {boolean} true для будь-якого `<obj>.unsafe(...)`
|
|
199
215
|
*/
|
|
200
|
-
function
|
|
216
|
+
function isUnsafeCall(node) {
|
|
201
217
|
if (!node || node.type !== 'CallExpression') return false
|
|
202
218
|
const callee = node.callee
|
|
203
219
|
if (!callee || callee.type !== 'MemberExpression' || callee.computed) return false
|
|
204
220
|
const prop = callee.property
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
221
|
+
return !!prop && prop.type === 'Identifier' && prop.name === 'unsafe'
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Чи є біля виклику маркер-коментар, що матчиться на `markerRe`, на тому ж рядку,
|
|
226
|
+
* що й початок виклику, або на рядку, що передує початку виклику. Це навмисно строга
|
|
227
|
+
* суміжність: відірваний коментар через порожній рядок не зараховується — щоб маркер
|
|
228
|
+
* стояв саме біля виклику, а не «загубився десь вище».
|
|
229
|
+
*
|
|
230
|
+
* Використовується для opt-in маркерів типу `// allow-unsafe: ...` і `// allow-pg-leftover: ...`.
|
|
231
|
+
* @param {{ start: number }} callNode виклик
|
|
232
|
+
* @param {{ type: 'Line' | 'Block', value: string, start: number, end: number }[]} comments коментарі з парсера
|
|
233
|
+
* @param {string} content вихідний код
|
|
234
|
+
* @param {RegExp} markerRe регекс, що валідує текст маркера в `comment.value`
|
|
235
|
+
* @returns {boolean} true, якщо маркер знайдено
|
|
236
|
+
*/
|
|
237
|
+
function hasMarkerCommentNear(callNode, comments, content, markerRe) {
|
|
238
|
+
const callStartLine = offsetToLine(content, callNode.start)
|
|
239
|
+
for (const c of comments) {
|
|
240
|
+
if (!c || (c.type !== 'Line' && c.type !== 'Block')) continue
|
|
241
|
+
if (typeof c.value !== 'string' || !markerRe.test(c.value)) continue
|
|
242
|
+
const startLine = offsetToLine(content, c.start)
|
|
243
|
+
const endLine = offsetToLine(content, c.end)
|
|
244
|
+
// trailing-коментар на тому ж рядку (`<call> // marker: ...`)
|
|
245
|
+
if (startLine === callStartLine) return true
|
|
246
|
+
// коментар на рядку, що безпосередньо передує виклику — для блокових
|
|
247
|
+
// коментарів важливим є саме `endLine`, бо block може займати кілька рядків.
|
|
248
|
+
if (endLine === callStartLine - 1) return true
|
|
249
|
+
}
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Чи це pg-leftover виклик: `<obj>.connect(...)` або `<obj>.end(...)`. Bun SQL пулом
|
|
255
|
+
* керує сам — і `.connect()`, і `.end()` у файлах з Bun SQL зазвичай зайві, тож такі
|
|
256
|
+
* виклики прапоруються (з opt-in маркером для рідкісних легітимних випадків).
|
|
257
|
+
* @param {unknown} node AST node
|
|
258
|
+
* @returns {{ name: 'connect' | 'end' } | null} ім'я pg-leftover методу або null
|
|
259
|
+
*/
|
|
260
|
+
function asPgLeftoverCall(node) {
|
|
261
|
+
if (!node || node.type !== 'CallExpression') return null
|
|
262
|
+
const callee = node.callee
|
|
263
|
+
if (!callee || callee.type !== 'MemberExpression' || callee.computed) return null
|
|
264
|
+
const prop = callee.property
|
|
265
|
+
if (!prop || prop.type !== 'Identifier' || typeof prop.name !== 'string') return null
|
|
266
|
+
if (!PG_LEFTOVER_METHOD_NAMES.has(prop.name)) return null
|
|
267
|
+
return { name: /** @type {'connect' | 'end'} */ (prop.name) }
|
|
212
268
|
}
|
|
213
269
|
|
|
214
270
|
/**
|
|
@@ -236,19 +292,28 @@ export function findBunSqlPerRequestConnectionInText(content, virtualPath = 'sca
|
|
|
236
292
|
}
|
|
237
293
|
|
|
238
294
|
/**
|
|
239
|
-
* Знаходить виклики
|
|
295
|
+
* Знаходить виклики `<obj>.unsafe(...)` без маркера-коментаря `// allow-unsafe: <reason>`
|
|
296
|
+
* на тому ж рядку або рядком вище. `sql.unsafe` за замовчуванням заборонено: дозволено
|
|
297
|
+
* лише коли значення контролюється кодом (не user input) і потрібно підставити те, що
|
|
298
|
+
* не можна параметризувати — назву таблиці/колонки або dynamic SQL/DDL. У всіх інших
|
|
299
|
+
* випадках — переробити на tagged template `sql\`...\${value}...\``.
|
|
300
|
+
*
|
|
301
|
+
* Маркер-коментар фіксує причину для ревʼюера й одночасно слугує opt-in: без нього
|
|
302
|
+
* перевірка падає, навіть якщо у `unsafe` лежить статичний рядок без інтерполяції.
|
|
240
303
|
* @param {string} content вихідний код
|
|
241
304
|
* @param {string} [virtualPath] шлях для вибору `lang`
|
|
242
305
|
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
243
306
|
*/
|
|
244
|
-
export function
|
|
245
|
-
const
|
|
246
|
-
if (!
|
|
307
|
+
export function findBunSqlUnsafeUseWithoutAllowMarkerInText(content, virtualPath = 'scan.ts') {
|
|
308
|
+
const parsed = parseProgramAndCommentsOrNull(content, virtualPath)
|
|
309
|
+
if (!parsed) return []
|
|
310
|
+
const { program, comments } = parsed
|
|
247
311
|
|
|
248
312
|
/** @type {{ line: number, snippet: string }[]} */
|
|
249
313
|
const out = []
|
|
250
314
|
walkAstWithAncestors(program, [], node => {
|
|
251
|
-
if (!
|
|
315
|
+
if (!isUnsafeCall(node)) return
|
|
316
|
+
if (hasMarkerCommentNear(node, comments, content, ALLOW_UNSAFE_MARKER_RE)) return
|
|
252
317
|
out.push({
|
|
253
318
|
line: offsetToLine(content, node.start),
|
|
254
319
|
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
@@ -257,6 +322,39 @@ export function findUnsafeBunSqlUnsafeCallInText(content, virtualPath = 'scan.ts
|
|
|
257
322
|
return out
|
|
258
323
|
}
|
|
259
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Знаходить pg-leftover виклики `<obj>.connect(...)` / `<obj>.end(...)` без маркера
|
|
327
|
+
* `// allow-pg-leftover: <reason>` у файлах, де **в цьому ж файлі** є `import { sql|SQL } from 'bun'`.
|
|
328
|
+
*
|
|
329
|
+
* Скоп навмисно вузький: ці метод-імена занадто загальні (WebSocket, Stream, інші бібліотеки),
|
|
330
|
+
* тож сканер обмежений файлами, що вже використовують Bun SQL — там pg-залишок є явним
|
|
331
|
+
* багом міграції. У не-Bun-SQL файлах прапоратися не буде, навіть якщо проєкт у цілому
|
|
332
|
+
* мігрував.
|
|
333
|
+
* @param {string} content вихідний код
|
|
334
|
+
* @param {string} [virtualPath] шлях для вибору `lang`
|
|
335
|
+
* @returns {{ line: number, snippet: string, methodName: 'connect' | 'end' }[]} список порушень
|
|
336
|
+
*/
|
|
337
|
+
export function findBunSqlPgLeftoverCallInText(content, virtualPath = 'scan.ts') {
|
|
338
|
+
if (!textHasBunSqlImport(content)) return []
|
|
339
|
+
const parsed = parseProgramAndCommentsOrNull(content, virtualPath)
|
|
340
|
+
if (!parsed) return []
|
|
341
|
+
const { program, comments } = parsed
|
|
342
|
+
|
|
343
|
+
/** @type {{ line: number, snippet: string, methodName: 'connect' | 'end' }[]} */
|
|
344
|
+
const out = []
|
|
345
|
+
walkAstWithAncestors(program, [], node => {
|
|
346
|
+
const m = asPgLeftoverCall(node)
|
|
347
|
+
if (!m) return
|
|
348
|
+
if (hasMarkerCommentNear(node, comments, content, ALLOW_PG_LEFTOVER_MARKER_RE)) return
|
|
349
|
+
out.push({
|
|
350
|
+
line: offsetToLine(content, node.start),
|
|
351
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end)),
|
|
352
|
+
methodName: m.name
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
return out
|
|
356
|
+
}
|
|
357
|
+
|
|
260
358
|
/**
|
|
261
359
|
* Знаходить динамічні SQL-списки у TaggedTemplateExpression / TemplateLiteral в контексті
|
|
262
360
|
* `IN (...)` або `VALUES (...)`, де серед expressions є виклик `.join(...)`.
|