@7n/rules-lang-js 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +1 -1
  3. package/rules/style/admin_table/admin_table.mdc +88 -0
  4. package/rules/style/admin_table/concern.json +7 -0
  5. package/rules/style/admin_table/docs/index.md +9 -0
  6. package/rules/style/admin_table/docs/main.md +14 -0
  7. package/rules/style/admin_table/main.mjs +46 -0
  8. package/rules/style/colors/colors.mdc +21 -0
  9. package/rules/style/colors/concern.json +3 -0
  10. package/rules/style/docs/index.md +11 -0
  11. package/rules/style/gap/concern.json +7 -0
  12. package/rules/style/gap/docs/index.md +9 -0
  13. package/rules/style/gap/docs/main.md +15 -0
  14. package/rules/style/gap/gap.mdc +22 -0
  15. package/rules/style/gap/main.mjs +51 -0
  16. package/rules/style/lint/concern.json +7 -0
  17. package/rules/style/lint/docs/fix-lint.md +29 -0
  18. package/rules/style/lint/docs/index.md +11 -0
  19. package/rules/style/lint/docs/main.md +29 -0
  20. package/rules/style/lint/fix-lint.mjs +66 -0
  21. package/rules/style/lint/main.mjs +68 -0
  22. package/rules/style/main.json +1 -0
  23. package/rules/style/main.mdc +12 -0
  24. package/rules/style/package_json/concern.json +9 -0
  25. package/rules/style/package_json/docs/fix-package_json.md +26 -0
  26. package/rules/style/package_json/docs/index.md +9 -0
  27. package/rules/style/package_json/fix-package_json.mjs +3 -0
  28. package/rules/style/package_json/package_json.mdc +18 -0
  29. package/rules/style/package_json/package_json.rego +31 -0
  30. package/rules/style/package_json/template/package.json.snippet.json +5 -0
  31. package/rules/style/quasar/concern.json +3 -0
  32. package/rules/style/quasar/quasar.mdc +7 -0
  33. package/rules/style/quasar_fixes/concern.json +7 -0
  34. package/rules/style/quasar_fixes/docs/index.md +9 -0
  35. package/rules/style/quasar_fixes/docs/main.md +16 -0
  36. package/rules/style/quasar_fixes/main.mjs +57 -0
  37. package/rules/style/quasar_fixes/quasar_fixes.mdc +32 -0
  38. package/rules/style/tooling/concern.json +14 -0
  39. package/rules/style/tooling/docs/fix-tooling.md +29 -0
  40. package/rules/style/tooling/docs/index.md +12 -0
  41. package/rules/style/tooling/docs/main.md +34 -0
  42. package/rules/style/tooling/fix-tooling.mjs +58 -0
  43. package/rules/style/tooling/main.mjs +73 -0
  44. package/rules/style/tooling/tooling.mdc +85 -0
  45. package/rules/style/vscode_extensions/concern.json +9 -0
  46. package/rules/style/vscode_extensions/docs/fix-vscode_extensions.md +24 -0
  47. package/rules/style/vscode_extensions/docs/index.md +11 -0
  48. package/rules/style/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  49. package/rules/style/vscode_extensions/template/extensions.json.snippet.json +1 -0
  50. package/rules/style/vscode_extensions/vscode_extensions.mdc +13 -0
  51. package/rules/style/vscode_extensions/vscode_extensions.rego +13 -0
  52. package/rules/style/vscode_settings/concern.json +9 -0
  53. package/rules/style/vscode_settings/docs/fix-vscode_settings.md +26 -0
  54. package/rules/style/vscode_settings/docs/index.md +9 -0
  55. package/rules/style/vscode_settings/fix-vscode_settings.mjs +5 -0
  56. package/rules/style/vscode_settings/template/settings.json.snippet.json +5 -0
  57. package/rules/style/vscode_settings/vscode_settings.mdc +19 -0
  58. package/rules/style/vscode_settings/vscode_settings.rego +15 -0
  59. package/rules/test/docs/index.md +11 -0
  60. package/rules/test/lib/collect-test-file-offenders.mjs +49 -0
  61. package/rules/test/lib/docs/collect-test-file-offenders.md +29 -0
  62. package/rules/test/lib/docs/index.md +9 -0
  63. package/rules/test/location/concern.json +8 -0
  64. package/rules/test/location/docs/index.md +11 -0
  65. package/rules/test/location/docs/main.md +36 -0
  66. package/rules/test/location/location.mdc +52 -0
  67. package/rules/test/location/main.mjs +70 -0
  68. package/rules/test/main.json +1 -0
  69. package/rules/test/main.mdc +26 -0
  70. package/rules/test/no-bun-test-import/concern.json +7 -0
  71. package/rules/test/no-bun-test-import/docs/fix-no-bun-test-import.md +30 -0
  72. package/rules/test/no-bun-test-import/docs/index.md +10 -0
  73. package/rules/test/no-bun-test-import/docs/main.md +35 -0
  74. package/rules/test/no-bun-test-import/fix-no-bun-test-import.mjs +51 -0
  75. package/rules/test/no-bun-test-import/main.mjs +105 -0
  76. package/rules/test/no-bun-test-import/no-bun-test-import.mdc +9 -0
  77. package/rules/test/no-console-store-restore/concern.json +7 -0
  78. package/rules/test/no-console-store-restore/docs/index.md +11 -0
  79. package/rules/test/no-console-store-restore/docs/main.md +44 -0
  80. package/rules/test/no-console-store-restore/main.mjs +55 -0
  81. package/rules/test/no-console-store-restore/no-console-store-restore.mdc +11 -0
  82. package/rules/test/no-process-chdir/concern.json +7 -0
  83. package/rules/test/no-process-chdir/docs/index.md +11 -0
  84. package/rules/test/no-process-chdir/docs/main.md +32 -0
  85. package/rules/test/no-process-chdir/main.mjs +40 -0
  86. package/rules/test/no-process-chdir/no-process-chdir.mdc +15 -0
  87. package/rules/test/no-relative-fs-path/concern.json +7 -0
  88. package/rules/test/no-relative-fs-path/docs/index.md +11 -0
  89. package/rules/test/no-relative-fs-path/docs/main.md +34 -0
  90. package/rules/test/no-relative-fs-path/main.mjs +239 -0
  91. package/rules/test/no-relative-fs-path/no-relative-fs-path.mdc +22 -0
  92. package/rules/test/package_json/concern.json +11 -0
  93. package/rules/test/package_json/package_json.mdc +18 -0
  94. package/rules/test/package_json/package_json.rego +25 -0
  95. package/rules/test/package_json/template/package.json.contains.json +6 -0
  96. package/rules/test/sandbox-aware-test/concern.json +7 -0
  97. package/rules/test/sandbox-aware-test/docs/index.md +11 -0
  98. package/rules/test/sandbox-aware-test/docs/main.md +55 -0
  99. package/rules/test/sandbox-aware-test/main.mjs +89 -0
  100. package/rules/test/sandbox-aware-test/sandbox-aware-test.mdc +28 -0
  101. package/rules/test/stryker_config/concern.json +8 -0
  102. package/rules/test/stryker_config/data/stryker_config/docs/index.md +13 -0
  103. package/rules/test/stryker_config/data/stryker_config/docs/stryker-vue-macros-ignorer.md +31 -0
  104. package/rules/test/stryker_config/data/stryker_config/docs/stryker.config.baseline.md +31 -0
  105. package/rules/test/stryker_config/data/stryker_config/docs/stryker.config.vue.baseline.md +35 -0
  106. package/rules/test/stryker_config/data/stryker_config/stryker-vue-macros-ignorer.mjs +48 -0
  107. package/rules/test/stryker_config/data/stryker_config/stryker.config.baseline.mjs +18 -0
  108. package/rules/test/stryker_config/data/stryker_config/stryker.config.vue.baseline.mjs +23 -0
  109. package/rules/test/stryker_config/data/vitest_config/docs/index.md +11 -0
  110. package/rules/test/stryker_config/data/vitest_config/docs/vitest.config.baseline.md +35 -0
  111. package/rules/test/stryker_config/data/vitest_config/vitest.config.baseline.js +22 -0
  112. package/rules/test/stryker_config/docs/fix-stryker_config.md +41 -0
  113. package/rules/test/stryker_config/docs/index.md +12 -0
  114. package/rules/test/stryker_config/docs/main.md +38 -0
  115. package/rules/test/stryker_config/fix-stryker_config.mjs +77 -0
  116. package/rules/test/stryker_config/main.mjs +504 -0
  117. package/rules/test/stryker_config/stryker_config.mdc +26 -0
  118. package/rules/test/vitest-api-conventions/concern.json +7 -0
  119. package/rules/test/vitest-api-conventions/docs/index.md +9 -0
  120. package/rules/test/vitest-api-conventions/docs/main.md +40 -0
  121. package/rules/test/vitest-api-conventions/main.mjs +153 -0
  122. package/rules/test/vitest-api-conventions/vitest-api-conventions.mdc +129 -0
  123. package/rules/test/vitest-config-pool-forks/concern.json +7 -0
  124. package/rules/test/vitest-config-pool-forks/docs/index.md +11 -0
  125. package/rules/test/vitest-config-pool-forks/docs/main.md +42 -0
  126. package/rules/test/vitest-config-pool-forks/main.mjs +41 -0
  127. package/rules/test/vitest-config-pool-forks/vitest-config-pool-forks.mdc +34 -0
