@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.
- package/CHANGELOG.md +6 -0
- package/package.json +14 -4
- package/rules/bun/bunfig/bunfig.mdc +17 -0
- package/rules/bun/bunfig/bunfig.rego +29 -0
- package/rules/bun/bunfig/concern.json +9 -0
- package/rules/bun/bunfig/template/bunfig.toml.snippet.toml +2 -0
- package/rules/bun/docs/index.md +11 -0
- package/rules/bun/layout/concern.json +16 -0
- package/rules/bun/layout/docs/fix-layout.md +29 -0
- package/rules/bun/layout/docs/main.md +34 -0
- package/rules/bun/layout/fix-layout.mjs +63 -0
- package/rules/bun/layout/layout.mdc +60 -0
- package/rules/bun/layout/main.mjs +53 -0
- package/rules/bun/licensee/concern.json +7 -0
- package/rules/bun/licensee/docs/fix-licensee.md +27 -0
- package/rules/bun/licensee/docs/index.md +12 -0
- package/rules/bun/licensee/docs/main.md +30 -0
- package/rules/bun/licensee/fix-licensee.mjs +31 -0
- package/rules/bun/licensee/main.mjs +68 -0
- package/rules/bun/lint-surface/concern.json +3 -0
- package/rules/bun/lint-surface/lint-surface.mdc +13 -0
- package/rules/bun/main.json +1 -0
- package/rules/bun/main.mdc +11 -0
- package/rules/bun/package_json/concern.json +9 -0
- package/rules/bun/package_json/docs/fix-package_json.md +28 -0
- package/rules/bun/package_json/docs/index.md +9 -0
- package/rules/bun/package_json/fix-package_json.mjs +311 -0
- package/rules/bun/package_json/package_json.mdc +14 -0
- package/rules/bun/package_json/package_json.rego +64 -0
- package/rules/bun/package_json/template/package.json.deny.json +4 -0
- package/rules/js/check/check.mdc +26 -0
- package/rules/js/check/concern.json +19 -0
- package/rules/js/check/docs/eslint-config.md +56 -0
- package/rules/js/check/docs/fix-check.md +48 -0
- package/rules/js/check/docs/index.md +13 -0
- package/rules/js/check/docs/main.md +48 -0
- package/rules/js/check/eslint-config.mjs +262 -0
- package/rules/js/check/fix-check.mjs +82 -0
- package/rules/js/check/main.mjs +310 -0
- package/rules/js/dep-policy/concern.json +4 -0
- package/rules/js/dep-policy/dep-policy.mdc +36 -0
- package/rules/js/dep-policy/docs/main.md +35 -0
- package/rules/js/dep-policy/main.mjs +99 -0
- package/rules/js/docs/index.md +11 -0
- package/rules/js/eslint/concern.json +8 -0
- package/rules/js/eslint/docs/fix-eslint.md +50 -0
- package/rules/js/eslint/docs/fix-worker.md +31 -0
- package/rules/js/eslint/docs/index.md +11 -0
- package/rules/js/eslint/docs/main.md +35 -0
- package/rules/js/eslint/fix-eslint.mjs +160 -0
- package/rules/js/eslint/fix-worker.mjs +139 -0
- package/rules/js/eslint/main.mjs +115 -0
- package/rules/js/file-extensions/concern.json +3 -0
- package/rules/js/file-extensions/file-extensions.mdc +12 -0
- package/rules/js/jscpd_config/concern.json +11 -0
- package/rules/js/jscpd_config/docs/fix-jscpd_config.md +25 -0
- package/rules/js/jscpd_config/docs/index.md +9 -0
- package/rules/js/jscpd_config/fix-jscpd_config.mjs +3 -0
- package/rules/js/jscpd_config/jscpd_config.mdc +42 -0
- package/rules/js/jscpd_config/jscpd_config.rego +44 -0
- package/rules/js/jscpd_config/template/.jscpd.json.snippet.json +7 -0
- package/rules/js/jscpd_duplicates/concern.json +7 -0
- package/rules/js/jscpd_duplicates/docs/main.md +29 -0
- package/rules/js/jscpd_duplicates/main.mjs +69 -0
- package/rules/js/knip/concern.json +7 -0
- package/rules/js/knip/docs/main.md +32 -0
- package/rules/js/knip/knip.mdc +15 -0
- package/rules/js/knip/main.mjs +68 -0
- package/rules/js/lint-findings/concern.json +3 -0
- package/rules/js/lint-findings/docs/main.md +40 -0
- package/rules/js/lint-findings/main.mjs +125 -0
- package/rules/js/main.json +1 -0
- package/rules/js/main.mdc +18 -0
- package/rules/js/package_json/concern.json +9 -0
- package/rules/js/package_json/docs/fix-package_json.md +25 -0
- package/rules/js/package_json/docs/index.md +9 -0
- package/rules/js/package_json/fix-package_json.mjs +3 -0
- package/rules/js/package_json/package_json.mdc +15 -0
- package/rules/js/package_json/package_json.rego +142 -0
- package/rules/js/package_json/template/package.json.snippet.json +6 -0
- package/rules/js/tooling/concern.json +3 -0
- package/rules/js/tooling/data/tooling/knip-canonical.json +30 -0
- package/rules/js/tooling/data/tooling/oxlint-canonical.json +400 -0
- package/rules/js/tooling/docs/main.md +53 -0
- package/rules/js/tooling/main.mjs +183 -0
- package/rules/js/utils_imports/concern.json +4 -0
- package/rules/js/utils_imports/docs/main.md +50 -0
- package/rules/js/utils_imports/main.mjs +185 -0
- package/rules/js/utils_imports/utils_imports.mdc +15 -0
- package/rules/js/vscode_extensions/concern.json +11 -0
- package/rules/js/vscode_extensions/docs/fix-vscode_extensions.md +24 -0
- package/rules/js/vscode_extensions/docs/index.md +11 -0
- package/rules/js/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/js/vscode_extensions/template/extensions.json.snippet.json +6 -0
- package/rules/js/vscode_extensions/vscode_extensions.mdc +11 -0
- package/rules/js/vscode_extensions/vscode_extensions.rego +12 -0
- package/rules/js-bun-db/connection/concern.json +3 -0
- package/rules/js-bun-db/connection/connection.mdc +42 -0
- package/rules/js-bun-db/docs/index.md +11 -0
- package/rules/js-bun-db/lib/bun-sql-scan.mjs +1047 -0
- package/rules/js-bun-db/lib/docs/bun-sql-scan.md +63 -0
- package/rules/js-bun-db/lib/docs/index.md +11 -0
- package/rules/js-bun-db/main.json +1 -0
- package/rules/js-bun-db/main.mdc +8 -0
- package/rules/js-bun-db/package_json/concern.json +9 -0
- package/rules/js-bun-db/package_json/package_json.mdc +31 -0
- package/rules/js-bun-db/package_json/package_json.rego +15 -0
- package/rules/js-bun-db/package_json/template/package.json.deny.json +6 -0
- package/rules/js-bun-db/pg_format_identifiers/concern.json +3 -0
- package/rules/js-bun-db/pg_format_identifiers/pg_format_identifiers.mdc +104 -0
- package/rules/js-bun-db/safety/concern.json +4 -0
- package/rules/js-bun-db/safety/docs/main.md +34 -0
- package/rules/js-bun-db/safety/main.mjs +430 -0
- package/rules/js-bun-db/safety/safety.mdc +458 -0
- package/rules/js-bun-redis/docs/index.md +11 -0
- package/rules/js-bun-redis/imports/concern.json +4 -0
- package/rules/js-bun-redis/imports/docs/main.md +36 -0
- package/rules/js-bun-redis/imports/imports.mdc +47 -0
- package/rules/js-bun-redis/imports/main.mjs +88 -0
- package/rules/js-bun-redis/lib/docs/index.md +11 -0
- package/rules/js-bun-redis/lib/docs/redis-imports.md +227 -0
- package/rules/js-bun-redis/lib/redis-imports.mjs +130 -0
- package/rules/js-bun-redis/main.json +1 -0
- package/rules/js-bun-redis/main.mdc +8 -0
- package/rules/js-bun-redis/package_json/concern.json +9 -0
- package/rules/js-bun-redis/package_json/package_json.mdc +11 -0
- package/rules/js-bun-redis/package_json/package_json.rego +15 -0
- package/rules/js-bun-redis/package_json/template/package.json.deny.json +12 -0
- package/rules/js-mssql/deps/concern.json +4 -0
- package/rules/js-mssql/deps/docs/main.md +33 -0
- package/rules/js-mssql/deps/main.mjs +297 -0
- package/rules/js-mssql/docs/index.md +11 -0
- package/rules/js-mssql/lib/docs/index.md +11 -0
- package/rules/js-mssql/lib/docs/mssql-pool-scan.md +380 -0
- package/rules/js-mssql/lib/mssql-pool-scan.mjs +610 -0
- package/rules/js-mssql/main.json +1 -0
- package/rules/js-mssql/main.mdc +144 -0
- package/rules/js-mssql/mssql-tvp/concern.json +3 -0
- package/rules/js-mssql/mssql-tvp/mssql-tvp.mdc +77 -0
- package/rules/js-mssql/package_json/concern.json +9 -0
- package/rules/js-mssql/package_json/package_json.mdc +9 -0
- package/rules/js-mssql/package_json/package_json.rego +57 -0
- package/rules/js-run/configmap/concern.json +9 -0
- package/rules/js-run/configmap/configmap.mdc +37 -0
- package/rules/js-run/configmap/configmap.rego +21 -0
- package/rules/js-run/configmap/template/configmap.yaml.contains.yml +4 -0
- package/rules/js-run/docs/index.md +11 -0
- package/rules/js-run/jsconfig/concern.json +9 -0
- package/rules/js-run/jsconfig/docs/fix-jsconfig.md +28 -0
- package/rules/js-run/jsconfig/docs/index.md +9 -0
- package/rules/js-run/jsconfig/fix-jsconfig.mjs +119 -0
- package/rules/js-run/jsconfig/jsconfig.mdc +48 -0
- package/rules/js-run/jsconfig/jsconfig.rego +59 -0
- package/rules/js-run/jsconfig/template/jsconfig.json.snippet.json +10 -0
- package/rules/js-run/lib/bunyan-imports.mjs +98 -0
- package/rules/js-run/lib/check-env-scan.mjs +338 -0
- package/rules/js-run/lib/conn-file-rules.mjs +214 -0
- package/rules/js-run/lib/conn-imports-scan.mjs +154 -0
- package/rules/js-run/lib/docs/bunyan-imports.md +121 -0
- package/rules/js-run/lib/docs/check-env-scan.md +438 -0
- package/rules/js-run/lib/docs/conn-file-rules.md +304 -0
- package/rules/js-run/lib/docs/conn-imports-scan.md +208 -0
- package/rules/js-run/lib/docs/index.md +16 -0
- package/rules/js-run/lib/docs/promise-settimeout-scan.md +334 -0
- package/rules/js-run/lib/docs/temporal-scan.md +29 -0
- package/rules/js-run/lib/promise-settimeout-scan.mjs +128 -0
- package/rules/js-run/lib/temporal-scan.mjs +52 -0
- package/rules/js-run/main.json +1 -0
- package/rules/js-run/main.mdc +16 -0
- package/rules/js-run/package_json/concern.json +9 -0
- package/rules/js-run/package_json/package_json.mdc +44 -0
- package/rules/js-run/package_json/package_json.rego +37 -0
- package/rules/js-run/package_json/template/package.json.deny.json +22 -0
- package/rules/js-run/project-structure/concern.json +3 -0
- package/rules/js-run/project-structure/project-structure.mdc +11 -0
- package/rules/js-run/runtime/concern.json +12 -0
- package/rules/js-run/runtime/docs/fix-runtime.md +27 -0
- package/rules/js-run/runtime/docs/main.md +35 -0
- package/rules/js-run/runtime/fix-runtime.mjs +46 -0
- package/rules/js-run/runtime/main.mjs +496 -0
- package/rules/js-run/runtime/runtime.mdc +184 -0
- package/rules/js-run/scope/concern.json +3 -0
- package/rules/js-run/scope/scope.mdc +11 -0
- package/rules/npm-module/docs/index.md +11 -0
- package/rules/npm-module/emit_types_config/concern.json +9 -0
- package/rules/npm-module/emit_types_config/docs/fix-emit_types_config.md +25 -0
- package/rules/npm-module/emit_types_config/docs/index.md +9 -0
- package/rules/npm-module/emit_types_config/emit_types_config.mdc +43 -0
- package/rules/npm-module/emit_types_config/emit_types_config.rego +28 -0
- package/rules/npm-module/emit_types_config/fix-emit_types_config.mjs +5 -0
- package/rules/npm-module/emit_types_config/template/tsconfig.emit-types.json.snippet.json +9 -0
- package/rules/npm-module/header_doc_pointer/concern.json +5 -0
- package/rules/npm-module/header_doc_pointer/docs/main.md +40 -0
- package/rules/npm-module/header_doc_pointer/header_doc_pointer.mdc +18 -0
- package/rules/npm-module/header_doc_pointer/main.mjs +131 -0
- package/rules/npm-module/main.json +1 -0
- package/rules/npm-module/main.mdc +36 -0
- package/rules/npm-module/npm_package_json/concern.json +9 -0
- package/rules/npm-module/npm_package_json/docs/fix-npm_package_json.md +26 -0
- package/rules/npm-module/npm_package_json/docs/index.md +9 -0
- package/rules/npm-module/npm_package_json/fix-npm_package_json.mjs +5 -0
- package/rules/npm-module/npm_package_json/npm_package_json.mdc +57 -0
- package/rules/npm-module/npm_package_json/npm_package_json.rego +73 -0
- package/rules/npm-module/npm_package_json/template/package.json.snippet.json +1 -0
- package/rules/npm-module/package_structure/concern.json +5 -0
- package/rules/npm-module/package_structure/docs/main.md +37 -0
- package/rules/npm-module/package_structure/main.mjs +448 -0
- package/rules/npm-module/package_structure/package_structure.mdc +63 -0
- package/rules/npm-module/root_package_json/concern.json +9 -0
- package/rules/npm-module/root_package_json/docs/fix-root_package_json.md +25 -0
- package/rules/npm-module/root_package_json/docs/index.md +9 -0
- package/rules/npm-module/root_package_json/fix-root_package_json.mjs +5 -0
- package/rules/npm-module/root_package_json/root_package_json.mdc +41 -0
- package/rules/npm-module/root_package_json/root_package_json.rego +28 -0
- package/rules/npm-module/root_package_json/template/package.json.snippet.json +1 -0
- package/rules/npm-module/rule_meta/concern.json +8 -0
- package/rules/npm-module/rule_meta/docs/main.md +35 -0
- package/rules/npm-module/rule_meta/main.mjs +119 -0
- package/rules/npm-module/rule_meta/rule_meta.mdc +11 -0
- package/rules/npm-module/skill_meta/concern.json +5 -0
- package/rules/npm-module/skill_meta/docs/main.md +149 -0
- package/rules/npm-module/skill_meta/main.mjs +91 -0
- package/rules/npm-module/skill_meta/skill_meta.mdc +11 -0
- package/rules/tool-surface/docs/index.md +11 -0
- package/rules/tool-surface/main.json +6 -0
- package/rules/tool-surface/main.mdc +72 -0
- package/rules/vue/composition-api/composition-api.mdc +82 -0
- package/rules/vue/composition-api/concern.json +3 -0
- package/rules/vue/docs/index.md +11 -0
- package/rules/vue/lib/docs/index.md +11 -0
- package/rules/vue/lib/docs/vue-forbidden-imports.md +265 -0
- package/rules/vue/lib/vue-forbidden-imports.mjs +240 -0
- package/rules/vue/main.json +1 -0
- package/rules/vue/main.mdc +18 -0
- package/rules/vue/nheader-layout/concern.json +3 -0
- package/rules/vue/nheader-layout/nheader-layout.mdc +171 -0
- package/rules/vue/package_json/concern.json +9 -0
- package/rules/vue/package_json/package_json.mdc +30 -0
- package/rules/vue/package_json/package_json.rego +140 -0
- package/rules/vue/packages/concern.json +6 -0
- package/rules/vue/packages/docs/index.md +11 -0
- package/rules/vue/packages/docs/main.md +35 -0
- package/rules/vue/packages/main.mjs +575 -0
- package/rules/vue/packages/packages.mdc +56 -0
- package/rules/vue/quasar-ui/concern.json +3 -0
- package/rules/vue/quasar-ui/quasar-ui.mdc +32 -0
- package/rules/vue/structure/concern.json +3 -0
- package/rules/vue/structure/structure.mdc +101 -0
- package/rules/vue/testing/concern.json +3 -0
- package/rules/vue/testing/testing.mdc +40 -0
- package/rules/vue/tfm-translations/concern.json +7 -0
- package/rules/vue/tfm-translations/docs/main.md +29 -0
- package/rules/vue/tfm-translations/main.mjs +55 -0
- package/rules/vue/tfm-translations/tfm-translations.mdc +32 -0
- package/rules/vue/vite-config/concern.json +3 -0
- package/rules/vue/vite-config/vite-config.mdc +153 -0
- package/rules/vue/vite-env/concern.json +3 -0
- package/rules/vue/vite-env/vite-env.mdc +61 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: promise-settimeout-scan.mjs
|
|
4
|
+
resource: plugins/lang-js/rules/js-run/lib/promise-settimeout-scan.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 83b41b33
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Модуль `promise-settimeout-scan.mjs` — це AST-сканер, який виявляє у вихідному коді JavaScript/TypeScript антипаттерн «обгортка `setTimeout` у `new Promise`» виду:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
new Promise(resolve => setTimeout(resolve, ms))
|
|
13
|
+
// або
|
|
14
|
+
await new Promise(resolve => setTimeout(resolve, ms))
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Згідно з правилом `js-run.mdc` (секція «Паузи через `setTimeout`») такий код потрібно замінити на ідіоматичний імпорт із `node:timers/promises`:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
21
|
+
|
|
22
|
+
await sleep(ms)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Сканер працює **структурно** (по AST), без regex-у по тілу: шукає `NewExpression`, у якого callee — Identifier `Promise`, а єдиний аргумент — функція з одним параметром-resolve, тіло якої — єдиний виклик `setTimeout(<resolve>, ms)`. Перший аргумент `setTimeout` мусить бути «голим» resolve — або сам ідентифікатор, або тривіальна безпараметрична обгортка `() => resolve()` / `function () { resolve() }` без жодних переданих аргументів. Якщо у виклик `resolve` передається значення — це вже не «чиста пауза», і паттерн **не** вмикається.
|
|
26
|
+
|
|
27
|
+
Сканер **толерантний до синтаксичних помилок**: якщо файл не парситься, повертається порожній список (узгоджено з рештою AST-сканерів проєкту — спочатку треба полагодити синтаксис, а вже потім ловити структурні порушення).
|
|
28
|
+
|
|
29
|
+
## Експорти / API
|
|
30
|
+
|
|
31
|
+
| Експорт | Тип | Призначення |
|
|
32
|
+
| ----------------------------------- | ---------- | ---------------------------------------------------------------------------------- |
|
|
33
|
+
| `findPromiseSetTimeoutInText` | `function` | Знаходить усі входження антипаттерну в тексті, повертає масив `{ line, snippet }`. |
|
|
34
|
+
| `isPromiseSetTimeoutScanSourceFile` | `function` | Фільтр по відносному шляху: чи варто взагалі сканувати цей файл (за розширенням). |
|
|
35
|
+
|
|
36
|
+
Внутрішні (не експортовані) хелпери, які формують ядро аналізу:
|
|
37
|
+
|
|
38
|
+
- `isBareResolveCallback(arg, paramName)` — перевірка «чистоти» першого аргументу `setTimeout`.
|
|
39
|
+
- `extractSingleCallExpression(body)` — витягнення єдиного `CallExpression` з тіла функції.
|
|
40
|
+
- `isPromiseSetTimeoutDelay(node)` — головний предикат паттерну на рівні `NewExpression`.
|
|
41
|
+
- `walkAst(node, visit)` — простий рекурсивний обхід AST.
|
|
42
|
+
|
|
43
|
+
Константа модульного рівня:
|
|
44
|
+
|
|
45
|
+
- `SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/` — regex розширень JS/TS-сім'ї (`.js`, `.mjs`, `.cjs`, `.ts`, `.mts`, `.cts`, `.jsx`, `.tsx`).
|
|
46
|
+
|
|
47
|
+
## Функції
|
|
48
|
+
|
|
49
|
+
### `findPromiseSetTimeoutInText(content, virtualPath = 'scan.ts')`
|
|
50
|
+
|
|
51
|
+
**Призначення.** Публічна точка входу: парсить вихідний код, обходить AST і збирає всі позиції, де знайдено антипаттерн «`new Promise(... setTimeout ...)`».
|
|
52
|
+
|
|
53
|
+
**Сигнатура.**
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
export function findPromiseSetTimeoutInText(
|
|
57
|
+
content: string,
|
|
58
|
+
virtualPath?: string
|
|
59
|
+
): { line: number, snippet: string }[]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Параметри.**
|
|
63
|
+
|
|
64
|
+
- `content: string` — повний текст файлу для сканування.
|
|
65
|
+
- `virtualPath: string` (за замовчуванням `'scan.ts'`) — віртуальний шлях, який передається у `parseProgramOrNull` для вибору мови парсера (TS/JSX тощо). Не використовується для read/write-у на диск; впливає лише на режим парсингу.
|
|
66
|
+
|
|
67
|
+
**Повертає.** Масив об'єктів `{ line: number, snippet: string }` — по одному запису на кожне виявлене порушення:
|
|
68
|
+
|
|
69
|
+
- `line` — 1-based номер рядка, де починається `NewExpression` (через `offsetToLine`).
|
|
70
|
+
- `snippet` — нормалізований текст самого `new Promise(...)`-виразу (через `normalizeSnippet`).
|
|
71
|
+
|
|
72
|
+
Якщо AST не побудувався (синтаксична помилка), повертає порожній масив `[]`.
|
|
73
|
+
|
|
74
|
+
**Side effects.** Чиста функція — жодного I/O, читання env, мутацій глобального стану. Усі дані повертаються через return value.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### `isPromiseSetTimeoutScanSourceFile(relativePath)`
|
|
79
|
+
|
|
80
|
+
**Призначення.** Фільтр для зовнішнього раннера/обхідника файлів: чи варто взагалі парсити цей файл.
|
|
81
|
+
|
|
82
|
+
**Сигнатура.**
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
export function isPromiseSetTimeoutScanSourceFile(
|
|
86
|
+
relativePath: string
|
|
87
|
+
): boolean
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Параметри.**
|
|
91
|
+
|
|
92
|
+
- `relativePath: string` — відносний шлях до файлу (наприклад, `src/utils/sleep.ts`).
|
|
93
|
+
|
|
94
|
+
**Повертає.** `true`, якщо:
|
|
95
|
+
|
|
96
|
+
1. Розширення збігається з `SOURCE_FILE_RE` (JS/TS-сім'я), **та**
|
|
97
|
+
2. Шлях **не** закінчується на `.d.ts` (декларації типів пропускаються).
|
|
98
|
+
|
|
99
|
+
Інакше — `false`.
|
|
100
|
+
|
|
101
|
+
**Side effects.** Немає.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### `isBareResolveCallback(arg, paramName)` (внутрішня)
|
|
106
|
+
|
|
107
|
+
**Призначення.** Перевіряє, що перший аргумент `setTimeout` — це або сам ідентифікатор `resolve`, або тривіальна безпараметрична обгортка, яка викликає `resolve()` без значення.
|
|
108
|
+
|
|
109
|
+
**Сигнатура.**
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
function isBareResolveCallback(
|
|
113
|
+
arg: Record<string, unknown> | null | undefined,
|
|
114
|
+
paramName: string
|
|
115
|
+
): boolean
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Параметри.**
|
|
119
|
+
|
|
120
|
+
- `arg` — AST-вузол першого аргументу виклику `setTimeout`.
|
|
121
|
+
- `paramName` — ім'я параметра-resolve у тіла-функції `Promise` (зазвичай `'resolve'`, але приймається будь-яке ім'я-Identifier).
|
|
122
|
+
|
|
123
|
+
**Повертає.** `true`, якщо аргумент — це:
|
|
124
|
+
|
|
125
|
+
- `Identifier` з іменем `paramName` (наприклад, `setTimeout(resolve, ms)`); **або**
|
|
126
|
+
- `ArrowFunctionExpression` / `FunctionExpression` без параметрів, тіло якого — рівно один `CallExpression`, callee — `Identifier paramName`, а список аргументів виклику — порожній (наприклад, `() => resolve()` або `function () { resolve() }`).
|
|
127
|
+
|
|
128
|
+
В інших випадках — `false`. Зокрема, якщо у виклик `resolve(x)` передається значення — це **не** «чиста пауза» (бо результат `await` був би `x`, а не `undefined`), і антипаттерн **не** фіксується.
|
|
129
|
+
|
|
130
|
+
**Side effects.** Немає.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### `extractSingleCallExpression(body)` (внутрішня)
|
|
135
|
+
|
|
136
|
+
**Призначення.** Витягує єдиний `CallExpression` з тіла функції — як у концизній стрілковій формі (`() => foo()`), так і в блоковій з рівно одним стейтментом (`() => { foo() }`).
|
|
137
|
+
|
|
138
|
+
**Сигнатура.**
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
function extractSingleCallExpression(
|
|
142
|
+
body: unknown
|
|
143
|
+
): Record<string, unknown> | null
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Параметри.**
|
|
147
|
+
|
|
148
|
+
- `body` — AST-вузол тіла функції (може бути `CallExpression`, `BlockStatement` або щось інше).
|
|
149
|
+
|
|
150
|
+
**Повертає.** AST-вузол `CallExpression`, якщо:
|
|
151
|
+
|
|
152
|
+
- `body.type === 'CallExpression'` — повертає сам body; **або**
|
|
153
|
+
- `body.type === 'BlockStatement'`, масив `body.body` містить рівно один елемент, цей елемент — `ExpressionStatement`, а його `expression.type === 'CallExpression'` — повертає цей `expression`.
|
|
154
|
+
|
|
155
|
+
Інакше — `null`.
|
|
156
|
+
|
|
157
|
+
**Side effects.** Немає.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### `isPromiseSetTimeoutDelay(node)` (внутрішня)
|
|
162
|
+
|
|
163
|
+
**Призначення.** Головний предикат паттерну: чи це `NewExpression` виду `new Promise(<resolve> => setTimeout(<resolve>, ms))`.
|
|
164
|
+
|
|
165
|
+
**Сигнатура.**
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
function isPromiseSetTimeoutDelay(
|
|
169
|
+
node: Record<string, unknown> | null | undefined
|
|
170
|
+
): boolean
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Параметри.**
|
|
174
|
+
|
|
175
|
+
- `node` — довільний AST-вузол (під час обходу через `walkAst`).
|
|
176
|
+
|
|
177
|
+
**Повертає.** `true`, якщо всі наступні умови виконані:
|
|
178
|
+
|
|
179
|
+
1. `node.type === 'NewExpression'`;
|
|
180
|
+
2. `node.callee` — `Identifier` з іменем `'Promise'` (саме глобальний/локальний ідентифікатор, без member-доступу `foo.Promise`);
|
|
181
|
+
3. `node.arguments.length === 1`;
|
|
182
|
+
4. Цей єдиний аргумент — `ArrowFunctionExpression` або `FunctionExpression` з мінімум одним параметром;
|
|
183
|
+
5. Перший параметр — `Identifier` (запам'ятовується його `name` як ім'я resolve);
|
|
184
|
+
6. Тіло функції містить рівно один `CallExpression` (через `extractSingleCallExpression`);
|
|
185
|
+
7. Callee цього виклику — `Identifier` з іменем `'setTimeout'` (джерело — глобальне, з `node:timers`, з `globalThis` — для сканера не важливо);
|
|
186
|
+
8. У виклику `setTimeout` хоча б один аргумент;
|
|
187
|
+
9. Перший аргумент `setTimeout` — «голий» resolve (через `isBareResolveCallback`).
|
|
188
|
+
|
|
189
|
+
Якщо будь-яка умова порушена — повертає `false`.
|
|
190
|
+
|
|
191
|
+
**Side effects.** Немає.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### `walkAst(node, visit)` (внутрішня)
|
|
196
|
+
|
|
197
|
+
**Призначення.** Простий generic-обхід AST: рекурсивно спускається по всіх властивостях і елементах масивів, викликаючи `visit` для кожного об'єкта-вузла, який має поле `type` (тобто справжнього AST-вузла, а не службового мета-обʼєкта).
|
|
198
|
+
|
|
199
|
+
**Сигнатура.**
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
function walkAst(
|
|
203
|
+
node: unknown,
|
|
204
|
+
visit: (n: Record<string, unknown>) => void
|
|
205
|
+
): void
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Параметри.**
|
|
209
|
+
|
|
210
|
+
- `node` — корінь або під-вузол (Program, окремий вузол, масив, скаляр — все підтримується).
|
|
211
|
+
- `visit` — колбек, який отримує кожен AST-вузол із полем `type`. Викликається **до** спуску в дочірні поля (pre-order).
|
|
212
|
+
|
|
213
|
+
**Поведінка.**
|
|
214
|
+
|
|
215
|
+
- `null`/примітиви — пропускаються;
|
|
216
|
+
- Масиви — обходяться поелементно;
|
|
217
|
+
- Об'єкти з `typeof node.type === 'string'` — спершу передаються у `visit`, потім обходяться їхні поля;
|
|
218
|
+
- Поле з ключем `'parent'` пропускається (захист від циклів у деяких AST-моделях, де є зворотні посилання);
|
|
219
|
+
- Інші поля-об'єкти — рекурсивно обходяться.
|
|
220
|
+
|
|
221
|
+
**Повертає.** `undefined`.
|
|
222
|
+
|
|
223
|
+
**Side effects.** Викликає `visit` зовнішнього коду — single side effect виноситься назовні через колбек.
|
|
224
|
+
|
|
225
|
+
## Залежності
|
|
226
|
+
|
|
227
|
+
### Зовнішні (relative imports)
|
|
228
|
+
|
|
229
|
+
Із `../../../scripts/utils/ast-scan-utils.mjs`:
|
|
230
|
+
|
|
231
|
+
- `parseProgramOrNull(content, virtualPath)` — парсить вихідний код у Program-вузол AST з урахуванням мови (вибір TS/JS/JSX за розширенням у `virtualPath`); повертає `null` при синтаксичних помилках.
|
|
232
|
+
- `offsetToLine(content, offset)` — перетворює byte/char offset у 1-based номер рядка.
|
|
233
|
+
- `normalizeSnippet(text)` — нормалізує текст сніпета (стискання пробілів/переносів) для зручного виводу у звітах.
|
|
234
|
+
|
|
235
|
+
### Глобальні
|
|
236
|
+
|
|
237
|
+
- `RegExp` (`SOURCE_FILE_RE`), `Array.isArray`, `Object.keys`, `String.prototype.endsWith`, `String.prototype.slice`, `Array.prototype.push` — стандартні API ES.
|
|
238
|
+
|
|
239
|
+
### Без рантайм-залежностей
|
|
240
|
+
|
|
241
|
+
Жодних `node:`-вбудованих модулів, жодних npm-пакетів, жодних звернень до файлової системи, env або мережі.
|
|
242
|
+
|
|
243
|
+
## Потік виконання / Використання
|
|
244
|
+
|
|
245
|
+
### Типовий сценарій (з раннера/перевіряча)
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
import { findPromiseSetTimeoutInText, isPromiseSetTimeoutScanSourceFile } from './promise-settimeout-scan.mjs'
|
|
249
|
+
import { readFile } from 'node:fs/promises'
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
*
|
|
253
|
+
*/
|
|
254
|
+
async function scanRepo(files) {
|
|
255
|
+
const violations = []
|
|
256
|
+
for (const relPath of files) {
|
|
257
|
+
if (!isPromiseSetTimeoutScanSourceFile(relPath)) continue
|
|
258
|
+
const content = await readFile(relPath, 'utf8')
|
|
259
|
+
const hits = findPromiseSetTimeoutInText(content, relPath)
|
|
260
|
+
for (const hit of hits) {
|
|
261
|
+
violations.push({ file: relPath, line: hit.line, snippet: hit.snippet })
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return violations
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Алгоритм всередині `findPromiseSetTimeoutInText`
|
|
269
|
+
|
|
270
|
+
1. **Парсинг.** `parseProgramOrNull(content, virtualPath)` → якщо `null` (синтаксична помилка), функція одразу повертає `[]`.
|
|
271
|
+
2. **Ініціалізація.** Створюється порожній масив `out` для накопичення знахідок.
|
|
272
|
+
3. **Обхід AST.** `walkAst(program, visit)` рекурсивно проходить весь Program-вузол.
|
|
273
|
+
4. **Перевірка паттерну.** Для кожного AST-вузла з полем `type` викликається `visit`, який:
|
|
274
|
+
- Перевіряє `isPromiseSetTimeoutDelay(node)`;
|
|
275
|
+
- Якщо `true` — пушить у `out` об'єкт `{ line, snippet }`, де `line` обчислюється з `node.start` через `offsetToLine`, а `snippet` — з `content.slice(node.start, node.end)` через `normalizeSnippet`.
|
|
276
|
+
5. **Повернення.** Масив `out` повертається як результат (може бути порожнім, якщо порушень немає).
|
|
277
|
+
|
|
278
|
+
### Що сканер ловить (приклади позитивних спрацювань)
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
new Promise(resolve => setTimeout(resolve, 1000))
|
|
282
|
+
await new Promise(r => setTimeout(r, ms))
|
|
283
|
+
new Promise(resolve => setTimeout(() => resolve(), 500))
|
|
284
|
+
new Promise(function (resolve) {
|
|
285
|
+
setTimeout(resolve, 200)
|
|
286
|
+
})
|
|
287
|
+
new Promise(resolve => {
|
|
288
|
+
setTimeout(function () {
|
|
289
|
+
resolve()
|
|
290
|
+
}, 100)
|
|
291
|
+
})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Що сканер свідомо **не** ловить (негативні приклади)
|
|
295
|
+
|
|
296
|
+
- `new Promise(resolve => setTimeout(() => resolve(value), ms))` — у `resolve` передається значення; це не «чиста пауза».
|
|
297
|
+
- `new Promise((resolve, reject) => setTimeout(() => doStuff().then(resolve, reject), ms))` — у тілі більше ніж один call або інший callee.
|
|
298
|
+
- `new MyPromise(resolve => setTimeout(resolve, ms))` — callee — не `Promise`.
|
|
299
|
+
- `globalThis.Promise` як callee (MemberExpression) — не Identifier.
|
|
300
|
+
- Файли з синтаксичними помилками — `parseProgramOrNull` повертає `null`, сканер віддає `[]`.
|
|
301
|
+
- Файли з розширенням `.d.ts` — фільтруються `isPromiseSetTimeoutScanSourceFile`.
|
|
302
|
+
|
|
303
|
+
### Інтеграція з правилом `js-run.mdc`
|
|
304
|
+
|
|
305
|
+
Цей сканер — частина перевірок правила js-run, секція «Паузи через `setTimeout`». Він використовується check-скриптом правила для збору списку порушень, які потім виводяться користувачу зі вказівкою замінити обгортку на `setTimeout` із `node:timers/promises`. Сам файл `promise-settimeout-scan.mjs` не виконує жодних дій з виправлення — лише детектує.
|
|
306
|
+
|
|
307
|
+
## Rebuild Test
|
|
308
|
+
|
|
309
|
+
Перевірка контрактів модуля (умоглядно, без запуску):
|
|
310
|
+
|
|
311
|
+
1. **`isPromiseSetTimeoutScanSourceFile`:**
|
|
312
|
+
- `'src/a.ts'` → `true`; `'src/a.tsx'` → `true`; `'src/a.mjs'` → `true`; `'src/a.cjs'` → `true`.
|
|
313
|
+
- `'src/a.d.ts'` → `false` (декларації пропускаються).
|
|
314
|
+
- `'src/a.md'` / `'src/a.json'` → `false` (інше розширення).
|
|
315
|
+
|
|
316
|
+
2. **`findPromiseSetTimeoutInText` — позитивний кейс:**
|
|
317
|
+
- Вхід: `const s = new Promise(r => setTimeout(r, 100))`.
|
|
318
|
+
- Очікувано: масив з одним записом, `line === 1`, `snippet` містить нормалізований текст `new Promise(r => setTimeout(r, 100))`.
|
|
319
|
+
|
|
320
|
+
3. **`findPromiseSetTimeoutInText` — негативний кейс (передача значення):**
|
|
321
|
+
- Вхід: `new Promise(r => setTimeout(() => r(42), 10))`.
|
|
322
|
+
- Очікувано: `[]` (бо `isBareResolveCallback` відсіває виклики з аргументами).
|
|
323
|
+
|
|
324
|
+
4. **`findPromiseSetTimeoutInText` — синтаксична помилка:**
|
|
325
|
+
- Вхід: `const x = (` (незакритий вираз).
|
|
326
|
+
- Очікувано: `[]` (бо `parseProgramOrNull` повертає `null`).
|
|
327
|
+
|
|
328
|
+
5. **`findPromiseSetTimeoutInText` — інший callee:**
|
|
329
|
+
- Вхід: `new MyPromise(r => setTimeout(r, 1))`.
|
|
330
|
+
- Очікувано: `[]` (бо `node.callee.name !== 'Promise'`).
|
|
331
|
+
|
|
332
|
+
6. **`isPromiseSetTimeoutDelay` — більше одного стейтменту в блоці:**
|
|
333
|
+
- Тіло Promise-функції: `{ doSomething(); setTimeout(resolve, 10) }`.
|
|
334
|
+
- Очікувано: `false` (бо `extractSingleCallExpression` вимагає рівно один стейтмент).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: temporal-scan.mjs
|
|
4
|
+
resource: plugins/lang-js/rules/js-run/lib/temporal-scan.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 37bae946
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Цей файл є частиною системи, яка сканує код Bun workspace на наявність використання ключового слова `Temporal`. Він запобігає використанню `Temporal` у backend-коді, оскільки Bun 1.3.x ще не має глобального `Temporal`, та охоплює сценарії з імпортом та polyfill. Це забезпечує відповідність коду поточним вимогам Bun runtime щодо обробки часу.
|
|
10
|
+
|
|
11
|
+
## Поведінка
|
|
12
|
+
|
|
13
|
+
Знаходить використання identifier `Temporal` у тексті. Повертає список рядків та фрагментів коду, де зустрічається `Temporal`.
|
|
14
|
+
Чи сканувати файл за розширенням (JS/TS-сім'я, виключно з `.d.ts`). Повертає `true`, якщо файл має відповідне розширення, і не є файлом `.d.ts`.
|
|
15
|
+
|
|
16
|
+
## Публічний API
|
|
17
|
+
|
|
18
|
+
- findTemporalUsageInText — Знаходить згадки про `Temporal` у тексті.
|
|
19
|
+
- isTemporalScanSourceFile — Визначає, чи потрібно сканувати файл за розширенням (JavaScript/TypeScript або `.d.ts`).
|
|
20
|
+
|
|
21
|
+
## Гарантії поведінки
|
|
22
|
+
|
|
23
|
+
- Функція `findTemporalUsageInText` повертає `true` лише якщо знайде identifier `Temporal` у наданому тексті.
|
|
24
|
+
- Функція `findTemporalUsageInText` повертає `false` якщо identifier `Temporal` не знайдено.
|
|
25
|
+
- Функція `isTemporalScanSourceFile` повертає `true` якщо у файлі є identifier `Temporal`.
|
|
26
|
+
- Функція `isTemporalScanSourceFile` повертає `false` якщо у файлі немає identifier `Temporal`.
|
|
27
|
+
- Результат роботи `findTemporalUsageInText` не гарантує, що identifier `Temporal` використовується правильно.
|
|
28
|
+
- Результат роботи `isTemporalScanSourceFile` не гарантує, що використання `Temporal` є допустимим.
|
|
29
|
+
- Кеш не використовується.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Знаходить паттерн `new Promise(resolve => setTimeout(resolve, ms))` (з `await` чи без)
|
|
3
|
+
* у джерелах — таку обгортку треба замінити на `setTimeout` з `node:timers/promises`
|
|
4
|
+
* згідно з js-run.mdc, секція «Паузи через setTimeout».
|
|
5
|
+
*
|
|
6
|
+
* Семантика — структурна (без regex по тілу): `NewExpression` з ідентифікатор-callee `Promise`
|
|
7
|
+
* і єдиним аргументом-функцією, тіло якої — виклик `setTimeout(<resolve>, ms)`. Перший
|
|
8
|
+
* аргумент `setTimeout` має передавати `resolve` напряму або тривіально загорнутим у
|
|
9
|
+
* безпараметричну функцію `() => resolve()` / `function () { resolve() }` без жодних
|
|
10
|
+
* аргументів — інакше це не «чиста пауза», і паттерн не вмикається.
|
|
11
|
+
*
|
|
12
|
+
* Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках повертається
|
|
13
|
+
* порожній результат (як інші сканери — спочатку треба полагодити синтаксис).
|
|
14
|
+
*/
|
|
15
|
+
import { normalizeSnippet, offsetToLine, parseProgramOrNull } from '@7n/rules/scripts/utils/ast-scan-utils.mjs'
|
|
16
|
+
|
|
17
|
+
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Чи аргумент, який передають у `setTimeout`, — це «голий» виклик `resolve`
|
|
21
|
+
* (тобто сам ідентифікатор або `() => resolve()` без аргументів).
|
|
22
|
+
* @param {Record<string, unknown> | null | undefined} arg AST-вузол першого аргументу `setTimeout`
|
|
23
|
+
* @param {string} paramName ім'я параметра-resolve у тіла-функції Promise
|
|
24
|
+
* @returns {boolean} `true`, якщо це чиста передача resolve без значення
|
|
25
|
+
*/
|
|
26
|
+
function isBareResolveCallback(arg, paramName) {
|
|
27
|
+
if (!arg || typeof arg !== 'object') return false
|
|
28
|
+
if (arg.type === 'Identifier' && arg.name === paramName) return true
|
|
29
|
+
if (arg.type !== 'ArrowFunctionExpression' && arg.type !== 'FunctionExpression') return false
|
|
30
|
+
if ((arg.params?.length ?? 0) !== 0) return false
|
|
31
|
+
const callExpr = extractSingleCallExpression(arg.body)
|
|
32
|
+
if (!callExpr) return false
|
|
33
|
+
if (callExpr.callee?.type !== 'Identifier' || callExpr.callee.name !== paramName) return false
|
|
34
|
+
return !Array.isArray(callExpr.arguments) || callExpr.arguments.length === 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Якщо тіло функції — рівно один `CallExpression` (концизне `() => foo()` або
|
|
39
|
+
* `{ foo() }` без інших стейтментів), повертає його. Інакше — `null`.
|
|
40
|
+
* @param {unknown} body тіло функції з AST
|
|
41
|
+
* @returns {Record<string, unknown> | null} AST-вузол `CallExpression` або `null`
|
|
42
|
+
*/
|
|
43
|
+
function extractSingleCallExpression(body) {
|
|
44
|
+
if (!body || typeof body !== 'object') return null
|
|
45
|
+
if (body.type === 'CallExpression') return body
|
|
46
|
+
if (body.type !== 'BlockStatement') return null
|
|
47
|
+
if (!Array.isArray(body.body) || body.body.length !== 1) return null
|
|
48
|
+
const stmt = body.body[0]
|
|
49
|
+
if (!stmt || stmt.type !== 'ExpressionStatement') return null
|
|
50
|
+
const expr = stmt.expression
|
|
51
|
+
return expr?.type === 'CallExpression' ? expr : null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Чи це `NewExpression` виду `new Promise(<resolve> => setTimeout(<resolve>, ms))`.
|
|
56
|
+
* Параметр-resolve має бути простим Identifier; setTimeout — глобальним викликом
|
|
57
|
+
* за іменем (з будь-якого джерела — node:timers, global, тощо: значення для нас має
|
|
58
|
+
* лише структурний паттерн).
|
|
59
|
+
* @param {Record<string, unknown> | null | undefined} node AST-вузол
|
|
60
|
+
* @returns {boolean} `true`, якщо це проблемний паттерн «обгортки таймера у Promise»
|
|
61
|
+
*/
|
|
62
|
+
function isPromiseSetTimeoutDelay(node) {
|
|
63
|
+
if (!node || node.type !== 'NewExpression') return false
|
|
64
|
+
if (node.callee?.type !== 'Identifier' || node.callee.name !== 'Promise') return false
|
|
65
|
+
if (!Array.isArray(node.arguments) || node.arguments.length !== 1) return false
|
|
66
|
+
const fn = node.arguments[0]
|
|
67
|
+
if (!fn || (fn.type !== 'ArrowFunctionExpression' && fn.type !== 'FunctionExpression')) return false
|
|
68
|
+
if (!Array.isArray(fn.params) || fn.params.length === 0) return false
|
|
69
|
+
const firstParam = fn.params[0]
|
|
70
|
+
if (!firstParam || firstParam.type !== 'Identifier') return false
|
|
71
|
+
const setTimeoutCall = extractSingleCallExpression(fn.body)
|
|
72
|
+
if (!setTimeoutCall) return false
|
|
73
|
+
if (setTimeoutCall.callee?.type !== 'Identifier' || setTimeoutCall.callee.name !== 'setTimeout') return false
|
|
74
|
+
if (!Array.isArray(setTimeoutCall.arguments) || setTimeoutCall.arguments.length < 1) return false
|
|
75
|
+
return isBareResolveCallback(setTimeoutCall.arguments[0], firstParam.name)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Простий рекурсивний обхід AST: заходимо в усі об'єкти/масиви, щоб знайти `NewExpression`.
|
|
80
|
+
* @param {unknown} node корінь або під-вузол AST
|
|
81
|
+
* @param {(n: Record<string, unknown>) => void} visit виклик для кожного об'єкта-вузла з `type`
|
|
82
|
+
* @returns {void}
|
|
83
|
+
*/
|
|
84
|
+
function walkAst(node, visit) {
|
|
85
|
+
if (!node || typeof node !== 'object') return
|
|
86
|
+
if (Array.isArray(node)) {
|
|
87
|
+
for (const item of node) walkAst(item, visit)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
if (typeof node.type === 'string') {
|
|
91
|
+
visit(node)
|
|
92
|
+
}
|
|
93
|
+
for (const [key, v] of Object.entries(node)) {
|
|
94
|
+
if (key === 'parent') continue
|
|
95
|
+
if (v && typeof v === 'object') walkAst(v, visit)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Знаходить усі `new Promise(resolve => setTimeout(resolve, ms))` у тексті.
|
|
101
|
+
* @param {string} content вихідний код
|
|
102
|
+
* @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/foo.ts`)
|
|
103
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
104
|
+
*/
|
|
105
|
+
export function findPromiseSetTimeoutInText(content, virtualPath = 'scan.ts') {
|
|
106
|
+
const program = parseProgramOrNull(content, virtualPath)
|
|
107
|
+
if (!program) return []
|
|
108
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
109
|
+
const out = []
|
|
110
|
+
walkAst(program, node => {
|
|
111
|
+
if (!isPromiseSetTimeoutDelay(node)) return
|
|
112
|
+
out.push({
|
|
113
|
+
line: offsetToLine(content, node.start),
|
|
114
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
return out
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Чи сканувати цей файл за розширенням (JS/TS-сім'я, виключно з `.d.ts`).
|
|
122
|
+
* @param {string} relativePath відносний шлях до файлу
|
|
123
|
+
* @returns {boolean} `true`, якщо розширення підходить для сканування
|
|
124
|
+
*/
|
|
125
|
+
export function isPromiseSetTimeoutScanSourceFile(relativePath) {
|
|
126
|
+
if (!SOURCE_FILE_RE.test(relativePath)) return false
|
|
127
|
+
return !relativePath.endsWith('.d.ts')
|
|
128
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-сканер заборони `Temporal` у Bun runtime-коді.
|
|
3
|
+
*
|
|
4
|
+
* Bun 1.3.x ще не має глобального `Temporal`, тому правило js-run забороняє
|
|
5
|
+
* будь-який identifier `Temporal` у backend workspace-коді. Заборона свідомо
|
|
6
|
+
* охоплює polyfill/import-сценарії: у цьому репозиторії канон для часу лишається
|
|
7
|
+
* через `Date` або ін'єкцію timestamp у чисті функції.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
normalizeSnippet,
|
|
11
|
+
offsetToLine,
|
|
12
|
+
parseProgramOrNull,
|
|
13
|
+
walkAstWithAncestors
|
|
14
|
+
} from '@7n/rules/scripts/utils/ast-scan-utils.mjs'
|
|
15
|
+
|
|
16
|
+
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Знаходить використання identifier `Temporal` у тексті.
|
|
20
|
+
* @param {string} content вихідний код
|
|
21
|
+
* @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/foo.ts`)
|
|
22
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
23
|
+
*/
|
|
24
|
+
export function findTemporalUsageInText(content, virtualPath = 'scan.ts') {
|
|
25
|
+
const program = parseProgramOrNull(content, virtualPath)
|
|
26
|
+
if (!program) return []
|
|
27
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
28
|
+
const out = []
|
|
29
|
+
/** @type {Set<string>} */
|
|
30
|
+
const seen = new Set()
|
|
31
|
+
walkAstWithAncestors(program, [], node => {
|
|
32
|
+
if (node.type !== 'Identifier' || node.name !== 'Temporal') return
|
|
33
|
+
const key = `${node.start}:${node.end}`
|
|
34
|
+
if (seen.has(key)) return
|
|
35
|
+
seen.add(key)
|
|
36
|
+
out.push({
|
|
37
|
+
line: offsetToLine(content, node.start),
|
|
38
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Чи сканувати цей файл за розширенням (JS/TS-сім'я, виключно з `.d.ts`).
|
|
46
|
+
* @param {string} relativePath відносний шлях до файлу
|
|
47
|
+
* @returns {boolean} `true`, якщо розширення підходить для сканування
|
|
48
|
+
*/
|
|
49
|
+
export function isTemporalScanSourceFile(relativePath) {
|
|
50
|
+
if (!SOURCE_FILE_RE.test(relativePath)) return false
|
|
51
|
+
return !relativePath.endsWith('.d.ts')
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "auto": { "predicate": "nestedPackageWithoutVite" } }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Це правила для backend проектів на JavaScript/Node.js, сюди входять і job і WEB сервери.
|
|
3
|
+
globs: "**/package.json,**/jsconfig.json,**/src/**/*.{js,mjs,cjs,ts,tsx}"
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
version: '1.12'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Правило охоплює backend Node.js workspace-пакети (jobs, GraphQL/HTTP-сервери, CLI) — визначення меж застосування, вимоги до runtime, структуру проекту, конфігурацію, логування, підключення до БД/GraphQL і безпечне використання env-змінних.
|
|
9
|
+
|
|
10
|
+
## Швидкий gate через conftest
|
|
11
|
+
|
|
12
|
+
Rego-пакети, які запускає `npx @7n/rules fix js-run` / `npx @7n/rules check`:
|
|
13
|
+
|
|
14
|
+
- [jsconfig.json.snippet.json](./jsconfig/template/jsconfig.json.snippet.json)
|
|
15
|
+
- [configmap.yaml.contains.yml](./configmap/template/configmap.yaml.contains.yml)
|
|
16
|
+
- [package.json.deny.json](./package_json/template/package.json.deny.json)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
## Rego-gate: package.json (залежності та scripts)
|
|
2
|
+
|
|
3
|
+
Rego-пакет: `js-run.package_json`
|
|
4
|
+
|
|
5
|
+
**Цільові файли:** `package.json` у backend workspace-пакетах (без `vite` у `devDependencies`)
|
|
6
|
+
|
|
7
|
+
Перевіряє три класи порушень за deny-списком із template:
|
|
8
|
+
|
|
9
|
+
Канон deny-списку: [package.json.deny.json](./template/package.json.deny.json)
|
|
10
|
+
|
|
11
|
+
**1. Заборонені залежності** (`dependencies` / `devDependencies`)
|
|
12
|
+
|
|
13
|
+
| Пакет | Причина |
|
|
14
|
+
|-------|---------|
|
|
15
|
+
| `bunyan` | використовуй стандартні логери |
|
|
16
|
+
| `@nitra/bunyan` | використовуй стандартні логери |
|
|
17
|
+
|
|
18
|
+
**2. Заборонений рантайм у `scripts`** (лише backend-пакети без `vite`)
|
|
19
|
+
|
|
20
|
+
- Патерн `\bnode(\s|$)` — заміни `node` на `bun`
|
|
21
|
+
- Патерн `\benv\s+\$\(cat\s+[^)]+\)\s+bun\b` — заміни `env $(cat A B) bun` на `bun --env-file=A --env-file=B`
|
|
22
|
+
|
|
23
|
+
**✓ Правильно**
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{ "scripts": { "start": "bun src/index.js" } }
|
|
27
|
+
{ "scripts": { "start": "bun --env-file=.env src/index.js" } }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**✗ Неправильно**
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{ "scripts": { "start": "node src/index.js" } }
|
|
34
|
+
{ "scripts": { "start": "env $(cat .env .env.local) bun src/index.js" } }
|
|
35
|
+
{ "dependencies": { "bunyan": "^1.0.0" } }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Пакети з `vite` у `devDependencies` — frontend, поза областю js-run, перевірка scripts не застосовується.
|
|
39
|
+
|
|
40
|
+
## Використання @nitra/pino для логування
|
|
41
|
+
|
|
42
|
+
Проект використовує @nitra/pino для логування. Якщо в проекті присутній @nitra/bunyan, то він повинен бути замінений на @nitra/pino — як у `package.json`, так і в коді: усі `import` / `require` / динамічні `import()` з `@nitra/bunyan` (і застарілого `bunyan`) треба замінити на `@nitra/pino` і за потреби адаптувати виклики під його API.
|
|
43
|
+
|
|
44
|
+
Заборона `bunyan` / `@nitra/bunyan` у `dependencies` / `devDependencies` — rego-gate вище (канон: [package.json.deny.json](./template/package.json.deny.json)). Заборонені імпорти в коді (AST-сканер): `../lib/bunyan-imports.mjs` (перевірка запускається з `../runtime/main.mjs`).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Перевірка `package.json` (js-run.mdc).
|
|
2
|
+
#
|
|
3
|
+
# Канон надходить через --data: { "template": { "deny": ... } }
|
|
4
|
+
# Структура --data сформована з template/package.json.deny.json.
|
|
5
|
+
# AST-скан коду (`bunyan`/`process.env`/`#conn/*`) — у JS.
|
|
6
|
+
package js_run.package_json
|
|
7
|
+
|
|
8
|
+
import rego.v1
|
|
9
|
+
|
|
10
|
+
deny contains msg if {
|
|
11
|
+
some pkg, reason in data.template.deny.dependencies
|
|
12
|
+
pkg in object.keys(object.get(input, "dependencies", {}))
|
|
13
|
+
msg := sprintf("dependencies.%s — %s", [pkg, reason])
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
deny contains msg if {
|
|
17
|
+
some pkg, reason in data.template.deny.devDependencies
|
|
18
|
+
pkg in object.keys(object.get(input, "devDependencies", {}))
|
|
19
|
+
msg := sprintf("devDependencies.%s — %s", [pkg, reason])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# ── deny: `node` як рантайм у `scripts` (backend-пакети без vite) ─────────
|
|
23
|
+
|
|
24
|
+
deny contains msg if {
|
|
25
|
+
js_run_backend_package
|
|
26
|
+
is_object(input.scripts)
|
|
27
|
+
some script_name, script_value in input.scripts
|
|
28
|
+
is_string(script_value)
|
|
29
|
+
some rule in object.get(data.template.deny, "scriptsForbidden", [])
|
|
30
|
+
regex.match(rule.pattern, script_value)
|
|
31
|
+
msg := sprintf("package.json: scripts.%s — %s", [script_name, rule.message])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Frontend-пакети (`vite` у devDependencies) — поза js-run (див. js-run.mdc).
|
|
35
|
+
js_run_backend_package if {
|
|
36
|
+
not "vite" in object.keys(object.get(input, "devDependencies", {}))
|
|
37
|
+
}
|