@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,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,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
|
+
}
|