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

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 +12 -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 +31 -0
  67. package/rules/js/knip/knip.mdc +15 -0
  68. package/rules/js/knip/main.mjs +89 -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,610 @@
1
+ /**
2
+ * Знаходить небезпечні патерни використання `mssql`, які створюють підключення/пул
3
+ * всередині функцій (наприклад handler на кожен запит), замість того щоб мати один
4
+ * singleton `sql.ConnectionPool` на рівні модуля та повторно використовувати його.
5
+ *
6
+ * Також знаходить небезпечний виклик `query(\`...\`)` — це НЕ tagged template, а звичайний
7
+ * виклик з інтерполяцією рядка, який може призвести до SQL injection. Натомість має
8
+ * використовуватись tagged template `query\`...\`` (див. js-mssql.mdc).
9
+ *
10
+ * Додатково знаходить:
11
+ * - shared `Request` (наприклад `export const request = pool.request()`), який не можна
12
+ * повторно використовувати між запитами.
13
+ * - небезпечні “динамічні списки” в SQL, коли в TemplateLiteral/TaggedTemplateExpression
14
+ * підставляють рядки, зібрані через `.join(',')` (типово для `IN (...)` або `VALUES (...)`).
15
+ * - підстановки `IN (${expr})`, де `expr` не пройшов числовий парсер (parseInt/Number/BigInt
16
+ * /parseFloat або унарний `+`) і не є літеральним масивом чисел — навіть у tagged template
17
+ * це додатковий шар захисту від SQL injection (див. js-mssql.mdc).
18
+ *
19
+ * Семантика береться з **oxc-parser** по AST, щоб не покладатися на regex.
20
+ * Якщо файл не парситься або містить синтаксичні помилки — повертаємо порожній
21
+ * результат (спочатку треба полагодити синтаксис, потім перезапустити перевірку).
22
+ */
23
+ import { parseSync } from 'oxc-parser'
24
+
25
+ import {
26
+ isFunctionNode,
27
+ isJoinCall,
28
+ isSqlListContextTemplate,
29
+ langFromPath,
30
+ normalizeSnippet,
31
+ offsetToLine,
32
+ walkAstWithAncestors
33
+ } from '@7n/rules/scripts/utils/ast-scan-utils.mjs'
34
+
35
+ const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
36
+ const IN_PLACEHOLDER_END_RE = /\bin\s*\(\s*$/iu
37
+ const NUMERIC_PARSE_FN_NAMES = new Set(['parseInt', 'parseFloat', 'Number', 'BigInt'])
38
+
39
+ /**
40
+ * Чи є вузол літералом числа `0` (як NumericLiteral, так і generic Literal).
41
+ * @param {unknown} node AST-вузол
42
+ * @returns {boolean} true для `0`
43
+ */
44
+ function isZeroLiteral(node) {
45
+ return (
46
+ !!node &&
47
+ typeof node === 'object' &&
48
+ ((node.type === 'NumericLiteral' && node.value === 0) || (node.type === 'Literal' && node.value === 0))
49
+ )
50
+ }
51
+
52
+ /**
53
+ * Чи `node` — це MemberExpression `name.length` (некомп'ютерований Identifier.Identifier).
54
+ * @param {unknown} node AST-вузол
55
+ * @param {string} name очікуване імʼя змінної-власника
56
+ * @returns {boolean} true для `<name>.length`
57
+ */
58
+ function isLengthMemberOf(node, name) {
59
+ return (
60
+ !!node &&
61
+ typeof node === 'object' &&
62
+ node.type === 'MemberExpression' &&
63
+ !node.computed &&
64
+ !!node.object &&
65
+ node.object.type === 'Identifier' &&
66
+ node.object.name === name &&
67
+ !!node.property &&
68
+ node.property.type === 'Identifier' &&
69
+ node.property.name === 'length'
70
+ )
71
+ }
72
+
73
+ const EMPTY_LIST_BINARY_OPERATORS = new Set(['===', '==', '<=', '<'])
74
+ const EMPTY_LIST_REVERSED_OPERATORS = new Set(['===', '=='])
75
+
76
+ /**
77
+ * Чи `BinaryExpression`-тест з оператором з {@link EMPTY_LIST_BINARY_OPERATORS}
78
+ * порівнює `<name>.length` з літералом `0` (у будь-якому порядку для `===`/`==`).
79
+ * @param {Record<string, unknown>} test BinaryExpression вузол
80
+ * @param {string} name імʼя змінної списку
81
+ * @returns {boolean} true для умов виду `name.length === 0`, `name.length <= 0`, `0 === name.length` тощо
82
+ */
83
+ function isEmptyListBinaryTest(test, name) {
84
+ const { left, right, operator } = test
85
+ if (!EMPTY_LIST_BINARY_OPERATORS.has(operator)) return false
86
+ if (isLengthMemberOf(left, name) && isZeroLiteral(right)) return true
87
+ return EMPTY_LIST_REVERSED_OPERATORS.has(operator) && isZeroLiteral(left) && isLengthMemberOf(right, name)
88
+ }
89
+
90
+ /**
91
+ * Чи містить тест if-умови перевірку “список порожній”.
92
+ * Підтримує базові форми:
93
+ * - `if (!ids.length) ...`
94
+ * - `if (ids.length === 0) ...` / `<= 0` / `< 1`
95
+ * @param {unknown} test IfStatement.test
96
+ * @param {string} name імʼя змінної списку
97
+ * @returns {boolean} true, якщо тест перевіряє, що `name` — порожній масив
98
+ */
99
+ function isEmptyListTest(test, name) {
100
+ if (!test || typeof test !== 'object') return false
101
+ if (test.type === 'UnaryExpression' && test.operator === '!') {
102
+ return isLengthMemberOf(test.argument, name)
103
+ }
104
+ if (test.type === 'BinaryExpression') {
105
+ return isEmptyListBinaryTest(test, name)
106
+ }
107
+ return false
108
+ }
109
+
110
+ /**
111
+ * Чи є в consequent (або в його BlockStatement) ThrowStatement.
112
+ * @param {unknown} consequent IfStatement.consequent
113
+ * @returns {boolean} true, якщо гілка consequent містить `throw`
114
+ */
115
+ function consequentHasThrow(consequent) {
116
+ if (!consequent || typeof consequent !== 'object') return false
117
+ if (consequent.type === 'ThrowStatement') return true
118
+ if (consequent.type === 'BlockStatement' && Array.isArray(consequent.body)) {
119
+ return consequent.body.some(s => s && typeof s === 'object' && s.type === 'ThrowStatement')
120
+ }
121
+ return false
122
+ }
123
+
124
+ /**
125
+ * Шукає “guard” `if (empty) throw` перед statementIndex у межах того ж BlockStatement.
126
+ * @param {unknown} block BlockStatement
127
+ * @param {number} statementIndex індекс statement, перед яким шукаємо guard
128
+ * @param {string} name імʼя змінної списку
129
+ * @returns {boolean} true, якщо знайдено guard `if (empty(name)) throw` до statementIndex
130
+ */
131
+ function hasEmptyGuardBefore(block, statementIndex, name) {
132
+ if (!block || typeof block !== 'object' || block.type !== 'BlockStatement') return false
133
+ const body = block.body
134
+ if (!Array.isArray(body)) return false
135
+ for (let i = 0; i < statementIndex; i++) {
136
+ const st = body[i]
137
+ if (!st || typeof st !== 'object') continue
138
+ if (st.type !== 'IfStatement') continue
139
+ if (!isEmptyListTest(st.test, name)) continue
140
+ if (!consequentHasThrow(st.consequent)) continue
141
+ return true
142
+ }
143
+ return false
144
+ }
145
+
146
+ /**
147
+ * Знаходить найближчий enclosing BlockStatement і statement всередині нього.
148
+ * @param {unknown[]} ancestors ancestors масив з walkAstWithAncestors
149
+ * @returns {{ block: unknown, index: number } | null} пара (BlockStatement, індекс statement) або null
150
+ */
151
+ function findEnclosingBlockAndStatementIndex(ancestors) {
152
+ if (!Array.isArray(ancestors) || ancestors.length === 0) return null
153
+
154
+ // statement — перший зверху вузол, який лежить у block.body
155
+ // шукаємо пару (block, statement), де statement ∈ block.body
156
+ for (let i = ancestors.length - 1; i >= 1; i--) {
157
+ const maybeStatement = ancestors[i]
158
+ const maybeBlock = ancestors[i - 1]
159
+ if (!maybeBlock || typeof maybeBlock !== 'object' || maybeBlock.type !== 'BlockStatement') continue
160
+ if (!Array.isArray(maybeBlock.body)) continue
161
+ const idx = maybeBlock.body.indexOf(maybeStatement)
162
+ if (idx !== -1) return { block: maybeBlock, index: idx }
163
+ }
164
+ return null
165
+ }
166
+
167
+ /**
168
+ * Чи це `new sql.ConnectionPool(...)` або `new mssql.ConnectionPool(...)`.
169
+ * @param {unknown} node AST node
170
+ * @returns {boolean} true, якщо це створення ConnectionPool
171
+ */
172
+ function isNewConnectionPool(node) {
173
+ if (!node || node.type !== 'NewExpression') return false
174
+ const callee = node.callee
175
+ if (!callee || callee.type !== 'MemberExpression') return false
176
+ if (callee.computed) return false
177
+ const prop = callee.property
178
+ if (!prop || prop.type !== 'Identifier' || prop.name !== 'ConnectionPool') return false
179
+ const obj = callee.object
180
+ if (!obj || obj.type !== 'Identifier') return false
181
+ return obj.name === 'sql' || obj.name === 'mssql'
182
+ }
183
+
184
+ /**
185
+ * Чи це виклик `.query(...)` з TemplateLiteral як першим аргументом (`query(\`...\`)`).
186
+ * @param {unknown} node AST node
187
+ * @returns {boolean} true, якщо це небезпечний патерн `query(\`...\`)`
188
+ */
189
+ function isUnsafeQueryCallWithTemplateLiteral(node) {
190
+ if (!node || node.type !== 'CallExpression') return false
191
+ const callee = node.callee
192
+ if (!callee || callee.type !== 'MemberExpression') return false
193
+ if (callee.computed) return false
194
+ const prop = callee.property
195
+ if (!prop || prop.type !== 'Identifier' || prop.name !== 'query') return false
196
+ const args = node.arguments
197
+ if (!Array.isArray(args) || args.length === 0) return false
198
+ const first = args[0]
199
+ return !!first && typeof first === 'object' && first.type === 'TemplateLiteral'
200
+ }
201
+
202
+ /**
203
+ * Чи це `something.request()` (наприклад `pool.request()`), яку не можна шарити між запитами.
204
+ * @param {unknown} node AST node
205
+ * @returns {boolean} true, якщо це CallExpression з `.request()`
206
+ */
207
+ function isRequestFactoryCall(node) {
208
+ if (!node || node.type !== 'CallExpression') return false
209
+ const callee = node.callee
210
+ if (!callee || callee.type !== 'MemberExpression') return false
211
+ if (callee.computed) return false
212
+ const prop = callee.property
213
+ return !!prop && prop.type === 'Identifier' && prop.name === 'request'
214
+ }
215
+
216
+ /**
217
+ * Знаходить створення `ConnectionPool` всередині функцій.
218
+ * @param {string} content вихідний код
219
+ * @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/db.ts`)
220
+ * @returns {{ line: number, snippet: string }[]} список порушень
221
+ */
222
+ export function findMssqlPerRequestConnectionInText(content, virtualPath = 'scan.ts') {
223
+ const lang = langFromPath(virtualPath || 'scan.ts')
224
+ let result
225
+ try {
226
+ result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
227
+ } catch {
228
+ return []
229
+ }
230
+ if (result.errors?.length) return []
231
+
232
+ /** @type {{ line: number, snippet: string }[]} */
233
+ const out = []
234
+
235
+ walkAstWithAncestors(result.program, [], (node, ancestors) => {
236
+ const insideFunction = ancestors.some(n => isFunctionNode(n))
237
+ if (!insideFunction) return
238
+
239
+ if (isNewConnectionPool(node)) {
240
+ out.push({
241
+ line: offsetToLine(content, node.start),
242
+ snippet: normalizeSnippet(content.slice(node.start, node.end))
243
+ })
244
+ }
245
+ })
246
+
247
+ return out
248
+ }
249
+
250
+ /**
251
+ * Знаходить небезпечні виклики `query(\`...\`)` (CallExpression з TemplateLiteral-аргументом).
252
+ * @param {string} content вихідний код
253
+ * @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/db.ts`)
254
+ * @returns {{ line: number, snippet: string }[]} список порушень
255
+ */
256
+ export function findUnsafeMssqlQueryTemplateCallInText(content, virtualPath = 'scan.ts') {
257
+ const lang = langFromPath(virtualPath || 'scan.ts')
258
+ let result
259
+ try {
260
+ result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
261
+ } catch {
262
+ return []
263
+ }
264
+ if (result.errors?.length) return []
265
+
266
+ /** @type {{ line: number, snippet: string }[]} */
267
+ const out = []
268
+ walkAstWithAncestors(result.program, [], node => {
269
+ if (isUnsafeQueryCallWithTemplateLiteral(node)) {
270
+ out.push({
271
+ line: offsetToLine(content, node.start),
272
+ snippet: normalizeSnippet(content.slice(node.start, node.end))
273
+ })
274
+ }
275
+ })
276
+ return out
277
+ }
278
+
279
+ /**
280
+ * Знаходить shared Request (`export const request = pool.request()` та подібні), які не можна
281
+ * повторно використовувати між запитами.
282
+ * @param {string} content вихідний код
283
+ * @param {string} [virtualPath] шлях для вибору `lang`
284
+ * @returns {{ line: number, snippet: string }[]} список порушень
285
+ */
286
+ export function findSharedMssqlRequestInText(content, virtualPath = 'scan.ts') {
287
+ const lang = langFromPath(virtualPath || 'scan.ts')
288
+ let result
289
+ try {
290
+ result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
291
+ } catch {
292
+ return []
293
+ }
294
+ if (result.errors?.length) return []
295
+
296
+ /** @type {{ line: number, snippet: string }[]} */
297
+ const out = []
298
+ walkAstWithAncestors(result.program, [], node => {
299
+ if (node.type !== 'VariableDeclarator') return
300
+ const id = node.id
301
+ if (!id || id.type !== 'Identifier') return
302
+ if (id.name !== 'request') return
303
+ const init = node.init
304
+ if (!init || typeof init !== 'object') return
305
+ if (!isRequestFactoryCall(init)) return
306
+
307
+ out.push({
308
+ line: offsetToLine(content, node.start),
309
+ snippet: normalizeSnippet(content.slice(node.start, node.end))
310
+ })
311
+ })
312
+ return out
313
+ }
314
+
315
+ /**
316
+ * Знаходить небезпечні динамічні списки в SQL, коли у TemplateLiteral/TaggedTemplateExpression
317
+ * підставляють рядки, зібрані через `.join(...)` у контексті `IN (...)` або `VALUES (...)`.
318
+ *
319
+ * Цей патерн небезпечний навіть якщо зовні використовується tagged template, бо в запит
320
+ * потрапляє “готовий шматок SQL”, а не параметризовані значення.
321
+ * @param {string} content вихідний код
322
+ * @param {string} [virtualPath] шлях для вибору `lang`
323
+ * @returns {{ line: number, snippet: string }[]} список порушень
324
+ */
325
+ export function findUnsafeMssqlDynamicSqlListInText(content, virtualPath = 'scan.ts') {
326
+ const lang = langFromPath(virtualPath || 'scan.ts')
327
+ let result
328
+ try {
329
+ result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
330
+ } catch {
331
+ return []
332
+ }
333
+ if (result.errors?.length) return []
334
+
335
+ /** @type {{ line: number, snippet: string }[]} */
336
+ const out = []
337
+ walkAstWithAncestors(result.program, [], node => {
338
+ /** @type {unknown} */
339
+ let template = null
340
+
341
+ if (node.type === 'TemplateLiteral') {
342
+ template = node
343
+ } else if (node.type === 'TaggedTemplateExpression') {
344
+ template = node.quasi
345
+ }
346
+
347
+ if (!template || typeof template !== 'object' || template.type !== 'TemplateLiteral') return
348
+ if (!isSqlListContextTemplate(template)) return
349
+ const expressions = template.expressions
350
+ if (!Array.isArray(expressions) || expressions.length === 0) return
351
+
352
+ const hasJoin = expressions.some(expr => isJoinCall(expr))
353
+ if (!hasJoin) return
354
+
355
+ out.push({
356
+ line: offsetToLine(content, template.start),
357
+ snippet: normalizeSnippet(content.slice(template.start, template.end))
358
+ })
359
+ })
360
+
361
+ return out
362
+ }
363
+
364
+ /**
365
+ * Чи елементи літерального масиву — лише числові (numeric/bigint) літерали.
366
+ * Такі масиви безпечні в `IN (...)` навіть без явного парсера.
367
+ * @param {unknown} node AST node
368
+ * @returns {boolean} true, якщо це непорожній масив суто числових літералів
369
+ */
370
+ function isLiteralNumericArrayExpression(node) {
371
+ if (!node || typeof node !== 'object' || node.type !== 'ArrayExpression') return false
372
+ const elements = node.elements
373
+ if (!Array.isArray(elements) || elements.length === 0) return false
374
+ return elements.every(el => {
375
+ if (!el || typeof el !== 'object') return false
376
+ if (el.type === 'NumericLiteral' || el.type === 'BigIntLiteral') return true
377
+ return el.type === 'Literal' && (typeof el.value === 'number' || typeof el.value === 'bigint')
378
+ })
379
+ }
380
+
381
+ /**
382
+ * Чи це безпосередній виклик числового парсера (parseInt/parseFloat/Number/BigInt)
383
+ * або обʼєктний доступ до них (наприклад `Number.parseInt(...)`).
384
+ * @param {Record<string, unknown>} node AST CallExpression
385
+ * @returns {boolean} true, якщо callee — числовий парсер
386
+ */
387
+ function isNumericParseCallExpression(node) {
388
+ if (node.type !== 'CallExpression') return false
389
+ const callee = node.callee
390
+ if (!callee) return false
391
+ if (callee.type === 'Identifier' && NUMERIC_PARSE_FN_NAMES.has(callee.name)) return true
392
+ if (callee.type === 'MemberExpression' && !callee.computed) {
393
+ const prop = callee.property
394
+ return !!prop && prop.type === 'Identifier' && NUMERIC_PARSE_FN_NAMES.has(prop.name)
395
+ }
396
+ return false
397
+ }
398
+
399
+ /**
400
+ * Чи містить піддерево виклик числового парсера (parseInt/parseFloat/Number/BigInt)
401
+ * або унарний `+` (приведення до Number). Це сигнал, що значення гарантовано числове
402
+ * і не може містити SQL-метасимволи.
403
+ * @param {unknown} node AST node
404
+ * @returns {boolean} true, якщо знайдено числовий парсер у піддереві
405
+ */
406
+ function subtreeHasNumericParseCall(node) {
407
+ if (!node || typeof node !== 'object') return false
408
+ if (Array.isArray(node)) return node.some(item => subtreeHasNumericParseCall(item))
409
+
410
+ if (isNumericParseCallExpression(node)) return true
411
+ if (node.type === 'UnaryExpression' && node.operator === '+') return true
412
+
413
+ for (const [key, v] of Object.entries(node)) {
414
+ if (key === 'parent') continue
415
+ if (v && typeof v === 'object' && subtreeHasNumericParseCall(v)) return true
416
+ }
417
+ return false
418
+ }
419
+
420
+ /**
421
+ * Збирає всі VariableDeclarator-вузли в AST (для трасування Identifier-ів до їх init).
422
+ * @param {unknown} programNode AST root (Program)
423
+ * @returns {Array<Record<string, unknown>>} список VariableDeclarator-ів
424
+ */
425
+ function collectVariableDeclarators(programNode) {
426
+ /** @type {Array<Record<string, unknown>>} */
427
+ const out = []
428
+ walkAstWithAncestors(programNode, [], node => {
429
+ if (node.type === 'VariableDeclarator') out.push(node)
430
+ })
431
+ return out
432
+ }
433
+
434
+ /**
435
+ * Чи виглядає вираз, який підставляється в `IN (${...})`, як «безпечно розпарсений»:
436
+ * - літеральний масив чисел;
437
+ * - саме піддерево містить виклик числового парсера (parseInt/Number/BigInt/parseFloat/+x);
438
+ * - Identifier, чий init у файлі рекурсивно задовольняє ці умови.
439
+ *
440
+ * Якщо для Identifier немає видимого init (наприклад параметр функції чи import),
441
+ * вираз вважається не парсованим — потрібен явний парсер на місці підстановки.
442
+ * @param {unknown} expr вираз з template.expressions
443
+ * @param {Array<Record<string, unknown>>} declarators VariableDeclarator-и файлу
444
+ * @param {Set<string>} [seen] іменa Identifier-ів, що вже трасуються (анти-цикл)
445
+ * @returns {boolean} true, якщо вираз можна вважати безпечно числовим
446
+ */
447
+ function isInListExpressionParsed(expr, declarators, seen = new Set()) {
448
+ if (!expr || typeof expr !== 'object') return false
449
+ if (isLiteralNumericArrayExpression(expr)) return true
450
+ if (subtreeHasNumericParseCall(expr)) return true
451
+
452
+ if (expr.type === 'Identifier' && typeof expr.name === 'string') {
453
+ if (seen.has(expr.name)) return false
454
+ const nextSeen = new Set(seen)
455
+ nextSeen.add(expr.name)
456
+ const inits = declarators
457
+ .filter(d => {
458
+ const id = d.id
459
+ return !!id && typeof id === 'object' && id.type === 'Identifier' && id.name === expr.name && !!d.init
460
+ })
461
+ .map(d => d.init)
462
+ if (inits.length === 0) return false
463
+ return inits.every(init => isInListExpressionParsed(init, declarators, nextSeen))
464
+ }
465
+
466
+ return false
467
+ }
468
+
469
+ /**
470
+ * Сирий текст quasi-елемента TemplateLiteral на позиції перед expressions[i].
471
+ * @param {unknown} q quasi-елемент TemplateLiteral
472
+ * @returns {string} `q.value.raw` або порожній рядок, якщо структура не підходить
473
+ */
474
+ function quasiRawText(q) {
475
+ return q && typeof q === 'object' && q.value && typeof q.value === 'object' && typeof q.value.raw === 'string'
476
+ ? q.value.raw
477
+ : ''
478
+ }
479
+
480
+ /**
481
+ * Збирає порушення для одного TemplateLiteral вузла: знаходить expressions, що
482
+ * стоять одразу після `IN (` без числового парсера значень.
483
+ * @param {Record<string, unknown>} node TemplateLiteral
484
+ * @param {string} content вихідний код
485
+ * @param {Array<Record<string, unknown>>} declarators VariableDeclarator-и для трасування
486
+ * @param {{ line: number, snippet: string }[]} out буфер результатів
487
+ */
488
+ function collectInListUnparsedFromTemplate(node, content, declarators, out) {
489
+ if (node.type !== 'TemplateLiteral') return
490
+ const quasis = node.quasis
491
+ const expressions = node.expressions
492
+ if (!Array.isArray(quasis) || !Array.isArray(expressions) || expressions.length === 0) return
493
+
494
+ for (const [i, expr] of expressions.entries()) {
495
+ if (!IN_PLACEHOLDER_END_RE.test(quasiRawText(quasis[i]))) continue
496
+ if (!expr || typeof expr !== 'object') continue
497
+ if (isJoinCall(expr)) continue
498
+ if (isInListExpressionParsed(expr, declarators)) continue
499
+
500
+ const startOffset = (typeof expr.start === 'number' ? expr : node).start
501
+ out.push({
502
+ line: offsetToLine(content, startOffset),
503
+ snippet: normalizeSnippet(content.slice(node.start, node.end))
504
+ })
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Збирає порушення для одного TemplateLiteral: якщо у `IN (${...})`:
510
+ * - `${...}` не є Identifier (значення не винесені у змінну);
511
+ * - або це Identifier, але перед запитом немає guard `if (empty) throw`.
512
+ * @param {Record<string, unknown>} node TemplateLiteral
513
+ * @param {unknown[]} ancestors ancestors від walkAstWithAncestors
514
+ * @param {string} content вихідний код
515
+ * @param {{ line: number, snippet: string, reason: 'not_var' | 'missing_guard', name?: string }[]} out буфер результатів
516
+ */
517
+ function collectInListMissingEmptyGuardFromTemplate(node, ancestors, content, out) {
518
+ if (node.type !== 'TemplateLiteral') return
519
+ const quasis = node.quasis
520
+ const expressions = node.expressions
521
+ if (!Array.isArray(quasis) || !Array.isArray(expressions) || expressions.length === 0) return
522
+
523
+ for (const [i, expr] of expressions.entries()) {
524
+ if (!IN_PLACEHOLDER_END_RE.test(quasiRawText(quasis[i]))) continue
525
+ if (!expr || typeof expr !== 'object') continue
526
+
527
+ if (expr.type !== 'Identifier' || typeof expr.name !== 'string') {
528
+ out.push({
529
+ line: offsetToLine(content, node.start),
530
+ snippet: normalizeSnippet(content.slice(node.start, node.end)),
531
+ reason: 'not_var'
532
+ })
533
+ continue
534
+ }
535
+
536
+ const place = findEnclosingBlockAndStatementIndex(ancestors)
537
+ if (!place || !hasEmptyGuardBefore(place.block, place.index, expr.name)) {
538
+ out.push({
539
+ line: offsetToLine(content, node.start),
540
+ snippet: normalizeSnippet(content.slice(node.start, node.end)),
541
+ reason: 'missing_guard',
542
+ name: expr.name
543
+ })
544
+ }
545
+ }
546
+ }
547
+
548
+ /**
549
+ * Знаходить підстановки IN (вираз) у TemplateLiteral, де вираз не пройшов числовий парсер.
550
+ *
551
+ * Навіть у безпечному tagged template pool.request().query краще явно парсити значення (parseInt,
552
+ * Number, BigInt, parseFloat) та фільтрувати NaN. Див. також findUnsafeMssqlDynamicSqlListInText для
553
+ * випадків arr.join у списках.
554
+ * @param {string} content вихідний код
555
+ * @param {string} [virtualPath] шлях для вибору мови парсера (lang)
556
+ * @returns {{ line: number, snippet: string }[]} список порушень
557
+ */
558
+ export function findUnsafeMssqlInListUnparsedInText(content, virtualPath = 'scan.ts') {
559
+ const lang = langFromPath(virtualPath || 'scan.ts')
560
+ let result
561
+ try {
562
+ result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
563
+ } catch {
564
+ return []
565
+ }
566
+ if (result.errors?.length) return []
567
+
568
+ const declarators = collectVariableDeclarators(result.program)
569
+
570
+ /** @type {{ line: number, snippet: string }[]} */
571
+ const out = []
572
+ walkAstWithAncestors(result.program, [], node => collectInListUnparsedFromTemplate(node, content, declarators, out))
573
+
574
+ return out
575
+ }
576
+
577
+ /**
578
+ * Знаходить підстановки списків у `IN (${...})`, які:
579
+ * - не винесені в окрему змінну (в `${...}` стоїть не Identifier);
580
+ * - або винесені, але перед запитом немає перевірки на пустоту з `throw`.
581
+ * @param {string} content вихідний код
582
+ * @param {string} [virtualPath] шлях для вибору мови парсера (lang)
583
+ * @returns {{ line: number, snippet: string, reason: 'not_var' | 'missing_guard', name?: string }[]} список порушень
584
+ */
585
+ export function findUnsafeMssqlInListMissingEmptyGuardInText(content, virtualPath = 'scan.ts') {
586
+ const lang = langFromPath(virtualPath || 'scan.ts')
587
+ let result
588
+ try {
589
+ result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
590
+ } catch {
591
+ return []
592
+ }
593
+ if (result.errors?.length) return []
594
+
595
+ /** @type {{ line: number, snippet: string, reason: 'not_var' | 'missing_guard', name?: string }[]} */
596
+ const out = []
597
+ walkAstWithAncestors(result.program, [], (node, ancestors) =>
598
+ collectInListMissingEmptyGuardFromTemplate(node, ancestors, content, out)
599
+ )
600
+ return out
601
+ }
602
+
603
+ /**
604
+ * Чи сканувати цей файл за розширенням (JS/TS-сім'я).
605
+ * @param {string} relativePathPosix відносний шлях (posix)
606
+ * @returns {boolean} `true`, якщо розширення підходить для AST-скану
607
+ */
608
+ export function isMssqlScanSourceFile(relativePathPosix) {
609
+ return SOURCE_FILE_RE.test(relativePathPosix) && !relativePathPosix.endsWith('.d.ts')
610
+ }
@@ -0,0 +1 @@
1
+ { "auto": { "predicate": "depInAnyPackageJson", "arg": ["mssql"] } }