@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,430 @@
1
+ /** @see ./docs/safety.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 {
8
+ findBunSqlPerRequestConnectionInText,
9
+ findBunSqlPgLeftoverCallInText,
10
+ findBunSqlUnsafeUseWithoutAllowMarkerInText,
11
+ findBunSqlUnsafeWithInterpolatedTemplateInText,
12
+ findJsonStringifyBeforeJsonbInText,
13
+ findPgFormatLikeQueryWrapperInText,
14
+ findPgFormatShimDefinitionInText,
15
+ findPgLibImportInText,
16
+ findPgListenNotifyUsageInText,
17
+ findSqlArrayWithoutTypeArgInText,
18
+ findUnsafeBunSqlDynamicSqlListInText,
19
+ findUnsafeBunSqlInListMissingEmptyGuardInText,
20
+ isBunSqlScanSourceFile,
21
+ textHasBunSqlImport,
22
+ textHasPgLibImport
23
+ } from '../lib/bun-sql-scan.mjs'
24
+ import { findAllPackageJsonPaths } from '@7n/rules/scripts/utils/find-package-json-paths.mjs'
25
+ import { loadCursorIgnorePaths } from '@7n/rules/scripts/lib/load-cursor-config.mjs'
26
+ import { walkDir } from '@7n/rules/scripts/utils/walkDir.mjs'
27
+
28
+ // Дешеві pre-filter regex'и для AST-сканера LISTEN/NOTIFY: уникаємо парсингу
29
+ // файлів, у яких ніяких сигналів немає. Винесено в модульний скоуп, щоб не
30
+ // перекомпілювати RegExp на кожному виклику `collectPgUsageForFile`.
31
+ const LISTEN_NOTIFY_KEYWORD_RE = /\b(LISTEN|UNLISTEN|NOTIFY)\b/iu
32
+ const NOTIFICATION_LITERAL_RE = /['"`]notification['"`]/u
33
+
34
+ /**
35
+ * Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану Bun SQL патернів.
36
+ * @param {string} repoRoot абсолютний шлях до кореня репозиторію
37
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
38
+ * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
39
+ */
40
+ async function findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths) {
41
+ /** @type {string[]} */
42
+ const paths = []
43
+ await walkDir(
44
+ repoRoot,
45
+ absPath => {
46
+ const rel = relative(repoRoot, absPath).split('\\').join('/')
47
+ if (isBunSqlScanSourceFile(rel)) {
48
+ paths.push(absPath)
49
+ }
50
+ },
51
+ ignorePaths
52
+ )
53
+ paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
54
+ return paths
55
+ }
56
+
57
+ /**
58
+ * Сканує JS/TS-джерела на небезпечні патерни Bun SQL і збирає метадані про
59
+ * використання `pg`/LISTEN-NOTIFY (для виключення dependency `pg`).
60
+ * @param {string[]} sourcePaths абсолютні шляхи джерел
61
+ * @param {string} repoRoot абсолютний шлях до кореня
62
+ * @param {{ pass: (m: string) => void, fail: (m: string) => void }} reporter колбеки pass і fail з перевірки
63
+ * @returns {Promise<{
64
+ * hasBunSqlImport: boolean,
65
+ * perRequest: number,
66
+ * unsafeCall: number,
67
+ * dynamicList: number,
68
+ * inListGuard: number,
69
+ * pgLeftover: number,
70
+ * pgFormatShim: number,
71
+ * queryWrapper: number,
72
+ * pgUsage: { rel: string, imports: { line: number, snippet: string }[], listenNotify: { line: number, snippet: string, kind: string }[] }[]
73
+ * }>}
74
+ * `hasBunSqlImport` — чи є хоч один `import { sql|SQL } from 'bun'`;
75
+ * `pgUsage` — список файлів, що або імпортують `'pg'`, або містять LISTEN/NOTIFY-патерн
76
+ * (інші — пропущено, щоб не тримати в пам'яті метадані про всі файли).
77
+ */
78
+ async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
79
+ const { fail } = reporter
80
+ const counts = {
81
+ perRequest: 0,
82
+ unsafeCall: 0,
83
+ unsafeTemplateInterp: 0,
84
+ dynamicList: 0,
85
+ inListGuard: 0,
86
+ pgLeftover: 0,
87
+ pgFormatShim: 0,
88
+ queryWrapper: 0,
89
+ jsonStringifyJsonb: 0,
90
+ sqlArrayNoType: 0
91
+ }
92
+ let hasBunSqlImport = false
93
+ /** @type {{ rel: string, imports: { line: number, snippet: string }[], listenNotify: { line: number, snippet: string, kind: string }[] }[]} */
94
+ const pgUsage = []
95
+
96
+ for (const absPath of sourcePaths) {
97
+ const rel = relative(repoRoot, absPath).split('\\').join('/')
98
+ const content = await readFile(absPath, 'utf8')
99
+ if (!hasBunSqlImport && textHasBunSqlImport(content)) {
100
+ hasBunSqlImport = true
101
+ }
102
+ scanFileForBunSqlPatterns(content, rel, fail, counts)
103
+ collectPgUsageForFile(content, rel, pgUsage)
104
+ }
105
+
106
+ return { hasBunSqlImport, pgUsage, ...counts }
107
+ }
108
+
109
+ /**
110
+ * Якщо у файлі є імпорт `'pg'` АБО LISTEN/NOTIFY-патерн — додає запис у `pgUsage`.
111
+ * Файли без жодного сигналу не зберігаються, щоб уникнути зайвої пам'яті.
112
+ * @param {string} content вміст файлу
113
+ * @param {string} rel posix-шлях відносно кореня
114
+ * @param {{ rel: string, imports: { line: number, snippet: string }[], listenNotify: { line: number, snippet: string, kind: string }[] }[]} pgUsage акумулятор
115
+ * @returns {void}
116
+ */
117
+ function collectPgUsageForFile(content, rel, pgUsage) {
118
+ // Дешевий pre-filter за текстом: AST-парсинг тільки коли файл містить
119
+ // або імпорт `'pg'`, або хоча б одне зі слів LISTEN / NOTIFY / UNLISTEN /
120
+ // 'notification' — інакше LISTEN/NOTIFY у ньому точно немає.
121
+ const mayHaveListenNotify = LISTEN_NOTIFY_KEYWORD_RE.test(content) || NOTIFICATION_LITERAL_RE.test(content)
122
+ if (!textHasPgLibImport(content) && !mayHaveListenNotify) return
123
+ const imports = findPgLibImportInText(content, rel)
124
+ const listenNotify = findPgListenNotifyUsageInText(content, rel)
125
+ if (imports.length === 0 && listenNotify.length === 0) return
126
+ pgUsage.push({ rel, imports, listenNotify })
127
+ }
128
+
129
+ /**
130
+ * Сканує один файл усіма AST-сканерами bun-sql і реєструє знайдені порушення.
131
+ * @param {string} content вміст файлу
132
+ * @param {string} rel posix-шлях відносно `repoRoot`
133
+ * @param {(msg: string) => void} fail callback при помилці
134
+ * @param {{ perRequest: number, unsafeCall: number, unsafeTemplateInterp: number, dynamicList: number, inListGuard: number, pgLeftover: number, pgFormatShim: number, queryWrapper: number, jsonStringifyJsonb: number, sqlArrayNoType: number }} counts акумулятори
135
+ * @returns {void}
136
+ */
137
+ function scanFileForBunSqlPatterns(content, rel, fail, counts) {
138
+ for (const v of findBunSqlPerRequestConnectionInText(content, rel)) {
139
+ counts.perRequest++
140
+ fail(
141
+ `js-bun-db: ${rel}:${v.line} — не створюй new SQL(...) всередині функцій; ` +
142
+ `тримай singleton на рівні модуля (js-bun-db.mdc): ${v.snippet}`
143
+ )
144
+ }
145
+ for (const v of findBunSqlUnsafeUseWithoutAllowMarkerInText(content, rel)) {
146
+ counts.unsafeCall++
147
+ fail(
148
+ `js-bun-db: ${rel}:${v.line} — sql.unsafe(...) заборонено за замовчуванням; ` +
149
+ `допустимо лише для підстановки назви таблиці/колонки чи dynamic SQL/DDL з code-controlled значенням, ` +
150
+ `інакше переробити на tagged template sql\`...\${value}...\`. ` +
151
+ `Якщо випадок легітимний — додай маркер "// allow-unsafe: <причина>" на тому ж рядку або рядком вище ` +
152
+ `(js-bun-db.mdc): ${v.snippet}`
153
+ )
154
+ }
155
+ for (const v of findBunSqlUnsafeWithInterpolatedTemplateInText(content, rel)) {
156
+ counts.unsafeTemplateInterp++
157
+ fail(
158
+ `js-bun-db: ${rel}:${v.line} — sql.unsafe(\`...\${x}...\`) з template-літералом і \${...}-інтерполяцією ` +
159
+ `заборонено навіть з allow-unsafe маркером: шаблонна підстановка identifier'у не екранує (reserved words, ` +
160
+ `спецсимволи), а значення не біндяться. Збери text через @scaleleap/pg-format format('%I', name) для ` +
161
+ `identifiers або позиційні $N для values, потім sql.unsafe(text, [params]). Деталі — секція ` +
162
+ `«Динамічна SQL-структура» в js-bun-db.mdc: ${v.snippet}`
163
+ )
164
+ }
165
+ for (const v of findBunSqlPgLeftoverCallInText(content, rel)) {
166
+ counts.pgLeftover++
167
+ fail(
168
+ `js-bun-db: ${rel}:${v.line} — pg-leftover виклик .${v.methodName}(...): Bun SQL пулом керує сам, ` +
169
+ `видали зайвий .connect()/.end() або, якщо випадок легітимний (graceful shutdown тощо), ` +
170
+ `додай маркер "// allow-pg-leftover: <причина>" на тому ж рядку або рядком вище ` +
171
+ `(js-bun-db.mdc): ${v.snippet}`
172
+ )
173
+ }
174
+ for (const v of findUnsafeBunSqlDynamicSqlListInText(content, rel)) {
175
+ counts.dynamicList++
176
+ fail(
177
+ `js-bun-db: ${rel}:${v.line} — заборонено підставляти у SQL динамічні списки через .join(',') ` +
178
+ `у IN (...) / VALUES (...); використовуй sql([...]) (js-bun-db.mdc): ${v.snippet}`
179
+ )
180
+ }
181
+ for (const v of findUnsafeBunSqlInListMissingEmptyGuardInText(content, rel)) {
182
+ counts.inListGuard++
183
+ fail(messageForBunSqlInListGuard(rel, v))
184
+ }
185
+ for (const v of findPgFormatShimDefinitionInText(content, rel)) {
186
+ counts.pgFormatShim++
187
+ if (v.kind === 'format_function') {
188
+ fail(
189
+ `js-bun-db: ${rel}:${v.line} — функція ${JSON.stringify(v.name)} виглядає як pg-format-сумісний шим ` +
190
+ `(тіло містить %L / %I / %s). Видали шим і переведи всі call-site на tagged template ` +
191
+ `sql\`...\${value}...\` (js-bun-db.mdc): ${v.snippet}`
192
+ )
193
+ } else {
194
+ fail(
195
+ `js-bun-db: ${rel}:${v.line} — ${JSON.stringify(v.name)} — це pg-format-специфічний escape-хелпер; ` +
196
+ `з Bun SQL він не потрібен (параметризація через tagged template), видали і перепиши call-site ` +
197
+ `(js-bun-db.mdc): ${v.snippet}`
198
+ )
199
+ }
200
+ }
201
+ for (const v of findPgFormatLikeQueryWrapperInText(content, rel)) {
202
+ counts.queryWrapper++
203
+ fail(
204
+ `js-bun-db: ${rel}:${v.line} — query(text, params)-обгортка над <obj>.unsafe(...) — це прихований ` +
205
+ `pg-сумісний шим. Видали обгортку (pgRead/pgWrite/db.query) і переведи всі call-site на tagged template ` +
206
+ `sql\`...\${value}...\` (js-bun-db.mdc): ${v.snippet}`
207
+ )
208
+ }
209
+ for (const v of findJsonStringifyBeforeJsonbInText(content, rel)) {
210
+ counts.jsonStringifyJsonb++
211
+ fail(
212
+ `js-bun-db: ${rel}:${v.line} — JSON.stringify(...) перед ::jsonb зайвий: Bun SQL серіалізує ` +
213
+ `об'єкти/масиви у JSON автоматично, явний stringify призводить до подвійної серіалізації ` +
214
+ `(js-bun-db.mdc query-safety): ${v.snippet}`
215
+ )
216
+ }
217
+ for (const v of findSqlArrayWithoutTypeArgInText(content, rel)) {
218
+ counts.sqlArrayNoType++
219
+ fail(
220
+ `js-bun-db: ${rel}:${v.line} — sql.array(arr) без другого аргументу типу — ` +
221
+ `вкажи явний pg-тип: sql.array(arr, 'int8') / sql.array(arr, 'uuid') тощо ` +
222
+ `(js-bun-db.mdc sql-array): ${v.snippet}`
223
+ )
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Перевіряє виключення `pg` для LISTEN/NOTIFY: по кожному `package.json` з
229
+ * `dependencies.pg` — чи є у проекті хоч одне використання LISTEN/NOTIFY-патерну;
230
+ * додатково — кожен файл з `import 'pg'` повинен сам містити LISTEN/NOTIFY (інакше
231
+ * звичайні SELECT/INSERT/UPDATE через `pg` ховаються за легітимним dependency).
232
+ * @param {string[]} pkgJsonPaths абсолютні шляхи до всіх package.json
233
+ * @param {string} repoRoot абсолютний шлях до кореня
234
+ * @param {{ rel: string, imports: { line: number, snippet: string }[], listenNotify: { line: number, snippet: string, kind: string }[] }[]} pgUsage метадані з scanSourcesForBunSqlPatterns
235
+ * @param {{ fail: (m: string) => void }} reporter колбек fail для повідомлень
236
+ * @returns {Promise<{ pgDepFails: number, pgImportFails: number, pgDepsFound: number, hasAnyListenNotify: boolean, listenNotifyEvidence: string | null }>}
237
+ * counters і метадані для підсумкового `pass`-повідомлення (де саме знайдено перший LISTEN/NOTIFY).
238
+ */
239
+ async function checkPgDependencyAndUsage(pkgJsonPaths, repoRoot, pgUsage, reporter) {
240
+ const { fail } = reporter
241
+ let pgDepFails = 0
242
+ let pgImportFails = 0
243
+ let pgDepsFound = 0
244
+
245
+ const firstWithListenNotify = pgUsage.find(u => u.listenNotify.length > 0)
246
+ const hasAnyListenNotify = !!firstWithListenNotify
247
+ const listenNotifyEvidence = firstWithListenNotify
248
+ ? `${firstWithListenNotify.rel}:${firstWithListenNotify.listenNotify[0].line}`
249
+ : null
250
+
251
+ for (const absPkgPath of pkgJsonPaths) {
252
+ const relPkg = relative(repoRoot, absPkgPath).split('\\').join('/')
253
+ let pkg
254
+ try {
255
+ pkg = JSON.parse(await readFile(absPkgPath, 'utf8'))
256
+ } catch {
257
+ // невалідний JSON у package.json — це проблема інших правил, тут пропускаємо
258
+ continue
259
+ }
260
+ if (!pkg || typeof pkg !== 'object') continue
261
+ const deps = pkg.dependencies
262
+ if (!deps || typeof deps !== 'object' || !Object.hasOwn(deps, 'pg')) continue
263
+ pgDepsFound++
264
+ if (!hasAnyListenNotify) {
265
+ pgDepFails++
266
+ fail(
267
+ `js-bun-db: ${relPkg}: dependencies.pg заборонено — у проекті не знайдено LISTEN / NOTIFY / UNLISTEN ` +
268
+ `(або listener'а .on('notification', ...)). Bun SQL покриває звичайні запити; ` +
269
+ `\`pg\` дозволений лише як виняток для LISTEN/NOTIFY (js-bun-db.mdc, ` +
270
+ `секція «pg для LISTEN/NOTIFY»)`
271
+ )
272
+ }
273
+ }
274
+
275
+ for (const f of pgUsage) {
276
+ if (f.imports.length === 0) continue
277
+ if (f.listenNotify.length > 0) continue
278
+ for (const imp of f.imports) {
279
+ pgImportFails++
280
+ fail(
281
+ `js-bun-db: ${f.rel}:${imp.line} — import 'pg' дозволено лише у файлах з LISTEN / NOTIFY / UNLISTEN ` +
282
+ `або .on('notification', ...). Перенеси звичайні запити на Bun SQL ` +
283
+ `(import { sql } from 'bun'), а LISTEN/NOTIFY-логіку лиши в окремому модулі ` +
284
+ `(js-bun-db.mdc): ${imp.snippet}`
285
+ )
286
+ }
287
+ }
288
+
289
+ return { pgDepFails, pgImportFails, pgDepsFound, hasAnyListenNotify, listenNotifyEvidence }
290
+ }
291
+
292
+ /**
293
+ * Будує повідомлення `fail` для порушення `findUnsafeBunSqlInListMissingEmptyGuardInText`
294
+ * залежно від `reason` (різні діагностики однакового сімейства).
295
+ * @param {string} rel posix-шлях відносно кореня репо
296
+ * @param {{ line: number, snippet: string, name?: string, reason: string }} v порушення
297
+ * @returns {string} готове повідомлення для `fail`
298
+ */
299
+ function messageForBunSqlInListGuard(rel, v) {
300
+ if (v.reason === 'missing_guard') {
301
+ return (
302
+ `js-bun-db: ${rel}:${v.line} — перед IN-списком ${JSON.stringify(v.name)} потрібна перевірка на пустоту ` +
303
+ `з throw (наприклад if (!${v.name}.length) throw ...), інакше можливі некоректні запити (js-bun-db.mdc): ${v.snippet}`
304
+ )
305
+ }
306
+ if (v.reason === 'sql_helper_not_var') {
307
+ return (
308
+ `js-bun-db: ${rel}:${v.line} — IN-список у \${sql(...)} має підставлятись зі змінної (Identifier) ` +
309
+ `після валідації на пустоту + throw (js-bun-db.mdc): ${v.snippet}`
310
+ )
311
+ }
312
+ return (
313
+ `js-bun-db: ${rel}:${v.line} — значення для IN (...) у template literal треба винести в окрему змінну ` +
314
+ `і перевірити на пустоту (throw), не підставляти вираз напряму (js-bun-db.mdc): ${v.snippet}`
315
+ )
316
+ }
317
+
318
+ /**
319
+ * Перевіряє відповідність проєкту правилу js-bun-db.mdc
320
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
321
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} перелік порушень
322
+ */
323
+ export async function lint(ctx) {
324
+ const cwd = ctx.cwd
325
+ const reporter = createViolationReporter(ctx)
326
+ const { pass } = reporter
327
+
328
+ const repoRoot = cwd
329
+ const rootPkg = join(repoRoot, 'package.json')
330
+ if (!existsSync(rootPkg)) {
331
+ pass('js-bun-db: package.json у корені відсутній — перевірку пропущено')
332
+ return reporter.result()
333
+ }
334
+
335
+ const ignorePaths = await loadCursorIgnorePaths(repoRoot)
336
+ const pkgJsonPaths = await findAllPackageJsonPaths(repoRoot, ignorePaths)
337
+ if (pkgJsonPaths.length === 0) {
338
+ pass('js-bun-db: package.json не знайдено — перевірку пропущено')
339
+ return reporter.result()
340
+ }
341
+
342
+ // Заборону `pg-format` / `mysql2` у `dependencies` тримає Rego-поліс
343
+ // `npm/policy/js_bun_db/package_json/`. `pg` оброблено тут — як виняток для
344
+ // LISTEN/NOTIFY (Rego не бачить JS-коду, тож не може зважити сигнал).
345
+
346
+ const sourcePaths = await findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths)
347
+ if (sourcePaths.length === 0) {
348
+ pass('js-bun-db: немає JS/TS файлів для скану патернів Bun SQL')
349
+ return reporter.result()
350
+ }
351
+
352
+ const {
353
+ hasBunSqlImport,
354
+ pgUsage,
355
+ perRequest,
356
+ unsafeCall,
357
+ unsafeTemplateInterp,
358
+ dynamicList,
359
+ inListGuard,
360
+ pgLeftover,
361
+ pgFormatShim,
362
+ queryWrapper,
363
+ jsonStringifyJsonb,
364
+ sqlArrayNoType
365
+ } = await scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter)
366
+
367
+ const { pgDepFails, pgImportFails, pgDepsFound, listenNotifyEvidence } = await checkPgDependencyAndUsage(
368
+ pkgJsonPaths,
369
+ repoRoot,
370
+ pgUsage,
371
+ reporter
372
+ )
373
+ if (pgDepFails === 0) {
374
+ if (pgDepsFound === 0) {
375
+ pass('js-bun-db: dependencies.pg відсутнє у жодному package.json')
376
+ } else {
377
+ pass(
378
+ `js-bun-db: dependencies.pg виправдано LISTEN/NOTIFY у коді (виключення з js-bun-db.mdc; ` +
379
+ `доказ: ${listenNotifyEvidence})`
380
+ )
381
+ }
382
+ }
383
+ if (pgImportFails === 0) {
384
+ pass("js-bun-db: усі `import 'pg'` або відсутні, або у файлах з LISTEN/NOTIFY")
385
+ }
386
+
387
+ if (!hasBunSqlImport) {
388
+ pass("js-bun-db: Bun SQL не використовується в коді (немає import { sql|SQL } from 'bun')")
389
+ return reporter.result()
390
+ }
391
+
392
+ if (perRequest === 0) {
393
+ pass('js-bun-db: немає створення new SQL(...) всередині функцій (singleton на рівні модуля)')
394
+ }
395
+ if (unsafeCall === 0) {
396
+ pass('js-bun-db: усі sql.unsafe(...) або відсутні, або супроводжуються маркером "// allow-unsafe: <причина>"')
397
+ }
398
+ if (unsafeTemplateInterp === 0) {
399
+ pass(
400
+ 'js-bun-db: немає sql.unsafe(template literal з інтерполяцією) ' +
401
+ '(identifiers через @scaleleap/pg-format %I, values — позиційні $N)'
402
+ )
403
+ }
404
+ if (pgLeftover === 0) {
405
+ pass(
406
+ 'js-bun-db: немає pg-leftover викликів .connect()/.end() у файлах з Bun SQL ' +
407
+ '(або всі вони мають маркер "// allow-pg-leftover: <причина>")'
408
+ )
409
+ }
410
+ if (dynamicList === 0) {
411
+ pass("js-bun-db: немає небезпечних динамічних SQL-списків через .join(',') у IN/VALUES")
412
+ }
413
+ if (inListGuard === 0) {
414
+ pass('js-bun-db: усі IN-списки винесені у змінні та мають перевірку на пустоту з throw')
415
+ }
416
+ if (pgFormatShim === 0) {
417
+ pass('js-bun-db: немає pg-format-сумісних шимів (format/quoteLiteral/quoteIdent/...) у файлах з Bun SQL')
418
+ }
419
+ if (queryWrapper === 0) {
420
+ pass('js-bun-db: немає query(text, params)-обгорток над unsafe(...) у файлах з Bun SQL')
421
+ }
422
+ if (jsonStringifyJsonb === 0) {
423
+ pass('js-bun-db: немає JSON.stringify(...) перед ::jsonb — Bun SQL серіалізує автоматично')
424
+ }
425
+ if (sqlArrayNoType === 0) {
426
+ pass('js-bun-db: усі sql.array() мають явний аргумент типу')
427
+ }
428
+
429
+ return reporter.result()
430
+ }