@7n/rules-lang-js 0.3.1 → 0.4.0

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.
Files changed (258) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +14 -4
  3. package/rules/bun/bunfig/bunfig.mdc +17 -0
  4. package/rules/bun/bunfig/bunfig.rego +29 -0
  5. package/rules/bun/bunfig/concern.json +9 -0
  6. package/rules/bun/bunfig/template/bunfig.toml.snippet.toml +2 -0
  7. package/rules/bun/docs/index.md +11 -0
  8. package/rules/bun/layout/concern.json +16 -0
  9. package/rules/bun/layout/docs/fix-layout.md +29 -0
  10. package/rules/bun/layout/docs/main.md +34 -0
  11. package/rules/bun/layout/fix-layout.mjs +63 -0
  12. package/rules/bun/layout/layout.mdc +60 -0
  13. package/rules/bun/layout/main.mjs +53 -0
  14. package/rules/bun/licensee/concern.json +7 -0
  15. package/rules/bun/licensee/docs/fix-licensee.md +27 -0
  16. package/rules/bun/licensee/docs/index.md +12 -0
  17. package/rules/bun/licensee/docs/main.md +30 -0
  18. package/rules/bun/licensee/fix-licensee.mjs +31 -0
  19. package/rules/bun/licensee/main.mjs +68 -0
  20. package/rules/bun/lint-surface/concern.json +3 -0
  21. package/rules/bun/lint-surface/lint-surface.mdc +13 -0
  22. package/rules/bun/main.json +1 -0
  23. package/rules/bun/main.mdc +11 -0
  24. package/rules/bun/package_json/concern.json +9 -0
  25. package/rules/bun/package_json/docs/fix-package_json.md +28 -0
  26. package/rules/bun/package_json/docs/index.md +9 -0
  27. package/rules/bun/package_json/fix-package_json.mjs +311 -0
  28. package/rules/bun/package_json/package_json.mdc +14 -0
  29. package/rules/bun/package_json/package_json.rego +64 -0
  30. package/rules/bun/package_json/template/package.json.deny.json +4 -0
  31. package/rules/js/check/check.mdc +26 -0
  32. package/rules/js/check/concern.json +19 -0
  33. package/rules/js/check/docs/eslint-config.md +56 -0
  34. package/rules/js/check/docs/fix-check.md +48 -0
  35. package/rules/js/check/docs/index.md +13 -0
  36. package/rules/js/check/docs/main.md +48 -0
  37. package/rules/js/check/eslint-config.mjs +262 -0
  38. package/rules/js/check/fix-check.mjs +82 -0
  39. package/rules/js/check/main.mjs +310 -0
  40. package/rules/js/dep-policy/concern.json +4 -0
  41. package/rules/js/dep-policy/dep-policy.mdc +36 -0
  42. package/rules/js/dep-policy/docs/main.md +35 -0
  43. package/rules/js/dep-policy/main.mjs +99 -0
  44. package/rules/js/docs/index.md +11 -0
  45. package/rules/js/eslint/concern.json +8 -0
  46. package/rules/js/eslint/docs/fix-eslint.md +50 -0
  47. package/rules/js/eslint/docs/fix-worker.md +31 -0
  48. package/rules/js/eslint/docs/index.md +11 -0
  49. package/rules/js/eslint/docs/main.md +35 -0
  50. package/rules/js/eslint/fix-eslint.mjs +160 -0
  51. package/rules/js/eslint/fix-worker.mjs +139 -0
  52. package/rules/js/eslint/main.mjs +115 -0
  53. package/rules/js/file-extensions/concern.json +3 -0
  54. package/rules/js/file-extensions/file-extensions.mdc +12 -0
  55. package/rules/js/jscpd_config/concern.json +11 -0
  56. package/rules/js/jscpd_config/docs/fix-jscpd_config.md +25 -0
  57. package/rules/js/jscpd_config/docs/index.md +9 -0
  58. package/rules/js/jscpd_config/fix-jscpd_config.mjs +3 -0
  59. package/rules/js/jscpd_config/jscpd_config.mdc +42 -0
  60. package/rules/js/jscpd_config/jscpd_config.rego +44 -0
  61. package/rules/js/jscpd_config/template/.jscpd.json.snippet.json +7 -0
  62. package/rules/js/jscpd_duplicates/concern.json +7 -0
  63. package/rules/js/jscpd_duplicates/docs/main.md +29 -0
  64. package/rules/js/jscpd_duplicates/main.mjs +69 -0
  65. package/rules/js/knip/concern.json +7 -0
  66. package/rules/js/knip/docs/main.md +32 -0
  67. package/rules/js/knip/knip.mdc +15 -0
  68. package/rules/js/knip/main.mjs +68 -0
  69. package/rules/js/lint-findings/concern.json +3 -0
  70. package/rules/js/lint-findings/docs/main.md +40 -0
  71. package/rules/js/lint-findings/main.mjs +125 -0
  72. package/rules/js/main.json +1 -0
  73. package/rules/js/main.mdc +18 -0
  74. package/rules/js/package_json/concern.json +9 -0
  75. package/rules/js/package_json/docs/fix-package_json.md +25 -0
  76. package/rules/js/package_json/docs/index.md +9 -0
  77. package/rules/js/package_json/fix-package_json.mjs +3 -0
  78. package/rules/js/package_json/package_json.mdc +15 -0
  79. package/rules/js/package_json/package_json.rego +142 -0
  80. package/rules/js/package_json/template/package.json.snippet.json +6 -0
  81. package/rules/js/tooling/concern.json +3 -0
  82. package/rules/js/tooling/data/tooling/knip-canonical.json +30 -0
  83. package/rules/js/tooling/data/tooling/oxlint-canonical.json +400 -0
  84. package/rules/js/tooling/docs/main.md +53 -0
  85. package/rules/js/tooling/main.mjs +183 -0
  86. package/rules/js/utils_imports/concern.json +4 -0
  87. package/rules/js/utils_imports/docs/main.md +50 -0
  88. package/rules/js/utils_imports/main.mjs +185 -0
  89. package/rules/js/utils_imports/utils_imports.mdc +15 -0
  90. package/rules/js/vscode_extensions/concern.json +11 -0
  91. package/rules/js/vscode_extensions/docs/fix-vscode_extensions.md +24 -0
  92. package/rules/js/vscode_extensions/docs/index.md +11 -0
  93. package/rules/js/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  94. package/rules/js/vscode_extensions/template/extensions.json.snippet.json +6 -0
  95. package/rules/js/vscode_extensions/vscode_extensions.mdc +11 -0
  96. package/rules/js/vscode_extensions/vscode_extensions.rego +12 -0
  97. package/rules/js-bun-db/connection/concern.json +3 -0
  98. package/rules/js-bun-db/connection/connection.mdc +42 -0
  99. package/rules/js-bun-db/docs/index.md +11 -0
  100. package/rules/js-bun-db/lib/bun-sql-scan.mjs +1047 -0
  101. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +63 -0
  102. package/rules/js-bun-db/lib/docs/index.md +11 -0
  103. package/rules/js-bun-db/main.json +1 -0
  104. package/rules/js-bun-db/main.mdc +8 -0
  105. package/rules/js-bun-db/package_json/concern.json +9 -0
  106. package/rules/js-bun-db/package_json/package_json.mdc +31 -0
  107. package/rules/js-bun-db/package_json/package_json.rego +15 -0
  108. package/rules/js-bun-db/package_json/template/package.json.deny.json +6 -0
  109. package/rules/js-bun-db/pg_format_identifiers/concern.json +3 -0
  110. package/rules/js-bun-db/pg_format_identifiers/pg_format_identifiers.mdc +104 -0
  111. package/rules/js-bun-db/safety/concern.json +4 -0
  112. package/rules/js-bun-db/safety/docs/main.md +34 -0
  113. package/rules/js-bun-db/safety/main.mjs +430 -0
  114. package/rules/js-bun-db/safety/safety.mdc +458 -0
  115. package/rules/js-bun-redis/docs/index.md +11 -0
  116. package/rules/js-bun-redis/imports/concern.json +4 -0
  117. package/rules/js-bun-redis/imports/docs/main.md +36 -0
  118. package/rules/js-bun-redis/imports/imports.mdc +47 -0
  119. package/rules/js-bun-redis/imports/main.mjs +88 -0
  120. package/rules/js-bun-redis/lib/docs/index.md +11 -0
  121. package/rules/js-bun-redis/lib/docs/redis-imports.md +227 -0
  122. package/rules/js-bun-redis/lib/redis-imports.mjs +130 -0
  123. package/rules/js-bun-redis/main.json +1 -0
  124. package/rules/js-bun-redis/main.mdc +8 -0
  125. package/rules/js-bun-redis/package_json/concern.json +9 -0
  126. package/rules/js-bun-redis/package_json/package_json.mdc +11 -0
  127. package/rules/js-bun-redis/package_json/package_json.rego +15 -0
  128. package/rules/js-bun-redis/package_json/template/package.json.deny.json +12 -0
  129. package/rules/js-mssql/deps/concern.json +4 -0
  130. package/rules/js-mssql/deps/docs/main.md +33 -0
  131. package/rules/js-mssql/deps/main.mjs +297 -0
  132. package/rules/js-mssql/docs/index.md +11 -0
  133. package/rules/js-mssql/lib/docs/index.md +11 -0
  134. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +380 -0
  135. package/rules/js-mssql/lib/mssql-pool-scan.mjs +610 -0
  136. package/rules/js-mssql/main.json +1 -0
  137. package/rules/js-mssql/main.mdc +144 -0
  138. package/rules/js-mssql/mssql-tvp/concern.json +3 -0
  139. package/rules/js-mssql/mssql-tvp/mssql-tvp.mdc +77 -0
  140. package/rules/js-mssql/package_json/concern.json +9 -0
  141. package/rules/js-mssql/package_json/package_json.mdc +9 -0
  142. package/rules/js-mssql/package_json/package_json.rego +57 -0
  143. package/rules/js-run/configmap/concern.json +9 -0
  144. package/rules/js-run/configmap/configmap.mdc +37 -0
  145. package/rules/js-run/configmap/configmap.rego +21 -0
  146. package/rules/js-run/configmap/template/configmap.yaml.contains.yml +4 -0
  147. package/rules/js-run/docs/index.md +11 -0
  148. package/rules/js-run/jsconfig/concern.json +9 -0
  149. package/rules/js-run/jsconfig/docs/fix-jsconfig.md +28 -0
  150. package/rules/js-run/jsconfig/docs/index.md +9 -0
  151. package/rules/js-run/jsconfig/fix-jsconfig.mjs +119 -0
  152. package/rules/js-run/jsconfig/jsconfig.mdc +48 -0
  153. package/rules/js-run/jsconfig/jsconfig.rego +59 -0
  154. package/rules/js-run/jsconfig/template/jsconfig.json.snippet.json +10 -0
  155. package/rules/js-run/lib/bunyan-imports.mjs +98 -0
  156. package/rules/js-run/lib/check-env-scan.mjs +338 -0
  157. package/rules/js-run/lib/conn-file-rules.mjs +214 -0
  158. package/rules/js-run/lib/conn-imports-scan.mjs +154 -0
  159. package/rules/js-run/lib/docs/bunyan-imports.md +121 -0
  160. package/rules/js-run/lib/docs/check-env-scan.md +438 -0
  161. package/rules/js-run/lib/docs/conn-file-rules.md +304 -0
  162. package/rules/js-run/lib/docs/conn-imports-scan.md +208 -0
  163. package/rules/js-run/lib/docs/index.md +16 -0
  164. package/rules/js-run/lib/docs/promise-settimeout-scan.md +334 -0
  165. package/rules/js-run/lib/docs/temporal-scan.md +29 -0
  166. package/rules/js-run/lib/promise-settimeout-scan.mjs +128 -0
  167. package/rules/js-run/lib/temporal-scan.mjs +52 -0
  168. package/rules/js-run/main.json +1 -0
  169. package/rules/js-run/main.mdc +16 -0
  170. package/rules/js-run/package_json/concern.json +9 -0
  171. package/rules/js-run/package_json/package_json.mdc +44 -0
  172. package/rules/js-run/package_json/package_json.rego +37 -0
  173. package/rules/js-run/package_json/template/package.json.deny.json +22 -0
  174. package/rules/js-run/project-structure/concern.json +3 -0
  175. package/rules/js-run/project-structure/project-structure.mdc +11 -0
  176. package/rules/js-run/runtime/concern.json +12 -0
  177. package/rules/js-run/runtime/docs/fix-runtime.md +27 -0
  178. package/rules/js-run/runtime/docs/main.md +35 -0
  179. package/rules/js-run/runtime/fix-runtime.mjs +46 -0
  180. package/rules/js-run/runtime/main.mjs +496 -0
  181. package/rules/js-run/runtime/runtime.mdc +184 -0
  182. package/rules/js-run/scope/concern.json +3 -0
  183. package/rules/js-run/scope/scope.mdc +11 -0
  184. package/rules/npm-module/docs/index.md +11 -0
  185. package/rules/npm-module/emit_types_config/concern.json +9 -0
  186. package/rules/npm-module/emit_types_config/docs/fix-emit_types_config.md +25 -0
  187. package/rules/npm-module/emit_types_config/docs/index.md +9 -0
  188. package/rules/npm-module/emit_types_config/emit_types_config.mdc +43 -0
  189. package/rules/npm-module/emit_types_config/emit_types_config.rego +28 -0
  190. package/rules/npm-module/emit_types_config/fix-emit_types_config.mjs +5 -0
  191. package/rules/npm-module/emit_types_config/template/tsconfig.emit-types.json.snippet.json +9 -0
  192. package/rules/npm-module/header_doc_pointer/concern.json +5 -0
  193. package/rules/npm-module/header_doc_pointer/docs/main.md +40 -0
  194. package/rules/npm-module/header_doc_pointer/header_doc_pointer.mdc +18 -0
  195. package/rules/npm-module/header_doc_pointer/main.mjs +131 -0
  196. package/rules/npm-module/main.json +1 -0
  197. package/rules/npm-module/main.mdc +36 -0
  198. package/rules/npm-module/npm_package_json/concern.json +9 -0
  199. package/rules/npm-module/npm_package_json/docs/fix-npm_package_json.md +26 -0
  200. package/rules/npm-module/npm_package_json/docs/index.md +9 -0
  201. package/rules/npm-module/npm_package_json/fix-npm_package_json.mjs +5 -0
  202. package/rules/npm-module/npm_package_json/npm_package_json.mdc +57 -0
  203. package/rules/npm-module/npm_package_json/npm_package_json.rego +73 -0
  204. package/rules/npm-module/npm_package_json/template/package.json.snippet.json +1 -0
  205. package/rules/npm-module/package_structure/concern.json +5 -0
  206. package/rules/npm-module/package_structure/docs/main.md +37 -0
  207. package/rules/npm-module/package_structure/main.mjs +448 -0
  208. package/rules/npm-module/package_structure/package_structure.mdc +63 -0
  209. package/rules/npm-module/root_package_json/concern.json +9 -0
  210. package/rules/npm-module/root_package_json/docs/fix-root_package_json.md +25 -0
  211. package/rules/npm-module/root_package_json/docs/index.md +9 -0
  212. package/rules/npm-module/root_package_json/fix-root_package_json.mjs +5 -0
  213. package/rules/npm-module/root_package_json/root_package_json.mdc +41 -0
  214. package/rules/npm-module/root_package_json/root_package_json.rego +28 -0
  215. package/rules/npm-module/root_package_json/template/package.json.snippet.json +1 -0
  216. package/rules/npm-module/rule_meta/concern.json +8 -0
  217. package/rules/npm-module/rule_meta/docs/main.md +35 -0
  218. package/rules/npm-module/rule_meta/main.mjs +119 -0
  219. package/rules/npm-module/rule_meta/rule_meta.mdc +11 -0
  220. package/rules/npm-module/skill_meta/concern.json +5 -0
  221. package/rules/npm-module/skill_meta/docs/main.md +149 -0
  222. package/rules/npm-module/skill_meta/main.mjs +91 -0
  223. package/rules/npm-module/skill_meta/skill_meta.mdc +11 -0
  224. package/rules/tool-surface/docs/index.md +11 -0
  225. package/rules/tool-surface/main.json +6 -0
  226. package/rules/tool-surface/main.mdc +72 -0
  227. package/rules/vue/composition-api/composition-api.mdc +82 -0
  228. package/rules/vue/composition-api/concern.json +3 -0
  229. package/rules/vue/docs/index.md +11 -0
  230. package/rules/vue/lib/docs/index.md +11 -0
  231. package/rules/vue/lib/docs/vue-forbidden-imports.md +265 -0
  232. package/rules/vue/lib/vue-forbidden-imports.mjs +240 -0
  233. package/rules/vue/main.json +1 -0
  234. package/rules/vue/main.mdc +18 -0
  235. package/rules/vue/nheader-layout/concern.json +3 -0
  236. package/rules/vue/nheader-layout/nheader-layout.mdc +171 -0
  237. package/rules/vue/package_json/concern.json +9 -0
  238. package/rules/vue/package_json/package_json.mdc +30 -0
  239. package/rules/vue/package_json/package_json.rego +140 -0
  240. package/rules/vue/packages/concern.json +6 -0
  241. package/rules/vue/packages/docs/index.md +11 -0
  242. package/rules/vue/packages/docs/main.md +35 -0
  243. package/rules/vue/packages/main.mjs +575 -0
  244. package/rules/vue/packages/packages.mdc +56 -0
  245. package/rules/vue/quasar-ui/concern.json +3 -0
  246. package/rules/vue/quasar-ui/quasar-ui.mdc +32 -0
  247. package/rules/vue/structure/concern.json +3 -0
  248. package/rules/vue/structure/structure.mdc +101 -0
  249. package/rules/vue/testing/concern.json +3 -0
  250. package/rules/vue/testing/testing.mdc +40 -0
  251. package/rules/vue/tfm-translations/concern.json +7 -0
  252. package/rules/vue/tfm-translations/docs/main.md +29 -0
  253. package/rules/vue/tfm-translations/main.mjs +55 -0
  254. package/rules/vue/tfm-translations/tfm-translations.mdc +32 -0
  255. package/rules/vue/vite-config/concern.json +3 -0
  256. package/rules/vue/vite-config/vite-config.mdc +153 -0
  257. package/rules/vue/vite-env/concern.json +3 -0
  258. package/rules/vue/vite-env/vite-env.mdc +61 -0