@@ -0,0 +1,44 @@
1
+ ---
2
+ type: JS Module
3
+ title: main.mjs
4
+ resource: plugins/lang-js/rules/test/no-console-store-restore/main.mjs
5
+ docgen:
6
+ crc: 747e7e48
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ issues: judge:inaccurate:0.99
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ Перевіряє відповідність тестової логіки стандарту, забобонячи пряме присвоєння методів об'єкта `console` у JS-тестах. Забезпечує використання рекомендованого механізму мокування (`vi.spyOn.mockReturnValue`) для збереження цілісності тестового середовища.
16
+
17
+ Поведінка:
18
+ main запускає перевірку вказаного кореня репозиторію.
19
+ Збирається список усіх файлів, що відповідають шаблону JS-тестів.
20
+ Для кожного знайденого тестового файлу зчитується його вміст.
21
+ Перевіряється наявність прямого присвоєння методів об'єкта `console` (наприклад, `console.log = ...`).
22
+ Якщо порушень не знайдено, виводиться успішне повідомлення згідно з (test.mdc).
23
+ У разі виявлення порушення (пряме присвоєння), фіксується місце інциденту (файл та рядок) і генерується повідомлення про помилку, яке рекомендує замінити присвоєння на `vi.spyOn.mockReturnValue`, відповідно до (test.mdc, no-console-store-restore).
24
+ Виводиться відповідний код завершення.
25
+
26
+ ## Поведінка
27
+
28
+ Поведінка:
29
+
30
+ 1. main викликається для запуску перевірки вказаного кореня репозиторію.
31
+ 2. Збирається список усіх файлів, що відповідають шаблону JS-тестів.
32
+ 3. Для кожного знайденого тестового файлу зчитується його вміст.
33
+ 4. Перевіряється, чи містить вміст пряме присвоєння методів об'єкта `console` (наприклад, `console.log = ...`).
34
+ 5. Якщо жодного такого присвоєння не знайдено у жодному тестовому файлі, система повідомляє про успіх згідно з (test.mdc).
35
+ 6. Якщо знайдено порушення (пряме присвоєння), система фіксує місце порушення (файл та рядок) і повідомляє про помилку, рекомендуючи замість цього використовувати `vi.spyOn.mockReturnValue` згідно з (test.mdc, no-console-store-restore).
36
+ 7. Виводиться відповідний код завершення.
37
+
38
+ ## Публічний API
39
+
40
+ main — перевіряє, що файли тесту не перевизначають консольні методи через пряме присвоєння, вимагаючи використання `vi.spyOn` для мокування.
41
+
42
+ ## Гарантії поведінки
43
+
44
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -0,0 +1,55 @@
1
+ /** @see ./docs/no-console-store-restore.md */
2
+ import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
3
+ import { collectTestFileOffenders } from '../lib/collect-test-file-offenders.mjs'
4
+
5
+ /**
6
+ * Ловить пряме присвоєння `console.<method> = …` у `*.test.{js,mjs}`.
7
+ * `console.log = fn` — process-wide мутація; канон: `vi.spyOn(console, 'log')`.
8
+ * `(?!=)` виключає `==` та `===` (лише одиночний `=`).
9
+ */
10
+ const CONSOLE_ASSIGN_RE =
11
+ /\bconsole\.(?:log|error|warn|info|debug|dir|table|trace|group|groupEnd|time|timeEnd)\s*=(?!=)/u
12
+
13
+ /**
14
+ * Знаходить рядки з прямим присвоєнням `console.<method> = …`.
15
+ * @param {string} body вміст файлу
16
+ * @returns {Array<{line: number}>} знайдені порушення
17
+ */
18
+ function findOffenders(body) {
19
+ const offenders = []
20
+ const lines = body.split('\n')
21
+ for (const [i, line] of lines.entries()) {
22
+ if (CONSOLE_ASSIGN_RE.test(line)) {
23
+ offenders.push({ line: i + 1 })
24
+ }
25
+ }
26
+ return offenders
27
+ }
28
+
29
+ /**
30
+ * Перевіряє, що жоден `*.test.{mjs,js}` файл не перевизначає `console.<method>`
31
+ * через пряме присвоєння. Канон — `vi.spyOn(console, 'log').mockReturnValue()`.
32
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту.
33
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат перевірки з порушеннями.
34
+ */
35
+ export async function lint(ctx) {
36
+ const reporter = createViolationReporter(ctx)
37
+ const { pass, fail } = reporter
38
+
39
+ const cwd = ctx.cwd
40
+ const { testFiles, offenders } = await collectTestFileOffenders(cwd, findOffenders)
41
+
42
+ if (offenders.length === 0) {
43
+ pass(`Жоден з ${testFiles.length} тестових файлів не присвоює console.<method> = … (test.mdc)`)
44
+ return reporter.result()
45
+ }
46
+
47
+ for (const { file, line } of offenders) {
48
+ fail(
49
+ `${file}:${line}: пряме присвоєння console.<method> = … заборонено — ` +
50
+ `використовуй vi.spyOn(console, 'method').mockReturnValue() (test.mdc, no-console-store-restore)`
51
+ )
52
+ }
53
+
54
+ return reporter.result()
55
+ }
@@ -0,0 +1,11 @@
1
+ ## Заборона ручного store/restore `console` у тестах
2
+
3
+ `console.log` / `console.error` / `console.warn` — **process-wide** мутації, рівно як `process.cwd()`. Якщо тест перехоплює їх через `const orig = console.log; console.log = (...) => …; try { … } finally { console.log = orig }`, паралельний test файл у `pool: 'forks'` (різний процес) ізольований, але в межах **одного** процесу всі тести у файлі ділять єдиний `console`-об'єкт. Якщо try/finally не виконається (наприклад, асинхронна помилка повз `await`), restore не спрацює і наступні тести втрачають вивід.
4
+
5
+ Тому:
6
+
7
+ - **Канон**: `vi.spyOn(console, 'log').mockReturnValue()` (та аналоги для `error`/`warn`/`info`) + `afterEach(() => vi.restoreAllMocks())`. Vitest сам слідкує за scope mock-у і гарантовано відновлює оригінал між тестами, навіть якщо тест кинув виняток.
8
+ - **Заборонено**: ручний store/restore (`const orig = console.log; console.log = stub`). Виняток — коли тест **необхідно** перехопити вивід **до** того, як завантажиться модуль із top-level-логуванням; у цьому випадку фіксуй pattern explicit-коментарем.
9
+ - Якщо потрібен лог зі stub-ом — `const logs = []; vi.spyOn(console, 'log').mockImplementation((...args) => logs.push(args.join(' ')))`.
10
+
11
+ **Перевірка** — концерн `no-console-store-restore` (`rules/test/js/no-console-store-restore.mjs`): AST-сканер, який ловить присвоєння `console.<method> = …` у `*.test.{js,mjs}`.
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "lint": {
4
+ "scope": "full",
5
+ "glob": ["**/*.test.mjs", "**/*.test.js"]
6
+ }
7
+ }
@@ -0,0 +1,11 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/test/no-process-chdir
4
+ resource: plugins/lang-js/rules/test/no-process-chdir/
5
+ ---
6
+
7
+ # npm/rules/test/no-process-chdir
8
+
9
+ | Файл | Тип |
10
+ | ------------------- | --------- |
11
+ | [main.mjs](main.md) | JS Module |
@@ -0,0 +1,32 @@
1
+ ---
2
+ type: JS Module
3
+ title: main.mjs
4
+ resource: plugins/lang-js/rules/test/no-process-chdir/main.mjs
5
+ docgen:
6
+ crc: 66ca3322
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ issues: judge:inaccurate:0.99
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ Поведінка програми полягає у виконанні основної логіки, визначеної в функції `main`. Програма надає функціональність, що не передбачає взаємодії з файловою системою або базами даних. Для верифікації коректності роботи логіки необхідно застосувати тести, які відносяться до `test.mdc`.
16
+
17
+ ## Поведінка
18
+
19
+ 1. Викликається `main` для початку перевірки.
20
+ 2. Програма шукає всі файли, що відповідають шаблону JS-тесту.
21
+ 3. Для кожного тестового файлу здійснюється пошук виклику `process.chdir`.
22
+ 4. Якщо виклик знайдено у тестовому файлі, фіксується його місцезнаходження.
23
+ 5. Якщо жоден виклик не знайдено, виводиться повідомлення про успішне виконання перевірки (test.mdc).
24
+ 6. Якщо знайдено виклики, для кожного інциденту виводиться повідомлення про порушення (test.mdc), з обов'язковою згадкою про рекомендацію використання `withTmpDir + явні join + cwd: dir`.
25
+
26
+ ## Публічний API
27
+
28
+ main — Забороняє зміну робочої директорії (`process.chdir`) у тестових файлах.
29
+
30
+ ## Гарантії поведінки
31
+
32
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -0,0 +1,40 @@
1
+ /** @see ./docs/no-process-chdir.md */
2
+ import { readFile } from 'node:fs/promises'
3
+
4
+ import { collectTestFiles, toRelPosix } from '@7n/rules/scripts/lib/collect-test-files.mjs'
5
+
6
+ /** Шукаємо викликний паттерн з відкривною дужкою — не зачепить згадку у docstring. */
7
+ const CHDIR_CALL_RE = /process\.chdir\s*\(/u
8
+
9
+ /**
10
+ * Detector: жоден `*.test.{mjs,js}` не викликає `process.chdir(` (test.mdc).
11
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx Контекст лінту (`cwd` тощо).
12
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} Результат лінту зі списком violations.
13
+ */
14
+ export async function lint(ctx) {
15
+ const { cwd } = ctx
16
+ const testFiles = await collectTestFiles(cwd)
17
+
18
+ /** @type {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintViolation[]} */
19
+ const violations = []
20
+ for (const absPath of testFiles) {
21
+ const body = await readFile(absPath, 'utf8')
22
+ if (!CHDIR_CALL_RE.test(body)) continue
23
+ const file = toRelPosix(cwd, absPath)
24
+ for (const [i, line] of body.split('\n').entries()) {
25
+ if (!CHDIR_CALL_RE.test(line)) continue
26
+ violations.push(
27
+ /** @type {Partial<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintViolation>} */ ({
28
+ reason: 'process-chdir-in-test',
29
+ message:
30
+ `${file}:${i + 1}: process.chdir() у тесті заборонений — використовуй ` +
31
+ 'withTmpDir(async dir => …) + явні join(dir, …) + cwd: dir (test.mdc)',
32
+ file,
33
+ data: { line: i + 1 }
34
+ })
35
+ )
36
+ }
37
+ }
38
+
39
+ return { violations }
40
+ }
@@ -0,0 +1,15 @@
1
+ ## Заборона `process.chdir` у тестах
2
+
3
+ `process.chdir(dir)` — **process-wide** мутація. Vitest за замовчуванням ставить `pool: 'threads'`, і всі workers ділять один процес: паралельний test file може перехопити cwd сусіда посеред FS- або `git`-операції. У реальному інциденті це призвело до того, що `git init`+`git commit` із tmp-фікстури потрапив у реальний робочий репозиторій і створив rogue commit з автором `test <test@test>`, знищивши `npm/package.json` / `CHANGELOG.md`.
4
+
5
+ Тому:
6
+
7
+ - **У тестах заборонено** `process.chdir(...)` напряму та будь-які хелпери, що його викликають (історичний `withTmpCwd` видалений у `1.28.0`).
8
+ - Канон: `withTmpDir(async dir => { ... })` зі `scripts/utils/test-helpers.mjs` — створює tmpdir і передає **абсолютний** `dir` у callback **без** `process.chdir`.
9
+ - Усі FS-операції у тесті — через `join(dir, …)` і `writeJson(join(dir, …), …)` / `ensureDir(join(dir, …))` (хелпери валідують `isAbsolute`).
10
+ - Усі child-процеси — `execFile(bin, args, { cwd: dir })`, `spawnSync(bin, args, { cwd: dir })`.
11
+ - Concern-функції правил — `await check(dir)`, `await applies(dir)`, `await fix(dir)`; усі production функції приймають перший параметр `cwd = process.cwd()` (default зберігає CLI-сумісність).
12
+
13
+ **Перевірка** (`rules/test/js/no-process-chdir.mjs`): сканує `**/*.test.{js,mjs}` і падає з ❌ на будь-яке вживання `process.chdir(`.
14
+
15
+ **Кожен виклик `execFile`/`execFileSync`/`spawn`/`spawnSync`** у `*.test.{js,mjs}` приймає або **явний `{ cwd: dir }`** (де `dir` — змінна, не `process.cwd()`), або працює з **абсолютними шляхами** до бінарників/fixture-файлів. Це гарантує, що мутаційний test-flow у `pool: 'forks'` не страждає від implicit-`process.cwd()`-stride між тестами в одному воркері.
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "lint": {
4
+ "scope": "full",
5
+ "glob": ["**/*.test.mjs", "**/*.test.js"]
6
+ }
7
+ }
@@ -0,0 +1,11 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/test/no-relative-fs-path
4
+ resource: plugins/lang-js/rules/test/no-relative-fs-path/
5
+ ---
6
+
7
+ # npm/rules/test/no-relative-fs-path
8
+
9
+ | Файл | Тип |
10
+ | ------------------- | --------- |
11
+ | [main.mjs](main.md) | JS Module |
@@ -0,0 +1,34 @@
1
+ ---
2
+ type: JS Module
3
+ title: main.mjs
4
+ resource: plugins/lang-js/rules/test/no-relative-fs-path/main.mjs
5
+ docgen:
6
+ crc: b8f4b7aa
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 95
9
+ issues: anchor-miss:(test.mdc),judge:inaccurate:0.99
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ main функція забезпечує виконання основного логічного блоку програми. Вона відповідає за ініціалізацію системи та керує її життєвим циклом. Кешування даних здійснюється протягом одного виконання для підвищення продуктивності за рахунок уникнення повторних обчислень. Поведінка системи включає інтеграцію маркерів повідомлень відповідно до правил `test.mdc`.
16
+
17
+ ## Поведінка
18
+
19
+ 1. Викликати `main` для запуску перевірки.
20
+ 2. Система збирає список усіх файлів, що є тестовими (завершуються на `*.test.mjs` або `*.test.js`) у директорії проєкту та її підкаталогах.
21
+ 3. Для кожного знайденого тестового файлу система зчитує його вміст.
22
+ 4. Система аналізує вміст кожного тестового файлу, шукаючи виклики функцій з модулю `node:fs` або `node:fs/promises`, які приймають шляхи.
23
+ 5. При виявленні такого виклику система перевіряє, чи використовується як літеральний шлях (string literal або template literal без виразів) відносний шлях.
24
+ 6. Якщо відносний шлях знайдено, система фіксує місце порушення: ім'я файлу, номер рядка, назву функції та позицію аргументу.
25
+ 7. Якщо порушень не знайдено у жодному тестовому файлі, виконується фіксація успішного завершення перевірки.
26
+ 8. Якщо порушення знайдено, для кожного знайденого випадку система фіксує помилку, вказуючи, що використовується відносний шлях і що слід замінити його на обчислення за допомогою `join`.
27
+
28
+ ## Публічний API
29
+
30
+ main — Гарантує, що тестові файли не використовують відносні шляхи як перший аргумент при виклику файлових операцій з `node:fs` або `node:fs/promises`.
31
+
32
+ ## Гарантії поведінки
33
+
34
+ - Кешує результати в межах одного прогону.
@@ -0,0 +1,239 @@
1
+ /** @see ./docs/no-relative-fs-path.md */
2
+ import { readFile } from 'node:fs/promises'
3
+ import { basename, relative } from 'node:path'
4
+
5
+ import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
6
+ import { loadCursorIgnorePaths } from '@7n/rules/scripts/lib/load-cursor-config.mjs'
7
+ import { parseProgramOrNull, walkAstWithAncestors } from '@7n/rules/scripts/utils/ast-scan-utils.mjs'
8
+ import { walkDir } from '@7n/rules/scripts/utils/walkDir.mjs'
9
+
10
+ /**
11
+ * FS-функції з `node:fs` / `node:fs/promises` / sync-API, які приймають path
12
+ * у фіксованих позиціях. Map: ім'я функції → масив 0-індексованих позицій
13
+ * path-аргументів (1-й, 2-й, або обидва — як у `copyFile/rename/symlink/link`).
14
+ */
15
+ const FS_PATH_ARG_POSITIONS = new Map([
16
+ ['writeFile', [0]],
17
+ ['writeFileSync', [0]],
18
+ ['readFile', [0]],
19
+ ['readFileSync', [0]],
20
+ ['appendFile', [0]],
21
+ ['appendFileSync', [0]],
22
+ ['mkdir', [0]],
23
+ ['mkdirSync', [0]],
24
+ ['rmdir', [0]],
25
+ ['rmdirSync', [0]],
26
+ ['rm', [0]],
27
+ ['rmSync', [0]],
28
+ ['unlink', [0]],
29
+ ['unlinkSync', [0]],
30
+ ['access', [0]],
31
+ ['accessSync', [0]],
32
+ ['stat', [0]],
33
+ ['statSync', [0]],
34
+ ['lstat', [0]],
35
+ ['lstatSync', [0]],
36
+ ['chmod', [0]],
37
+ ['chmodSync', [0]],
38
+ ['chown', [0]],
39
+ ['chownSync', [0]],
40
+ ['truncate', [0]],
41
+ ['truncateSync', [0]],
42
+ ['existsSync', [0]],
43
+ ['readdir', [0]],
44
+ ['readdirSync', [0]],
45
+ ['copyFile', [0, 1]],
46
+ ['copyFileSync', [0, 1]],
47
+ ['rename', [0, 1]],
48
+ ['renameSync', [0, 1]],
49
+ ['symlink', [0, 1]],
50
+ ['symlinkSync', [0, 1]],
51
+ ['link', [0, 1]],
52
+ ['linkSync', [0, 1]],
53
+ ['cp', [0, 1]],
54
+ ['cpSync', [0, 1]],
55
+ // test-helpers абсолютні-only форми (зайвий захист)
56
+ ['writeJson', [0]],
57
+ ['ensureDir', [0]]
58
+ ])
59
+
60
+ /**
61
+ * Префікси абсолютних шляхів або очевидно-обчислених. Якщо literal починається з
62
+ * одного з них — це OK (тест свідомо передає absolute чи URL).
63
+ */
64
+ const ABSOLUTE_PREFIXES = ['/', '\\', 'file:', 'http:', 'https:', 'data:']
65
+ const WINDOWS_DRIVE_RE = /^[A-Za-z]:[\\/]/u
66
+
67
+ /**
68
+ * Чи string literal — relative path (тобто баг). Перевіряє лише string-літерали
69
+ * та template literals без виразів. Виклики `join(...)` / `resolve(...)` /
70
+ * перемінні з ${} — пропускаємо (припускаємо absolute).
71
+ * @param {object} arg AST node аргументу
72
+ * @returns {string|null} relative-path значення (для меседжа), або null якщо OK
73
+ */
74
+ function extractRelativeLiteralPath(arg) {
75
+ if (!arg) return null
76
+ if (arg.type === 'Literal' && typeof arg.value === 'string') {
77
+ return isRelativeString(arg.value) ? arg.value : null
78
+ }
79
+ if (arg.type === 'TemplateLiteral' && arg.expressions.length === 0) {
80
+ const raw = arg.quasis.map(q => q.value.cooked).join('')
81
+ return isRelativeString(raw) ? raw : null
82
+ }
83
+ // Не string-literal — не аналізуємо (припускаємо обчислений absolute через join/resolve).
84
+ return null
85
+ }
86
+
87
+ /**
88
+ * Чи рядок виглядає як relative path. Порожній рядок — false (це не path).
89
+ * Windows-disk-letter `C:\…` — absolute, бо містить `:` між літерою і `\`.
90
+ * @param {string} s рядок-шлях
91
+ * @returns {boolean} true якщо relative
92
+ */
93
+ function isRelativeString(s) {
94
+ if (!s) return false
95
+ for (const prefix of ABSOLUTE_PREFIXES) {
96
+ if (s.startsWith(prefix)) return false
97
+ }
98
+ // Windows drive letter, наприклад `C:\foo` або `C:/foo`.
99
+ return !WINDOWS_DRIVE_RE.test(s)
100
+ }
101
+
102
+ /**
103
+ * Витягує ім'я FS-функції з callee:
104
+ * - `writeFile(…)` → "writeFile" (Identifier callee)
105
+ * - `fs.writeFile(…)` чи `fsp.writeFile(…)` → "writeFile" (MemberExpression)
106
+ * - `await fs.promises.writeFile(…)` → "writeFile"
107
+ * Повертає null для будь-якого іншого виклику.
108
+ * @param {object} callee AST callee node
109
+ * @returns {string|null} ім'я FS-функції або null
110
+ */
111
+ function extractFsFunctionName(callee) {
112
+ if (!callee) return null
113
+ if (callee.type === 'Identifier') {
114
+ return FS_PATH_ARG_POSITIONS.has(callee.name) ? callee.name : null
115
+ }
116
+ if (callee.type === 'MemberExpression' && !callee.computed && callee.property?.type === 'Identifier') {
117
+ const name = callee.property.name
118
+ return FS_PATH_ARG_POSITIONS.has(name) ? name : null
119
+ }
120
+ return null
121
+ }
122
+
123
+ /**
124
+ * Чи файл — JS-тест (`*.test.mjs` / `*.test.js`).
125
+ * @param {string} absPath абсолютний шлях
126
+ * @returns {boolean} true якщо файл є тестом
127
+ */
128
+ function isTestFile(absPath) {
129
+ const name = basename(absPath)
130
+ return name.endsWith('.test.mjs') || name.endsWith('.test.js')
131
+ }
132
+
133
+ /**
134
+ * Знаходить порушення у одному тестовому файлі.
135
+ * @param {string} body вміст тесту
136
+ * @returns {Array<{line: number, fn: string, path: string, argPos: number}>} порушення
137
+ */
138
+ function findOffendersInBody(body) {
139
+ const program = parseProgramOrNull(body, 'test.mjs')
140
+ if (!program) return []
141
+ const offenders = []
142
+ const lineOffsets = computeLineOffsets(body)
143
+ walkAstWithAncestors(program, [], node => {
144
+ if (node?.type !== 'CallExpression') return
145
+ const fnName = extractFsFunctionName(node.callee)
146
+ if (!fnName) return
147
+ const positions = FS_PATH_ARG_POSITIONS.get(fnName)
148
+ for (const pos of positions) {
149
+ const arg = node.arguments?.[pos]
150
+ const relPath = extractRelativeLiteralPath(arg)
151
+ if (relPath !== null) {
152
+ const start = arg?.start ?? node.start ?? 0
153
+ const line = offsetToLineFromCache(lineOffsets, start)
154
+ offenders.push({ line, fn: fnName, path: relPath, argPos: pos })
155
+ }
156
+ }
157
+ })
158
+ return offenders
159
+ }
160
+
161
+ /**
162
+ * Кешований offset→line: бінарний пошук по newline-offsets.
163
+ * @param {string} body source
164
+ * @returns {number[]} offsets newline char positions
165
+ */
166
+ function computeLineOffsets(body) {
167
+ const offsets = [0]
168
+ let pos = 0
169
+ for (const ch of body) {
170
+ if (ch === '\n') offsets.push(pos + 1)
171
+ pos += 1
172
+ }
173
+ return offsets
174
+ }
175
+
176
+ /**
177
+ * @param {number[]} offsets newline-offsets
178
+ * @param {number} offset 0-індекс символу
179
+ * @returns {number} 1-індексований рядок
180
+ */
181
+ function offsetToLineFromCache(offsets, offset) {
182
+ let lo = 0
183
+ let hi = offsets.length - 1
184
+ while (lo < hi) {
185
+ const mid = Math.floor((lo + hi + 1) / 2)
186
+ if (offsets[mid] <= offset) lo = mid
187
+ else hi = mid - 1
188
+ }
189
+ return lo + 1
190
+ }
191
+
192
+ /**
193
+ * Перевіряє, що жоден `*.test.{mjs,js}` файл не передає relative-path як 1-й
194
+ * (або для `copyFile`/`rename`/`symlink`/`link`/`cp` — 1-й і 2-й) аргумент
195
+ * у FS-функцію з `node:fs` / `node:fs/promises`.
196
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx Контекст лінту (cwd, перелік файлів тощо).
197
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} Результат лінту з переліком порушень.
198
+ */
199
+ export async function lint(ctx) {
200
+ const reporter = createViolationReporter(ctx)
201
+ const { pass, fail } = reporter
202
+
203
+ const cwd = ctx.cwd
204
+ const ignorePaths = await loadCursorIgnorePaths(cwd)
205
+
206
+ /** @type {string[]} */
207
+ const testFiles = []
208
+ await walkDir(
209
+ cwd,
210
+ absPath => {
211
+ if (isTestFile(absPath)) testFiles.push(absPath)
212
+ },
213
+ ignorePaths
214
+ )
215
+
216
+ /** @type {Array<{file: string, line: number, fn: string, path: string, argPos: number}>} */
217
+ const offenders = []
218
+ for (const absPath of testFiles) {
219
+ const body = await readFile(absPath, 'utf8')
220
+ const found = findOffendersInBody(body)
221
+ for (const o of found) {
222
+ offenders.push({ file: relative(cwd, absPath), ...o })
223
+ }
224
+ }
225
+
226
+ if (offenders.length === 0) {
227
+ pass(`Жоден з ${testFiles.length} тестових файлів не передає relative-path у FS-функції (test.mdc)`)
228
+ return reporter.result()
229
+ }
230
+
231
+ for (const { file, line, fn, path, argPos } of offenders) {
232
+ const which = argPos === 0 ? '1-й аргумент' : `${argPos + 1}-й аргумент`
233
+ fail(
234
+ `${file}:${line}: ${fn}() — ${which} '${path}' relative; використовуй join(dir, …) (test.mdc, no-relative-fs-path)`
235
+ )
236
+ }
237
+
238
+ return reporter.result()
239
+ }
@@ -0,0 +1,22 @@
1
+ ## Заборона відносних шляхів у FS-функціях тестів
2
+
3
+ **`no-relative-fs-path`** (`rules/test/js/no-relative-fs-path.mjs`) — AST-сканер (`oxc-parser`): знаходить виклики FS-функцій із `node:fs`/`node:fs/promises` (`writeFile`, `copyFile`, `mkdir`, `readFile`, `existsSync`, `rename`, `symlink`, `cp`, … включно з `*Sync`-варіантами та `writeJson`/`ensureDir`-хелперами), де path-аргумент — це **string literal** без префікса `/`, `\`, `file:`, `http(s):`, `data:`, чи Windows-disk-letter `C:\`.
4
+
5
+ Виклики `copyFile`/`rename`/`symlink`/`link`/`cp` перевіряють **обидва** path-аргументи.
6
+
7
+ Виклики з обчисленим path (`join(dir, …)`, змінна, template-literal з виразом) пропускаються — припускається, що обчислений шлях абсолютний.
8
+
9
+ **Чому важливо:** відносний path у FS-виклику означає запис/читання відносно `process.cwd()` на момент виклику. У `pool: 'threads'` (дефолт Vitest) паралельні файли ділять процес — cwd може змінитися між тестами. Інцидент `v1.28.0`: `copyFile(src, 'default.conf.template')` у `tests/check-rule-fixtures.test.mjs` записав файл у production tree замість sandbox.
10
+
11
+ **Канон:** замінювати відносні шляхи на `join(dir, 'filename')`, де `dir` — абсолютний tmpdir із `withTmpDir`.
12
+
13
+ **Список FS-функцій, що перевіряються:**
14
+
15
+ | Функція | Перевірювані аргументи |
16
+ |---|---|
17
+ | `writeFile`, `readFile`, `appendFile`, `mkdir`, `rmdir`, `rm`, `unlink` | 1-й |
18
+ | `access`, `stat`, `lstat`, `chmod`, `chown`, `truncate`, `existsSync`, `readdir` | 1-й |
19
+ | `copyFile`, `rename`, `symlink`, `link`, `cp` | 1-й і 2-й |
20
+ | `writeJson`, `ensureDir` (test-helpers) | 1-й |
21
+
22
+ Усі `*Sync`-варіанти охоплені аналогічно.
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "fixability": "config",
4
+ "policy": {
5
+ "files": {
6
+ "single": "package.json",
7
+ "required": true
8
+ },
9
+ "missingMessage": "package.json не існує — створи зі scripts.coverage (test.mdc)"
10
+ }
11
+ }
@@ -0,0 +1,18 @@
1
+ ## Обовʼязкові скрипти в package.json
2
+
3
+ Rego-пакет: `test.package_json`
4
+
5
+ Цільовий файл: `package.json` кожного workspace.
6
+
7
+ Перевіряє substring-відповідність значень у `scripts`:
8
+
9
+ | Поле | Має містити |
10
+ |---|---|
11
+ | `scripts.coverage` | `@7n/test coverage` |
12
+ | `scripts.test` | `vitest` і `--bun` (напр. `"bun run --bun vitest run"`) |
13
+
14
+ Substring-семантика: команди-обгортки (наприклад `bun run pre-coverage && npx @7n/test coverage`, `bun run pre-test && bun run --bun vitest run`) дозволені — головне, щоб потрібні рядки були присутні.
15
+
16
+ **Чому `--bun` для `scripts.test`:** без Bun-рушія (`bun run vitest` без `--bun`, чи vitest під Node) `import { SQL } from 'bun'` та інші Bun-нативні built-in модулі не резолвуються у forked test-процесах — консьюмери змушені тримати exclude-списки для тестів, що їх торкаються. Під `bun run --bun vitest run` forked pool-процеси успадковують Bun-рушій і резолюція працює без жодних exclude.
17
+
18
+ Канон підтягується через `--data`: [package.json.contains.json](./template/package.json.contains.json)
@@ -0,0 +1,25 @@
1
+ # Перевірка `package.json` для правила test (test.mdc).
2
+ #
3
+ # Канон надходить через --data: { "template": { "contains": ... } }
4
+ # Структура --data сформована з template/package.json.contains.json.
5
+ # Перевіряємо substring-вимоги до scripts.coverage і scripts.test:
6
+ # рядки мають містити відповідно "@7n/test coverage" і "vitest" + "--bun".
7
+ package test.package_json
8
+
9
+ import rego.v1
10
+
11
+ deny contains msg if {
12
+ some script_name, needles in data.template.contains.scripts
13
+ actual := object.get(object.get(input, "scripts", {}), script_name, "")
14
+ some needle in needles
15
+ not contains(actual, needle)
16
+ msg := sprintf("package.json: scripts.%s має містити %q (test.mdc)%s", [script_name, needle, explain_suffix(needle)])
17
+ }
18
+
19
+ # --bun пояснюється окремо: без нього forked vitest pool-процеси не успадковують
20
+ # Bun-рушій, і Bun-нативні built-in модулі (напр. `import { SQL } from 'bun'`)
21
+ # не резолвуються у forked test-процесах.
22
+ explain_suffix(needle) := msg if {
23
+ needle == "--bun"
24
+ msg := " — без --bun forked vitest pool-процеси не успадковують Bun-рушій, тож Bun-нативні built-in модулі (напр. import { SQL } from 'bun') не резолвуються у forked test-процесах"
25
+ } else := ""
@@ -0,0 +1,6 @@
1
+ {
2
+ "scripts": {
3
+ "coverage": ["@7n/test coverage"],
4
+ "test": ["vitest", "--bun"]
5
+ }
6
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "lint": {
4
+ "scope": "full",
5
+ "glob": ["**/*.test.mjs", "**/*.test.js"]
6
+ }
7
+ }
@@ -0,0 +1,11 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/test/sandbox-aware-test
4
+ resource: plugins/lang-js/rules/test/sandbox-aware-test/
5
+ ---
6
+
7
+ # npm/rules/test/sandbox-aware-test
8
+
9
+ | Файл | Тип |
10
+ | ------------------- | --------- |
11
+ | [main.mjs](main.md) | JS Module |