@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,59 @@
1
+ # Перевірка `jsconfig.json` (js-run.mdc).
2
+ #
3
+ # Канон надходить через --data: { "template": { "snippet": ... } }
4
+ # Структура --data сформована з template/jsconfig.json.snippet.yml.
5
+ # Walker: для кожного leaf у template's snippet порівнюємо input[path].
6
+ # Для масивів — equality (не subset-of, бо в jsconfig потрібен точний набір).
7
+ package js_run.jsconfig
8
+
9
+ import rego.v1
10
+
11
+ # Generic 2-level walker: section → key → expected (для compilerOptions).
12
+ deny contains msg if {
13
+ some section, expected_inner in data.template.snippet
14
+ is_object(expected_inner)
15
+ inner := object.get(input, section, {})
16
+ is_object(inner)
17
+ some leaf_key, expected_value in expected_inner
18
+ actual := object.get(inner, leaf_key, null)
19
+ not values_match(actual, expected_value)
20
+ msg := sprintf("jsconfig.json: %s.%s має бути %v (js-run.mdc)", [section, leaf_key, expected_value])
21
+ }
22
+
23
+ # Section відсутня або не обʼєкт.
24
+ deny contains msg if {
25
+ some section, expected_inner in data.template.snippet
26
+ is_object(expected_inner)
27
+ raw := object.get(input, section, null)
28
+ not is_object(raw)
29
+ msg := sprintf("jsconfig.json: відсутній обʼєкт %s (js-run.mdc)", [section])
30
+ }
31
+
32
+ # Top-level масив (наприклад include) — порівнюємо як множину.
33
+ deny contains msg if {
34
+ some field, expected_array in data.template.snippet
35
+ is_array(expected_array)
36
+ actual := object.get(input, field, null)
37
+ not is_array(actual)
38
+ msg := sprintf("jsconfig.json: %s має бути масив %v (js-run.mdc)", [field, expected_array])
39
+ }
40
+
41
+ deny contains msg if {
42
+ some field, expected_array in data.template.snippet
43
+ is_array(expected_array)
44
+ is_array(object.get(input, field, null))
45
+ {x | some x in input[field]} != {x | some x in expected_array}
46
+ msg := sprintf("jsconfig.json: %s має бути %v (js-run.mdc)", [field, expected_array])
47
+ }
48
+
49
+ # Helper: leaf-level value match, з підтримкою масивів як множин.
50
+ values_match(actual, expected) if {
51
+ not is_array(expected)
52
+ actual == expected
53
+ }
54
+
55
+ values_match(actual, expected) if {
56
+ is_array(expected)
57
+ is_array(actual)
58
+ {x | some x in actual} == {x | some x in expected}
59
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["esnext"],
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "target": "esnext",
7
+ "checkJs": false
8
+ },
9
+ "include": ["src/**/*"]
10
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Знаходить імпорти з `@nitra/bunyan` (і застарілого `bunyan`) у джерелах — їх треба замінити
3
+ * на `@nitra/pino` згідно з js-run.mdc.
4
+ *
5
+ * Семантика береться з **oxc-parser** (`module.staticImports`) — без regex по тілу файлу.
6
+ * Додатково по AST програми ловимо `require('@nitra/bunyan')` і динамічний `import('@nitra/bunyan')`,
7
+ * щоб правило працювало й у CommonJS і при динамічному import у межах одного файлу.
8
+ *
9
+ * Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках повертається порожній
10
+ * результат — спочатку треба полагодити синтаксис, потім перезапустити перевірку.
11
+ */
12
+ import { parseSync } from 'oxc-parser'
13
+
14
+ import {
15
+ dynamicImportModule,
16
+ langFromPath,
17
+ normalizeSnippet,
18
+ offsetToLine,
19
+ requireCallModule,
20
+ walkAstWithAncestors
21
+ } from '@7n/rules/scripts/utils/ast-scan-utils.mjs'
22
+
23
+ const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
24
+ const FORBIDDEN_MODULES = new Set(['@nitra/bunyan', 'bunyan'])
25
+
26
+ /**
27
+ * Знаходить заборонені імпорти/require з `@nitra/bunyan` у тексті.
28
+ * @param {string} content вихідний код
29
+ * @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/foo.ts`)
30
+ * @returns {{ line: number, snippet: string, module: string }[]} список порушень
31
+ */
32
+ export function findBunyanImportsInText(content, virtualPath = 'scan.ts') {
33
+ const pathForLang = virtualPath || 'scan.ts'
34
+ const lang = langFromPath(pathForLang)
35
+ let result
36
+ try {
37
+ result = parseSync(pathForLang, content, { lang, sourceType: 'module' })
38
+ } catch {
39
+ return []
40
+ }
41
+ if (result.errors?.length) {
42
+ return []
43
+ }
44
+
45
+ /** @type {{ line: number, snippet: string, module: string }[]} */
46
+ const out = []
47
+
48
+ for (const imp of result.module?.staticImports ?? []) {
49
+ const mod = imp.moduleRequest?.value
50
+ if (mod && FORBIDDEN_MODULES.has(mod)) {
51
+ out.push({
52
+ line: offsetToLine(content, imp.start),
53
+ snippet: normalizeSnippet(content.slice(imp.start, imp.end)),
54
+ module: mod
55
+ })
56
+ }
57
+ }
58
+
59
+ walkAstWithAncestors(result.program, [], node => {
60
+ const reqMod = requireCallModule(node)
61
+ if (reqMod && FORBIDDEN_MODULES.has(reqMod)) {
62
+ out.push({
63
+ line: offsetToLine(content, node.start),
64
+ snippet: normalizeSnippet(content.slice(node.start, node.end)),
65
+ module: reqMod
66
+ })
67
+ return
68
+ }
69
+ const dynMod = dynamicImportModule(node)
70
+ if (dynMod && FORBIDDEN_MODULES.has(dynMod)) {
71
+ out.push({
72
+ line: offsetToLine(content, node.start),
73
+ snippet: normalizeSnippet(content.slice(node.start, node.end)),
74
+ module: dynMod
75
+ })
76
+ }
77
+ })
78
+
79
+ return out
80
+ }
81
+
82
+ /**
83
+ * Чи сканувати цей файл за розширенням (JS/TS-сім'я).
84
+ * @param {string} relativePath відносний шлях до файлу
85
+ * @returns {boolean} `true`, якщо розширення підходить для пошуку імпорту
86
+ */
87
+ export function isBunyanScanSourceFile(relativePath) {
88
+ return SOURCE_FILE_RE.test(relativePath)
89
+ }
90
+
91
+ /**
92
+ * Чи слід пропустити файл під час обходу пакета (декларації типів).
93
+ * @param {string} relativePosix шлях з posix-слешами
94
+ * @returns {boolean} `true`, якщо файл не сканувати
95
+ */
96
+ export function shouldSkipFileForBunyanScan(relativePosix) {
97
+ return relativePosix.endsWith('.d.ts')
98
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * AST-сканер правила «process.env / CheckEnv» (js-run.mdc).
3
+ *
4
+ * Правило в .mdc формулює два контракти:
5
+ * 1. Прямий доступ до `process.env.X` має бути замінено на `env` — з пакета
6
+ * `@nitra/check-env` (для обов'язкових змінних, із викликом `checkEnv([...])`)
7
+ * або з `node:process` (для опційних). Тому будь-яке `process.env.X` сканер
8
+ * завжди реєструє як порушення з порадою про конкретну заміну.
9
+ * 2. Якщо у файл імпортовано `env` саме з `@nitra/check-env`, то кожне `env.X`
10
+ * має бути закрите літеральним викликом `checkEnv(['X', ...])` у тому ж файлі
11
+ * (порядок не важливий, кілька викликів зливаються в один список).
12
+ *
13
+ * Обидва контракти можна точково «приглушити» коментарем-маркером
14
+ * `// \@7n/rules ignore-next-line checkEnv` на рядку безпосередньо перед
15
+ * порушенням — це залишається сумісним escape-hatch для legacy-коду.
16
+ *
17
+ * Семантика береться з **oxc-parser** через `parseProgramOrNull`: regex по тілу
18
+ * файлу не використовується, лише сирий текст рядка з коментарем перевіряється
19
+ * на маркер. Якщо файл не парситься — повертаємо порожній результат, спочатку
20
+ * треба полагодити синтаксис.
21
+ *
22
+ * Покриті форми доступу:
23
+ * - `process.env.X` / `process.env['X']` (як MemberExpression);
24
+ * - `const { X, Y } = process.env` (ObjectPattern; ім'я з ключа, не з alias);
25
+ * - аналогічно для `env.X` / `env['X']` / `const { X } = env`,
26
+ * де `env` має бути імпортований з `@nitra/check-env` (інакше ігноруємо —
27
+ * це може бути локальна змінна чи `env` з `node:process`).
28
+ *
29
+ * Якщо ключ обчислюваний (`process.env[varName]`) — пропускаємо без помилки,
30
+ * бо за статичним AST неможливо встановити, яка саме змінна оточення використовується.
31
+ */
32
+ import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from '@7n/rules/scripts/utils/ast-scan-utils.mjs'
33
+
34
+ const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
35
+ // Приймає і legacy `@nitra/cursor` маркер — консюмерський код з ним не має ламатись після перейменування
36
+ const IGNORE_DIRECTIVE_RE = /\/\/\s*(?:@7n\/rules|@nitra\/cursor)\s+ignore-next-line\s+checkEnv\b/u
37
+
38
+ const CHECK_ENV_PACKAGE = '@nitra/check-env'
39
+
40
+ /**
41
+ * Чи є цей вузол виразом `process.env`.
42
+ * @param {unknown} node AST вузол
43
+ * @returns {boolean} true, якщо це `MemberExpression` `process.env` (Identifier . Identifier)
44
+ */
45
+ function isProcessEnvAccess(node) {
46
+ if (!node || typeof node !== 'object') return false
47
+ if (node.type !== 'MemberExpression' || node.computed) return false
48
+ const obj = node.object
49
+ const prop = node.property
50
+ return (
51
+ !!obj &&
52
+ obj.type === 'Identifier' &&
53
+ obj.name === 'process' &&
54
+ !!prop &&
55
+ prop.type === 'Identifier' &&
56
+ prop.name === 'env'
57
+ )
58
+ }
59
+
60
+ /**
61
+ * Витягує ім'я ENV з MemberExpression `obj.X` або `obj['X']`.
62
+ * @param {Record<string, unknown>} node MemberExpression, чий object — `process.env` або `env`
63
+ * @returns {string | null} ім'я змінної оточення або null, якщо ключ не статичний
64
+ */
65
+ function envNameFromMember(node) {
66
+ const prop = node.property
67
+ if (!prop || typeof prop !== 'object') return null
68
+ if (!node.computed && prop.type === 'Identifier' && typeof prop.name === 'string') return prop.name
69
+ if (node.computed && prop.type === 'Literal' && typeof prop.value === 'string') return prop.value
70
+ return null
71
+ }
72
+
73
+ /**
74
+ * Збирає всі літеральні імена з виклику `checkEnv([...])` у файлі.
75
+ * Якщо callee — Identifier `checkEnv` і перший аргумент — ArrayExpression, додає
76
+ * всі string-літерали до set. Не-літеральні елементи (Identifier, SpreadElement) ігноруються —
77
+ * це робить перевірку «ліберальною»: ми лише ловимо явно неперевірені змінні.
78
+ * @param {unknown} programNode корінь AST
79
+ * @returns {Set<string>} перелік закритих імен ENV
80
+ */
81
+ function collectCheckedEnvNames(programNode) {
82
+ /** @type {Set<string>} */
83
+ const out = new Set()
84
+ walkAstWithAncestors(programNode, [], node => {
85
+ if (node.type !== 'CallExpression') return
86
+ const callee = node.callee
87
+ if (!callee || callee.type !== 'Identifier' || callee.name !== 'checkEnv') return
88
+ const args = node.arguments
89
+ if (!Array.isArray(args) || args.length === 0) return
90
+ const first = args[0]
91
+ if (!first || typeof first !== 'object' || first.type !== 'ArrayExpression') return
92
+ const elements = first.elements
93
+ if (!Array.isArray(elements)) return
94
+ for (const el of elements) {
95
+ if (!el || typeof el !== 'object') continue
96
+ if (el.type === 'Literal' && typeof el.value === 'string') out.add(el.value)
97
+ }
98
+ })
99
+ return out
100
+ }
101
+
102
+ /**
103
+ * Чи імпортовано локальний ідентифікатор `env` саме з `@nitra/check-env`.
104
+ * Перевіряє ImportDeclaration на specifier {imported.name === 'env', local.name === 'env'}.
105
+ * Aliased-варіанти (`{ env as x }`) свідомо не підтримуються — у наших правилах
106
+ * приклади завжди використовують канонічне ім'я `env`.
107
+ * @param {unknown} programNode корінь AST
108
+ * @returns {boolean} true, якщо у файлі є `import { env } from '@nitra/check-env'`
109
+ */
110
+ function hasCheckEnvImport(programNode) {
111
+ let found = false
112
+ walkAstWithAncestors(programNode, [], node => {
113
+ if (found) return
114
+ if (node.type !== 'ImportDeclaration') return
115
+ const source = node.source
116
+ if (!source || typeof source !== 'object' || source.value !== CHECK_ENV_PACKAGE) return
117
+ const specifiers = node.specifiers
118
+ if (!Array.isArray(specifiers)) return
119
+ for (const s of specifiers) {
120
+ if (!s || typeof s !== 'object' || s.type !== 'ImportSpecifier') continue
121
+ const imported = s.imported
122
+ if (!imported || imported.name !== 'env') continue
123
+ const local = s.local
124
+ if (!local || local.name !== 'env') continue
125
+ found = true
126
+ return
127
+ }
128
+ })
129
+ return found
130
+ }
131
+
132
+ /**
133
+ * Чи закритий рядок ignore-коментарем `// \@7n/rules ignore-next-line checkEnv`.
134
+ * @param {string[]} lines рядки файлу (split за \n, без CR)
135
+ * @param {number} oneBasedLine 1-based номер рядка з порушенням
136
+ * @returns {boolean} true, якщо попередній рядок містить маркер
137
+ */
138
+ function hasIgnoreDirective(lines, oneBasedLine) {
139
+ if (oneBasedLine <= 1) return false
140
+ const prev = lines[oneBasedLine - 2] ?? ''
141
+ return IGNORE_DIRECTIVE_RE.test(prev)
142
+ }
143
+
144
+ /**
145
+ * Чи є вузол MemberExpression виду `env.X` / `env['X']`, де `env` — Identifier
146
+ * (в AST oxc-parser globals і локальні імпорти не розрізняються — фільтр джерела
147
+ * робиться на рівні `hasCheckEnvImport`).
148
+ * @param {unknown} node AST вузол
149
+ * @returns {boolean} true, якщо це `env.<...>`
150
+ */
151
+ function isEnvIdentifierMember(node) {
152
+ if (!node || typeof node !== 'object' || node.type !== 'MemberExpression') return false
153
+ const obj = node.object
154
+ return !!obj && obj.type === 'Identifier' && obj.name === 'env'
155
+ }
156
+
157
+ /**
158
+ * @typedef {{
159
+ * line: number,
160
+ * name: string,
161
+ * kind: 'process-env' | 'check-env-missing-checkEnv'
162
+ * }} EnvViolation
163
+ */
164
+
165
+ /**
166
+ * Чи parent — це MemberExpression, у якому `node` (process.env) є об'єктом доступу.
167
+ * @param {unknown} parent ancestor вузла
168
+ * @param {unknown} node перевіряємий вузол `process.env`
169
+ * @returns {boolean} true для `process.env.X` / `process.env['X']`
170
+ */
171
+ function isParentEnvMember(parent, node) {
172
+ return !!parent && typeof parent === 'object' && parent.type === 'MemberExpression' && parent.object === node
173
+ }
174
+
175
+ /**
176
+ * Чи parent — це VariableDeclarator виду `const { ... } = <node>`.
177
+ * @param {unknown} parent ancestor вузла
178
+ * @param {unknown} node перевіряємий вузол (process.env або Identifier `env`)
179
+ * @returns {boolean} true для `const { ... } = node`
180
+ */
181
+ function isParentObjectPatternDeclarator(parent, node) {
182
+ return (
183
+ !!parent &&
184
+ typeof parent === 'object' &&
185
+ parent.type === 'VariableDeclarator' &&
186
+ parent.init === node &&
187
+ !!parent.id &&
188
+ parent.id.type === 'ObjectPattern' &&
189
+ Array.isArray(parent.id.properties)
190
+ )
191
+ }
192
+
193
+ /**
194
+ * Чи node — це VariableDeclarator виду `const { ... } = env`, де `env` — Identifier.
195
+ * @param {Record<string, unknown>} node AST-вузол
196
+ * @returns {boolean} true для `const { ... } = env`
197
+ */
198
+ function isEnvObjectPatternDeclarator(node) {
199
+ return (
200
+ node.type === 'VariableDeclarator' &&
201
+ !!node.init &&
202
+ node.init.type === 'Identifier' &&
203
+ node.init.name === 'env' &&
204
+ !!node.id &&
205
+ node.id.type === 'ObjectPattern' &&
206
+ Array.isArray(node.id.properties)
207
+ )
208
+ }
209
+
210
+ /**
211
+ * Перебирає AST і для кожного знайденого доступу до `process.env` чи `env`
212
+ * (де `env` — імпорт з `@nitra/check-env`) реєструє порушення відповідного типу.
213
+ * @param {unknown} program корінь AST
214
+ * @param {string} content вихідний код (для offset → line)
215
+ * @param {string[]} lines split-рядки content (для ignore-маркера)
216
+ * @param {Set<string>} checkedNames імена, закриті літеральним `checkEnv([...])`
217
+ * @param {boolean} envFromCheckEnv чи імпортовано `env` саме з `@nitra/check-env`
218
+ * @returns {EnvViolation[]} список порушень (відсортований за порядком зустрічі в AST)
219
+ */
220
+ function collectViolations(program, content, lines, checkedNames, envFromCheckEnv) {
221
+ /** @type {EnvViolation[]} */
222
+ const out = []
223
+ /** @type {Set<string>} */
224
+ const reported = new Set()
225
+
226
+ /**
227
+ * Реєструє порушення з дедуплікацією за «kind|name|line» і урахуванням ignore-маркера.
228
+ * @param {'process-env' | 'check-env-missing-checkEnv'} kind тип порушення
229
+ * @param {string} name ім'я ENV
230
+ * @param {number} line 1-based рядок
231
+ */
232
+ function report(kind, name, line) {
233
+ if (hasIgnoreDirective(lines, line)) return
234
+ const key = `${kind}|${name}|${line}`
235
+ if (reported.has(key)) return
236
+ reported.add(key)
237
+ out.push({ kind, name, line })
238
+ }
239
+
240
+ /**
241
+ * Реєструє порушення для всіх статичних ключів ObjectPattern (`const { A, B } = …`).
242
+ * @param {Record<string, unknown>} declarator VariableDeclarator з ObjectPattern у `id`
243
+ * @param {'process-env' | 'check-env-missing-checkEnv'} kind тип порушення
244
+ * @param {(name: string) => boolean} skipName предикат «пропустити це ім'я» (наприклад, вже у checkEnv)
245
+ */
246
+ function reportObjectPatternKeys(declarator, kind, skipName) {
247
+ const fallbackOffset = declarator.start
248
+ for (const p of declarator.id.properties) {
249
+ const name = staticPropertyName(p)
250
+ if (!name || skipName(name)) continue
251
+ report(kind, name, offsetToLine(content, p.start ?? fallbackOffset))
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Обробка `process.env`-доступу: і `parent.X`, і деструктуризація.
257
+ * @param {unknown} node AST-вузол `process.env`
258
+ * @param {unknown[]} ancestors стек предків з walkAstWithAncestors
259
+ */
260
+ function handleProcessEnv(node, ancestors) {
261
+ const parent = ancestors.at(-1)
262
+ if (isParentEnvMember(parent, node)) {
263
+ const envName = envNameFromMember(parent)
264
+ if (envName) report('process-env', envName, offsetToLine(content, parent.start))
265
+ }
266
+ if (isParentObjectPatternDeclarator(parent, node)) {
267
+ reportObjectPatternKeys(parent, 'process-env', () => false)
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Обробка вузлів, що стосуються `env` з `@nitra/check-env`.
273
+ * @param {Record<string, unknown>} node AST-вузол
274
+ */
275
+ function handleCheckEnvAccess(node) {
276
+ if (isEnvIdentifierMember(node)) {
277
+ const envName = envNameFromMember(node)
278
+ if (envName && !checkedNames.has(envName)) {
279
+ report('check-env-missing-checkEnv', envName, offsetToLine(content, node.start))
280
+ }
281
+ return
282
+ }
283
+ if (isEnvObjectPatternDeclarator(node)) {
284
+ reportObjectPatternKeys(node, 'check-env-missing-checkEnv', name => checkedNames.has(name))
285
+ }
286
+ }
287
+
288
+ walkAstWithAncestors(program, [], (node, ancestors) => {
289
+ if (isProcessEnvAccess(node)) {
290
+ handleProcessEnv(node, ancestors)
291
+ return
292
+ }
293
+ if (envFromCheckEnv) handleCheckEnvAccess(node)
294
+ })
295
+
296
+ return out
297
+ }
298
+
299
+ /**
300
+ * Витягує статичне ім'я з вузла Property у ObjectPattern.
301
+ * @param {unknown} property AST-вузол ObjectPattern.properties[i]
302
+ * @returns {string | null} ім'я ключа або null
303
+ */
304
+ function staticPropertyName(property) {
305
+ if (!property || typeof property !== 'object' || property.type !== 'Property') return null
306
+ if (property.computed) return null
307
+ const key = property.key
308
+ if (!key || typeof key !== 'object') return null
309
+ if (key.type === 'Identifier' && typeof key.name === 'string') return key.name
310
+ if (key.type === 'Literal' && typeof key.value === 'string') return key.value
311
+ return null
312
+ }
313
+
314
+ /**
315
+ * Знаходить порушення правила «process.env / CheckEnv» у файлі.
316
+ * @param {string} content вихідний код
317
+ * @param {string} [virtualPath] шлях для вибору `lang` парсера
318
+ * @returns {EnvViolation[]} список порушень із типом, іменем змінної та рядком
319
+ */
320
+ export function findUncheckedProcessEnvInText(content, virtualPath = 'scan.ts') {
321
+ const program = parseProgramOrNull(content, virtualPath)
322
+ if (!program) return []
323
+
324
+ const checked = collectCheckedEnvNames(program)
325
+ const envFromCheckEnv = hasCheckEnvImport(program)
326
+ const lines = content.split('\n').map(s => (s.endsWith('\r') ? s.slice(0, -1) : s))
327
+
328
+ return collectViolations(program, content, lines, checked, envFromCheckEnv)
329
+ }
330
+
331
+ /**
332
+ * Чи сканувати цей файл за розширенням (JS/TS-сім'я, без `.d.ts`).
333
+ * @param {string} relativePathPosix відносний шлях (posix)
334
+ * @returns {boolean} true, якщо розширення підходить для AST-скану
335
+ */
336
+ export function isCheckEnvScanSourceFile(relativePathPosix) {
337
+ return SOURCE_FILE_RE.test(relativePathPosix) && !relativePathPosix.endsWith('.d.ts')
338
+ }