@@ -0,0 +1,458 @@
1
+ ## Безпечне виконання запитів через Bun SQL
2
+
3
+ JS/TS-скан: `lib/bun-sql-scan.mjs`, детектори підключено з `safety/main.mjs`.
4
+
5
+ Цільові файли: усі JS/TS джерела репозиторію (крім ігнорованих через cursor-конфіг), де є `import { sql, SQL } from 'bun'`.
6
+
7
+ ### `sql.unsafe(...)` за замовчуванням заборонено
8
+
9
+ Будь-який виклик `sql.unsafe(...)` (так само `tx.unsafe(...)` всередині `sql.begin`) **заборонено**, окрім випадків, коли **обидві** умови виконані:
10
+
11
+ 1. значення підставляється з **коду** — константа, конфіг, whitelist; **не з user input**;
12
+ 2. треба підставити те, що **не можна параметризувати** через tagged template:
13
+ - назву **таблиці**,
14
+ - назву **колонки**,
15
+ - **dynamic SQL / DDL** (`CREATE`, `ALTER`, `DROP`, multi-statement migration, серверні `SET`/`SHOW` і подібне).
16
+
17
+ В усіх інших випадках — переробити на звичайний tagged template виду `` sql`...${value}...` ``: значення біндяться як параметри й injection не лишається.
18
+
19
+ Кожен легітимний `sql.unsafe(...)` має супроводжуватись **маркером-коментарем** з причиною — на тому ж рядку (trailing) або на рядку безпосередньо перед викликом. Маркер — opt-in для перевірки `js-bun-db` і слід для ревʼюера:
20
+
21
+ ```javascript
22
+ import format from '@scaleleap/pg-format'
23
+
24
+ const query = format('CREATE TABLE %I (id int)', tableName)
25
+ // allow-unsafe: DDL — назву таблиці параметризувати не можна; ідентифікатор екранує pg-format
26
+ await sql.unsafe(query)
27
+
28
+ await sql.unsafe('SELECT pg_advisory_lock($1)', [lockId]) // allow-unsafe: pg_advisory_lock — окремий шлях, без tagged template
29
+ ```
30
+
31
+ Формат маркера: `allow-unsafe: <непорожня причина>` у line- або block-коментарі. Без причини (`// allow-unsafe:`) і без маркера взагалі — **fail** перевірки (детектор — `findBunSqlUnsafeUseWithoutAllowMarkerInText` у `lib/bun-sql-scan.mjs`).
32
+
33
+ #### `sql.unsafe` з template-літералом і `${...}`-інтерполяцією — заборонено навіть з маркером
34
+
35
+ ``sql.unsafe(`...${x}...`)`` — окремий **hard fail** (детектор `findBunSqlUnsafeWithInterpolatedTemplateInText`), який не знімається маркером `// allow-unsafe`. Шаблонна підстановка `${x}` у `sql.unsafe`-рядок:
36
+
37
+ - **не екранує** identifier'ів (reserved words, спецсимволи, пробіли в імені);
38
+ - **не біндить** значень (вони потрапляють у запит сирим текстом, як injection-вектор);
39
+ - виглядає «безпечно» через знайому tagged-template-форму, але не має жодних гарантій Bun SQL.
40
+
41
+ Канон — побудувати `text` окремо, потім передати в `sql.unsafe(text, [params])`:
42
+
43
+ - для **identifiers** — `@scaleleap/pg-format` `format('%I', name)` (екранує спецсимволи, reserved words; деталі й приклади — `pg_format_identifiers/pg_format_identifiers.mdc`);
44
+ - для **values** — позиційні `$1`, `$2`, … як placeholder'и в тексті + масив значень другим аргументом;
45
+ - для **fragments** з whitelist (`ASC`/`DESC`) — `format('%s', whitelistedValue)`.
46
+
47
+ ```javascript
48
+ // ❌ template-літерал з ${...} — fail навіть з allow-unsafe
49
+ // allow-unsafe: DDL
50
+ await sql.unsafe(`CREATE TABLE ${tableName} (id int)`)
51
+
52
+ // ✅ format('%I', ...) екранує identifier, sql.unsafe приймає готовий text
53
+ import format from '@scaleleap/pg-format'
54
+ const query = format('CREATE TABLE %I (id int)', tableName)
55
+ // allow-unsafe: DDL — назву таблиці параметризувати не можна
56
+ await sql.unsafe(query)
57
+ ```
58
+
59
+ Статичні `` sql.unsafe(`SELECT 1`) `` (без `${...}`) і `sql.unsafe(text, [params])` зі змінною `text`, зібраною заздалегідь, — допустимі (за наявності `// allow-unsafe`-маркера).
60
+
61
+ Заборонені кейси (треба переробити на tagged template):
62
+
63
+ ```javascript
64
+ // ❌ дані від користувача — параметризуй через tagged template
65
+ await sql.unsafe(`SELECT * FROM users WHERE id = ${userId}`)
66
+
67
+ // ❌ навіть у tagged template — динамічний список через .join(',')
68
+ await sql`SELECT * FROM users WHERE id IN (${ids.join(',')})`
69
+ ```
70
+
71
+ Для динамічних списків — `sql([...])` або `sql(rows, 'colA', 'colB')`, **не** `.join(',')` (детектор — `findUnsafeBunSqlDynamicSqlListInText`).
72
+
73
+ ### Прибирати pg-leftover виклики (`.connect()`, `.end()`)
74
+
75
+ У файлах з Bun SQL (`import { sql, SQL } from 'bun'`) залишки від `pg` — `pool.connect()`, `client.end()`, `pool.end()` — мають бути видалені. Bun SQL пулом керує сам: на першому запиті підключається, idle/lifetime закриває за конфігом — окремий життєвий цикл вручну не потрібен.
76
+
77
+ ```javascript
78
+ // ❌ pg-leftover: ручний lifecycle, який Bun SQL робить за тебе
79
+ const client = await pool.connect()
80
+ try {
81
+ await client.query('...')
82
+ } finally {
83
+ await client.end()
84
+ }
85
+
86
+ // ✅ Bun SQL — без явних .connect()/.end()
87
+ await sql`...`
88
+ ```
89
+
90
+ Якщо виклик дійсно потрібен (наприклад, `sql.end()` у graceful shutdown або `.connect()` на сторонньому об'єкті, що випадково ділить імʼя методу), додай маркер `// allow-pg-leftover: <причина>` на тому ж рядку (trailing) або на рядку безпосередньо перед викликом:
91
+
92
+ ```javascript
93
+ // allow-pg-leftover: graceful shutdown — закриваємо пул перед exit
94
+ await sql.end()
95
+
96
+ ws.connect(url) // allow-pg-leftover: WebSocket, не pg
97
+ ```
98
+
99
+ Формат маркера: `allow-pg-leftover: <непорожня причина>` у line- або block-коментарі. Без маркера й без причини — **fail** перевірки (детектор — `findBunSqlPgLeftoverCallInText` у `lib/bun-sql-scan.mjs`; скоп навмисно обмежений файлами, де вже є Bun SQL import, щоб не хибно спрацьовувати на WebSocket / Stream / інших бібліотеках з такими самими іменами методів).
100
+
101
+ ### `pg`: виключення для LISTEN/NOTIFY
102
+
103
+ Bun SQL **поки не реалізує PostgreSQL LISTEN/NOTIFY** (асинхронні нотифікації через `pg_notify` / `LISTEN <channel>`). Тому якщо проєкт справді користується LISTEN/NOTIFY, npm-пакет `pg` дозволено тримати в `dependencies` **виключно** для LISTEN/NOTIFY-клієнта. Усі інші запити (SELECT/INSERT/UPDATE/DELETE/migration) — далі через Bun SQL.
104
+
105
+ Перевірка `pg` зважує цей сигнал автоматично (тому `pg` прибрано з [denylist](../package_json/template/package.json.deny.json) — Rego не бачить JS-коду, тож зважування LISTEN/NOTIFY перенесено у JS-сканер `safety/main.mjs` + `lib/bun-sql-scan.mjs`).
106
+
107
+ #### Як перевірка визначає, що LISTEN/NOTIFY у проєкті є
108
+
109
+ AST-сканер (`findPgListenNotifyUsageInText`) шукає будь-який із сигналів:
110
+
111
+ - `client.query('LISTEN <channel>')` / `client.query('UNLISTEN *')` / `client.query('NOTIFY <channel>, ...')` — string- або template-literal-аргумент, що починається з `LISTEN` / `UNLISTEN` / `NOTIFY` (case-insensitive, leading whitespace допускається). Також покриті `queryArray` / `queryStream`.
112
+ - `client.on('notification', handler)` — listener на pg-події `notification`.
113
+ - TaggedTemplateExpression `` <tag>`LISTEN ...` `` — на випадок, якщо хтось загорнув LISTEN у власний tagged template.
114
+
115
+ Якщо хоч один сигнал є — `dependencies.pg` зважено як виправдане; інакше — `fail` із посиланням на цю секцію.
116
+
117
+ #### Правила для файлів з `import 'pg'`
118
+
119
+ Кожен файл, який імпортує `'pg'`, повинен **сам** містити один із LISTEN/NOTIFY-сигналів. Сценарій «один файл слухає, інший виконує `SELECT * FROM users`» — теж `fail`: звичайні запити через `pg` треба переписати на Bun SQL, а LISTEN/NOTIFY-логіку лишити в окремому модулі.
120
+
121
+ #### Приклад — окремий модуль для LISTEN
122
+
123
+ ```javascript
124
+ // src/db/pg-listen.ts — єдине місце, де живе import 'pg'
125
+ import { Client } from 'pg'
126
+
127
+ const listener = new Client({ connectionString: process.env.DATABASE_URL })
128
+
129
+ // allow-pg-leftover: pg LISTEN-клієнт не керується Bun SQL пулом
130
+ await listener.connect()
131
+ await listener.query('LISTEN orders_channel')
132
+ listener.on('notification', msg => {
133
+ // обробка нотифікації
134
+ })
135
+ ```
136
+
137
+ ```javascript
138
+ // src/db/users.ts — звичайні запити, через Bun SQL
139
+ import { sql } from 'bun'
140
+
141
+ export const getUser = id => sql`SELECT * FROM users WHERE id = ${id}`
142
+ ```
143
+
144
+ `pg-listen.ts` буде дозволений завдяки `LISTEN orders_channel` і `.on('notification', ...)`; `users.ts` не має імпорту `'pg'`, тож вільно живе з Bun SQL. `client.connect()` у файлі з Bun SQL потребував би маркер `// allow-pg-leftover: ...`; у файлі, де **Bun SQL не імпортовано**, pg-leftover-сканер не спрацьовує, але маркер як коментар-причина — корисний для рев'ю.
145
+
146
+ #### Що лишається забороненим
147
+
148
+ - `import 'pg'` у файлі без LISTEN/NOTIFY — `fail` з повідомленням «перенеси на Bun SQL, лиши LISTEN в окремому модулі».
149
+ - `dependencies.pg` без жодного LISTEN/NOTIFY-сигналу у проєкті — `fail` навіть якщо `pg` нібито «потрібен історично».
150
+ - `pg-format` (unscoped) — лишається у [denylist](../package_json/template/package.json.deny.json); виключення для LISTEN/NOTIFY стосується **тільки** самого `pg`.
151
+ - `pg-pool`, `pg-native`, `mysql`, `mysql2` — виключень немає, видаляти повністю.
152
+
153
+ ### Безпечне виконання запитів
154
+
155
+ Тільки **tagged template** з `${...}` — Bun сам біндить позиційні параметри й захищає від SQL injection:
156
+
157
+ ```javascript
158
+ import { sql } from 'bun'
159
+
160
+ const userId = 42
161
+ const status = 'active'
162
+
163
+ const users = await sql`
164
+ SELECT * FROM users
165
+ WHERE id = ${userId} AND status = ${status}
166
+ `
167
+ ```
168
+
169
+ Об'єктний INSERT/UPDATE та `IN (...)` — через helper `sql(...)`:
170
+
171
+ ```javascript
172
+ const user = { name: 'Alice', email: 'a@example.com' }
173
+
174
+ const [created] = await sql`
175
+ INSERT INTO users ${sql(user)}
176
+ RETURNING *
177
+ `
178
+
179
+ await sql`UPDATE users SET ${sql(user, 'name', 'email')} WHERE id = ${created.id}`
180
+
181
+ const ids = [1, 2, 3]
182
+ await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
183
+ ```
184
+
185
+ Multi-row INSERT з масиву об'єктів — `sql(rows)` генерує column list і VALUES автоматично:
186
+
187
+ ```javascript
188
+ // ❌ format + pgWrite.unsafe — ручне склеювання рядків, injection-вектор
189
+ const insertWfQry = `insert into approval.workflow (request_id, job_title_id, name, status)
190
+ values ${approverJobs.map(job => `('${request.id}', ${job.id}, '${job.short_name}', 'pending')`).join(', ')}`
191
+ await pgWrite.unsafe(insertWfQry)
192
+
193
+ // ✅ sql(rows) — один параметр-масив, bind через wire-protocol
194
+ const wfRows = approverJobs.map(job => ({
195
+ request_id: request.id,
196
+ job_title_id: job.id,
197
+ name: job.short_name,
198
+ status: job.id === nextJobId ? 'current' : 'pending'
199
+ }))
200
+ await sql`INSERT INTO approval.workflow ${sql(wfRows)}`
201
+ ```
202
+
203
+ Коли потрібен стабільний план для великих batch'ів (N > 20) або строгі типи колонок — використовуй `unnest` (деталі й приклад MERGE з UNNEST — секція «pg-format: повне видалення, без шимів» нижче). Для невеликих INSERT'ів де колонки відомі — `sql(rows)` коротший і зрозуміліший.
204
+
205
+ #### `IN (...)`: значення з template literal — тільки через змінну + guard на пустоту
206
+
207
+ Якщо список для `IN (...)` підставляється через `${...}` у template literal, його **потрібно**:
208
+
209
+ - винести в **окрему змінну** (не підставляти вираз напряму в `${...}`);
210
+ - **перевірити на пустоту** перед запитом і **throw** (щоб не виконувати некоректний SQL або запит з неочікуваною семантикою).
211
+
212
+ Приклад:
213
+
214
+ ```javascript
215
+ const ids = inputIds.map(Number).filter(n => Number.isFinite(n))
216
+ if (!ids.length) throw new Error('ids is empty')
217
+
218
+ await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
219
+ ```
220
+
221
+ Транзакції — через `sql.begin` (auto-commit/rollback), вкладені — через `tx.savepoint`:
222
+
223
+ ```javascript
224
+ await sql.begin(async tx => {
225
+ await tx`INSERT INTO users ${sql(user)}`
226
+ await tx`UPDATE accounts SET balance = balance - ${100} WHERE user_id = ${user.id}`
227
+ })
228
+ ```
229
+
230
+ #### JSONB-параметри: без `JSON.stringify`
231
+
232
+ Bun SQL серіалізує JS-об'єкти й масиви у JSON автоматично — викликати `JSON.stringify` перед передачею в `::jsonb` / `::jsonb[]` **заборонено**.
233
+
234
+ ```javascript
235
+ // ❌ зайвий JSON.stringify — подвійна серіалізація або зайвий рядок
236
+ await sql`INSERT INTO events (details) VALUES (${JSON.stringify(detailsForEvent)}::jsonb)`
237
+
238
+ await sql`SELECT * FROM unnest(${sql.array(batch.map(r => JSON.stringify(r.data)), 'jsonb')})`
239
+
240
+ // ✅ об'єкт/масив передається напряму
241
+ await sql`INSERT INTO events (details) VALUES (${detailsForEvent}::jsonb)`
242
+
243
+ await sql`SELECT * FROM unnest(${sql.array(col(batch, 'data'), 'jsonb')})`
244
+ ```
245
+
246
+ `UNION ALL`-цикл замість `unnest` підходить для малих динамічних запитів (2–5 рядків), де кожна гілка семантично різна. Для bulk upsert — завжди `unnest`.
247
+
248
+ #### Коментар під час виправлення SQL injection
249
+
250
+ Коли виправляєш місце з потенційним **SQL injection** (наприклад, заміна конкатенації/`.join(',')` на `sql(ids)` або перехід з `sql.unsafe(...)` на tagged template), **додай поруч короткий коментар** з описом причини.
251
+
252
+ Вимоги до коментаря:
253
+
254
+ - пояснити **що саме було небезпечно** (конкатенація, підмішування user input, динамічний `IN (...)`, тощо);
255
+ - пояснити **чому новий варіант безпечний** (параметризація через tagged template / `sql(...)`);
256
+ - без "романів": 1–2 рядки, достатньо для ревʼю.
257
+
258
+ Приклад:
259
+
260
+ ```javascript
261
+ // SQLi fix: не конкатенуємо значення в `IN (...)`; Bun parameterize через `sql(ids)`.
262
+ await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
263
+ ```
264
+
265
+ #### Що НЕ робити з бібліотеками
266
+
267
+ Якщо в коді з'явився `import { sql } from 'bun'`, то `pg`, `pg-format` та `mysql2` мають бути прибрані і з `dependencies`, і з імпортів — щоб не лишалось двох паралельних шляхів до БД та ручного форматування поряд із параметризованими template literal.
268
+
269
+ Те саме стосується **локальних шимів**: будь-який модуль, що експортує `format`, `pgRead`, `pgWrite`, `query(text, params)`, `quoteLiteral`, `quoteIdent` як обгортку над `sql.unsafe(...)`, потрібно переписати — всі call-site на tagged template, сам шим видалити (детектори — `findPgFormatShimDefinitionInText` і `findPgFormatLikeQueryWrapperInText` у `lib/bun-sql-scan.mjs`; деталі — секція «pg-format: повне видалення, без шимів» нижче).
270
+
271
+ ### `sql.array(arr, type)` для передачі масивів
272
+
273
+ Коли JS-масив передається як параметр у Bun SQL template literal всередині `unnest(...)` або іншого контексту, де PostgreSQL очікує типізований масив (`int4[]`, `uuid[]` тощо), — обов'язково використовувати `sql.array(arr, type)` (або `pgWrite.array` / `pgRead.array` — вони є екземплярами `SQL`). Другий аргумент (тип елементів) — обов'язковий; без нього перевірка (`findSqlArrayWithoutTypeArgInText` у `lib/bun-sql-scan.mjs`) фейлить.
274
+
275
+ #### Заборонені патерни
276
+
277
+ ```javascript
278
+ // ❌ пряма підстановка масиву — Bun серіалізує як рядок, не як pg-масив
279
+ ${ids}
280
+
281
+ // ❌ cast-синтаксис без .array() — працює в деяких версіях, але не гарантований
282
+ ${ids}::int8[]
283
+
284
+ // ❌ відсутній тип — Bun не може вивести тип pg, можливий mismatch
285
+ sql.array(ids)
286
+ ```
287
+
288
+ #### Дозволені патерни
289
+
290
+ ```javascript
291
+ // ✅ pgWrite.array з явним типом
292
+ ${pgWrite.array(ids, 'int8')}
293
+ ${pgWrite.array(uuids, 'uuid')}
294
+ ${pgWrite.array(flags, 'bool')}
295
+ ${pgWrite.array(amounts, 'numeric')}
296
+ ${pgWrite.array(names, 'text')}
297
+ ${pgWrite.array(dates, 'date')}
298
+ ${pgWrite.array(timestamps, 'timestamptz')}
299
+
300
+ // ✅ pgRead.array — те саме правило
301
+ ${pgRead.array(ids, 'int4')}
302
+ ```
303
+
304
+ #### Таблиця типів
305
+
306
+ | JS-тип | PostgreSQL тип | Аргумент |
307
+ | ------------- | -------------- | --------------- |
308
+ | number (int) | int4 | `'int4'` |
309
+ | bigint / id | int8 | `'int8'` |
310
+ | UUID string | uuid | `'uuid'` |
311
+ | boolean | bool | `'bool'` |
312
+ | decimal/float | numeric | `'numeric'` |
313
+ | string | text | `'text'` |
314
+ | date string | date | `'date'` |
315
+ | ISO datetime | timestamptz | `'timestamptz'` |
316
+
317
+ #### `col(arr, key)` — хелпер для unnest-колонок
318
+
319
+ OXC formatter (oxfmt ≥ 0.49) примусово розгортає будь-який `CallExpression`, де перший аргумент є `CallExpression` з callback, у багаторядковий блок — незалежно від `printWidth`. Тому `pgWrite.array(arr.map(r => r.field), 'type')` всередині tagged template literal завжди стає 4-рядковим блоком. `col(arr, 'field')` (перший аргумент — identifier, другий — string literal) цей тригер не зачіпає і лишається однорядковим.
320
+
321
+ Канонічне місце хелпера — `src/utils/col.mjs` (або `src/conn/col.mjs` залежно від структури проєкту):
322
+
323
+ ```javascript
324
+ // src/utils/col.mjs
325
+ export const col = (arr, key) => arr.map(r => r[key])
326
+ ```
327
+
328
+ ```javascript
329
+ import { pgWrite } from '#src/conn/db.mjs'
330
+ import { col } from '#src/utils/col.mjs'
331
+
332
+ // ❌ oxfmt розгортає на 4+ рядки незалежно від printWidth
333
+ ${pgWrite.array(rows.map(r => r.id), 'int4')}
334
+
335
+ // ✅ col(arr, key) — перший аргумент не є callback; oxfmt лишає однорядковим
336
+ ${pgWrite.array(col(rows, 'id'), 'int4')}
337
+ ```
338
+
339
+ #### Повний приклад (UNNEST + MERGE)
340
+
341
+ ```javascript
342
+ await pgWrite`
343
+ MERGE INTO "order".product p
344
+ USING (
345
+ SELECT * FROM unnest(
346
+ ${pgWrite.array(col(rows, 'order_id'), 'uuid')},
347
+ ${pgWrite.array(col(rows, 'product_id'), 'int4')},
348
+ ${pgWrite.array(col(rows, 'qty'), 'numeric')},
349
+ ${pgWrite.array(col(rows, 'is_refund'), 'bool')}
350
+ ) AS s(order_id, product_id, qty, is_refund)
351
+ ) AS s ON p.order_id = s.order_id AND p.product_id = s.product_id
352
+ WHEN MATCHED THEN
353
+ UPDATE SET qty = s.qty
354
+ WHEN NOT MATCHED THEN
355
+ INSERT (order_id, product_id, qty, is_refund)
356
+ VALUES (s.order_id, s.product_id, s.qty, s.is_refund)
357
+ `
358
+ ```
359
+
360
+ ### `pg-format`: повне видалення, без шимів
361
+
362
+ Міграція з `pg-format` — це **зміна стилю запитів**, а не збереження API. У проєкті після переходу на Bun SQL **заборонено** залишати:
363
+
364
+ - функцію з іменем `format` (чи `pgFormat`, `sqlFormat`, `pgFmt`), що приймає шаблон з `%L` / `%I` / `%s` і значення;
365
+ - допоміжні `quoteLiteral`, `quoteIdent`, `escapeLiteral`, `escapeIdent` як публічні експорти модуля;
366
+ - обгортки `pgRead.query(text, params)` / `pgWrite.query(text, params)` / `db.query(text, params)`, які складають SQL-рядок (з або без `format`) і викликають `sql.unsafe(text, params)` — це повертає injection-поверхню, від якої ми йдемо, тільки під «зручним» іменем.
367
+
368
+ Замість цього всі точки використання потрібно перевести на tagged template ``sql`...${value}...` ``. Кожен параметр `${value}` стає окремим bind-значенням, без рядкового екранування.
369
+
370
+ #### Типові ідіоми `pg-format` → Bun SQL
371
+
372
+ | Було (`pg-format`) | Стало (Bun SQL) |
373
+ | -------------------------------------------------- | --------------------------------------------------------------------------------------------- |
374
+ | `format('... WHERE id = %L', id)` | ``sql`... WHERE id = ${id}` `` |
375
+ | `format('... IN (%L)', ids)` | ``sql`... IN ${sql(ids)}` `` (з guard на пустоту перед запитом) |
376
+ | `format('INSERT ... VALUES %L', [row])` (1 рядок) | ``sql`INSERT ... VALUES (${a}, ${b}, ...)` `` |
377
+ | `format('INSERT ... VALUES %L', rows)` (N рядків) | ``sql`INSERT INTO t ${sql(rows, 'a', 'b')}` `` або `unnest($1::T[], $2::T[]) AS t(a, b)` |
378
+ | `format('MERGE ... USING (VALUES %L) AS d(...)')` | ``sql`MERGE ... USING (SELECT * FROM unnest(${arrA}::A[], ${arrB}::B[]) AS t(a, b)) AS d` `` |
379
+ | `format('... %I ...', tableName)` (whitelist) | `@scaleleap/pg-format`: `format('%I', name)` + `sql.unsafe(text, [params])` з маркером |
380
+
381
+ Для multi-row `VALUES` у `MERGE` / `INSERT` з конкретними типами — паралельні масиви по колонках і `unnest($1::TYPE[], $2::TYPE[], ...) AS t(col1, col2, ...)`. Кожна колонка передається одним параметром-масивом; типи задаються кастом масиву (`::uuid[]`, `::bigint[]`, `::numeric[]`, `::text[]`, …).
382
+
383
+ ##### Приклад: MERGE з UNNEST і динамічними колонками
384
+
385
+ ```javascript
386
+ // ❌ format + pgWrite.unsafe — N×7 окремих значень, план змінюється при кожному batch.length
387
+ const valuesSql = batch
388
+ .map(row => format('(%L::int, %L::date, %L::jsonb)', row.id, row.date, JSON.stringify(row.data)))
389
+ .join(',')
390
+ const sql = format(`MERGE INTO t USING (VALUES %s) AS s(id, date, data) ON ...`, valuesSql)
391
+ await pgWrite.unsafe(sql)
392
+
393
+ // ✅ UNNEST — 3 параметри незалежно від розміру batch; план стабільний і може кешуватись
394
+ await pgWrite`
395
+ WITH s(id, date, data) AS (
396
+ SELECT * FROM unnest(
397
+ ${pgWrite.array(col(batch, 'id'), 'int4')},
398
+ ${pgWrite.array(col(batch, 'date'), 'date')},
399
+ ${pgWrite.array(col(batch, 'data'), 'jsonb')}
400
+ )
401
+ )
402
+ MERGE INTO my_table AS t
403
+ USING s ON t.id = s.id
404
+ WHEN MATCHED THEN
405
+ UPDATE SET date = s.date, data = s.data
406
+ WHEN NOT MATCHED THEN
407
+ INSERT (id, date, data) VALUES (s.id, s.date, s.data)
408
+ `
409
+ ```
410
+
411
+ Якщо частина колонок у SET/INSERT залежить від параметра (plan/fact, тип тощо) — динамічні імена колонок не можна параметризувати через `${value}`; використовуй умовні Bun SQL фрагменти:
412
+
413
+ ```javascript
414
+ // ✅ умовні фрагменти для динамічних ідентифікаторів колонок
415
+ const colFrag = isPlan ? pgWrite`plan_value` : pgWrite`fact_value`
416
+ const hashFrag = isPlan ? pgWrite`hash = s.hash,` : pgWrite``
417
+
418
+ await pgWrite`
419
+ ...
420
+ WHEN MATCHED THEN
421
+ UPDATE SET
422
+ ${colFrag} = s.value,
423
+ ${hashFrag}
424
+ updated_by = s.updated_by
425
+ WHEN NOT MATCHED THEN
426
+ INSERT (id, ${colFrag}, updated_by)
427
+ VALUES (s.id, s.value, s.updated_by)
428
+ `
429
+ ```
430
+
431
+ #### Заборонений «drop-in» шим
432
+
433
+ ```javascript
434
+ // ❌ pg-format-сумісний шим, що ховає `unsafe` під «безпечним» іменем
435
+ export function format(fmt, ...args) {
436
+ let i = 0
437
+ return fmt.replaceAll(/%[LIs]/g, () => quoteLiteral(args[i++]))
438
+ }
439
+
440
+ // ❌ і його типовий call-site — той самий injection-вектор, що і прямий sql.unsafe із конкатенацією
441
+ await sql.unsafe(format('... WHERE id = %L', userId))
442
+ ```
443
+
444
+ ```javascript
445
+ // ❌ pg-сумісна обгортка над Bun SQL — ще один прихований `unsafe`
446
+ export const pgWrite = {
447
+ query(text, params) {
448
+ return sql.unsafe(text, params)
449
+ }
450
+ }
451
+ ```
452
+
453
+ ```javascript
454
+ // ✅ напряму tagged template — параметризація через wire-protocol bind
455
+ await sql`... WHERE id = ${userId}`
456
+ ```
457
+
458
+ Виняток для шиму `format()` під час поетапної міграції допускається **тільки** в окремому commit'і з TODO-маркером і дедлайном; готовий код у main з таким шимом — `fail` правила (детектори у `lib/bun-sql-scan.mjs`: `findPgFormatShimDefinitionInText`, `findPgFormatLikeQueryWrapperInText`).
@@ -0,0 +1,11 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/js-bun-redis
4
+ resource: plugins/lang-js/rules/js-bun-redis/
5
+ ---
6
+
7
+ # npm/rules/js-bun-redis
8
+
9
+ | Файл | Тип |
10
+ | ------------------- | --------- |
11
+ | [main.mjs](main.md) | JS Module |
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "lint": { "scope": "full", "glob": ["**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}", "**/package.json"] }
4
+ }
@@ -0,0 +1,36 @@
1
+ ---
2
+ type: JS Module
3
+ title: main.mjs
4
+ resource: plugins/lang-js/rules/js-bun-redis/imports/main.mjs
5
+ docgen:
6
+ crc: dc513509
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ issues: judge:inaccurate:0.98
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ Код сканує джерела JavaScript/TypeScript-коду, визначаючи шляхи для ігнорування на основі конфігурації, що спирається на `package.json`. Він виявляє використання заборонених бібліотек Redis (`ioredis`, `node-redis`, `redis`) у коді, щоб гарантувати відповідність використанню вбудованого Redis Bun, який доступний за https://bun.com/docs/runtime/redis.
16
+
17
+ ## Поведінка
18
+
19
+ Поведінка
20
+
21
+ 1. Перевіряє наявність файлу package.json у корені проєкту. Якщо відсутній, перевірку пропускає.
22
+ 2. Визначає шляхи, які ігнорувати під час пошуку коду, спираючись на конфігурацію проєкту.
23
+ 3. Збирає абсолютні шляхи всіх JS/TS-джерел у проєкті, які підлягають скануванню.
24
+ 4. Якщо джерел для сканування немає, перевірку вважає успішною.
25
+ 5. Сканує знайдені JS/TS-джерела на використання заборонених імпортів пакетів `ioredis`, `node-redis` або `redis`.
26
+ 6. При виявленні забороненого імпорту, реєструє порушення з вказівкою файлу, рядка та фрагмента коду.
27
+ 7. У разі виявлення порушень, повідомляє про них та вказує на необхідність заміни на вбудований Redis Bun за посиланням https://bun.com/docs/runtime/redis.
28
+ 8. Якщо порушень не знайдено, вважає, що проєкт відповідає вимогам щодо використання Bun native Redis.
29
+
30
+ ## Публічний API
31
+
32
+ main — Перевіряє, чи відповідає проєкт вимогам, встановленим у правилі `js-bun-redis.mdc`.
33
+
34
+ ## Гарантії поведінки
35
+
36
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -0,0 +1,47 @@
1
+ ## Сканування заборонених redis-імпортів у JS/TS-джерелах
2
+
3
+ Правило сканує всі JS/TS-файли проєкту (`.js`, `.ts`, `.mjs`, `.cjs`, `.jsx`, `.tsx` тощо, без `.d.ts`) на наявність `import`, `require()` або динамічного `import()` з заборонених пакетів:
4
+
5
+ - `ioredis` та підшляхи (`ioredis/…`)
6
+ - `node-redis`
7
+ - `redis` та підшляхи (`redis/…`)
8
+ - `@redis/client`, `@redis/json`, `@redis/search`, `@redis/time-series`, `@redis/bloom`
9
+
10
+ Усі ці пакети слід замінити на **Bun native Redis**: `import { redis } from 'bun'` — документація: <https://bun.com/docs/runtime/redis>.
11
+
12
+ Сканування виконується через **oxc-parser** по AST (не regex по тексту) — статичні імпорти, `require()`-виклики і динамічні `import()` окремо.
13
+
14
+ При синтаксичних помилках у файлі результат — порожній (спочатку виправити синтаксис).
15
+
16
+ Директорії, зазначені у `.n-rules-ignore` / `cursorignore` конфігурації, повністю пропускаються.
17
+
18
+ ### Повідомлення про порушення
19
+
20
+ ```
21
+ js-bun-redis: src/conn/redis.ts:3 — заміни 'ioredis' на Bun native Redis (import { redis } from 'bun', https://bun.com/docs/runtime/redis): import IORedis from 'ioredis'
22
+ ```
23
+
24
+ ### Міграція
25
+
26
+ ```ts
27
+ // До (ioredis)
28
+ import IORedis from 'ioredis'
29
+ const client = new IORedis({ host: 'localhost', port: 6379 })
30
+ await client.set('key', 'value')
31
+
32
+ // Після (Bun native Redis)
33
+ import { redis } from 'bun'
34
+ await redis.set('key', 'value')
35
+ ```
36
+
37
+ ```ts
38
+ // До (node-redis / redis v4)
39
+ import { createClient } from 'redis'
40
+ const client = createClient()
41
+ await client.connect()
42
+ await client.set('key', 'value')
43
+
44
+ // Після (Bun native Redis)
45
+ import { redis } from 'bun'
46
+ await redis.set('key', 'value')
47
+ ```
@@ -0,0 +1,88 @@
1
+ /** @see ./docs/imports.md */
2
+ import { existsSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { join, relative } from 'node:path'
5
+
6
+ import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
7
+ import { loadCursorIgnorePaths } from '@7n/rules/scripts/lib/load-cursor-config.mjs'
8
+ import { findRedisImportsInText, isRedisScanSourceFile, shouldSkipFileForRedisScan } from '../lib/redis-imports.mjs'
9
+ import { walkDir } from '@7n/rules/scripts/utils/walkDir.mjs'
10
+
11
+ /**
12
+ * Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану заборонених redis-імпортів.
13
+ * @param {string} repoRoot абсолютний шлях до кореня репозиторію
14
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
15
+ * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
16
+ */
17
+ async function findAllSourcePathsForRedisScan(repoRoot, ignorePaths) {
18
+ /** @type {string[]} */
19
+ const paths = []
20
+ await walkDir(
21
+ repoRoot,
22
+ absPath => {
23
+ const rel = relative(repoRoot, absPath).split('\\').join('/')
24
+ if (isRedisScanSourceFile(rel) && !shouldSkipFileForRedisScan(rel)) {
25
+ paths.push(absPath)
26
+ }
27
+ },
28
+ ignorePaths
29
+ )
30
+ paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
31
+ return paths
32
+ }
33
+
34
+ /**
35
+ * Сканує JS/TS-джерела на заборонені імпорти/require пакетів `ioredis` / `node-redis` / `redis`.
36
+ * @param {string[]} sourcePaths абсолютні шляхи джерел
37
+ * @param {string} repoRoot абсолютний шлях до кореня
38
+ * @param {(msg: string) => void} fail callback при помилці
39
+ * @returns {Promise<number>} кількість знайдених порушень
40
+ */
41
+ async function scanSourcesForRedisImports(sourcePaths, repoRoot, fail) {
42
+ let violations = 0
43
+ for (const absPath of sourcePaths) {
44
+ const rel = relative(repoRoot, absPath).split('\\').join('/')
45
+ const content = await readFile(absPath, 'utf8')
46
+ for (const v of findRedisImportsInText(content, rel)) {
47
+ violations++
48
+ fail(
49
+ `js-bun-redis: ${rel}:${v.line} — заміни '${v.module}' на Bun native Redis ` +
50
+ `(import { redis } from 'bun', https://bun.com/docs/runtime/redis): ${v.snippet}`
51
+ )
52
+ }
53
+ }
54
+ return violations
55
+ }
56
+
57
+ /**
58
+ * Перевіряє відповідність проєкту правилу `js-bun-redis.mdc`.
59
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
60
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} перелік порушень
61
+ */
62
+ export async function lint(ctx) {
63
+ const reporter = createViolationReporter(ctx)
64
+ const { pass, fail } = reporter
65
+
66
+ const repoRoot = ctx.cwd
67
+ if (!existsSync(join(repoRoot, 'package.json'))) {
68
+ pass('js-bun-redis: package.json у корені відсутній — перевірку пропущено')
69
+ return reporter.result()
70
+ }
71
+
72
+ const ignorePaths = await loadCursorIgnorePaths(repoRoot)
73
+ const sourcePaths = await findAllSourcePathsForRedisScan(repoRoot, ignorePaths)
74
+ if (sourcePaths.length === 0) {
75
+ pass('js-bun-redis: немає JS/TS файлів для скану імпортів ioredis / node-redis / redis')
76
+ return reporter.result()
77
+ }
78
+
79
+ const violations = await scanSourcesForRedisImports(sourcePaths, repoRoot, fail)
80
+ if (violations === 0) {
81
+ pass(
82
+ "js-bun-redis: немає імпортів 'ioredis' / 'node-redis' / 'redis' / '@redis/*' у джерелах " +
83
+ '(використовується Bun native Redis або redis взагалі не задіяно)'
84
+ )
85
+ }
86
+
87
+ return reporter.result()
88
+ }