@7n/rules 1.20.0 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/rules/bun/licensee/docs/fix-licensee.md +1 -2
- package/rules/bun/licensee/docs/main.md +1 -1
- package/rules/bun/licensee/fix-licensee.mjs +3 -4
- package/rules/bun/licensee/main.mjs +7 -7
- package/rules/docker/lib/docker-hadolint.mjs +6 -8
- package/rules/docker/lib/docs/docker-hadolint.md +1 -1
- package/rules/docker/lint/docs/main.md +1 -1
- package/rules/docker/lint/main.mjs +1 -1
- package/rules/image-compress/check/docs/fix-check.md +1 -2
- package/rules/image-compress/check/docs/main.md +1 -1
- package/rules/image-compress/check/fix-check.mjs +27 -22
- package/rules/image-compress/check/main.mjs +14 -14
- package/rules/js/jscpd_duplicates/docs/main.md +1 -1
- package/rules/js/jscpd_duplicates/main.mjs +7 -8
- package/rules/k8s/kubeconform/docs/main.md +1 -1
- package/rules/k8s/kubeconform/main.mjs +1 -1
- package/rules/k8s/manifests/main.mjs +52 -39
- package/rules/php/cs_fixer/docs/main.md +1 -2
- package/rules/php/cs_fixer/main.mjs +8 -7
- package/rules/php/phpcs/docs/main.md +1 -2
- package/rules/php/phpcs/main.mjs +12 -11
- package/rules/php/project/docs/main.md +1 -2
- package/rules/php/project/main.mjs +19 -16
- package/rules/python/lib/docs/uv-run.md +1 -2
- package/rules/python/lib/uv-run.mjs +12 -8
- package/rules/python/mypy/docs/main.md +1 -1
- package/rules/python/mypy/main.mjs +9 -8
- package/rules/python/project/docs/main.md +1 -2
- package/rules/python/project/main.mjs +33 -23
- package/rules/python/ruff/docs/fix-ruff.md +1 -2
- package/rules/python/ruff/docs/main.md +1 -1
- package/rules/python/ruff/fix-ruff.mjs +12 -10
- package/rules/python/ruff/main.mjs +13 -12
- package/rules/rego/conftest_verify/docs/main.md +1 -2
- package/rules/rego/conftest_verify/main.mjs +12 -10
- package/rules/rego/lib/docs/run-external-tool.md +1 -1
- package/rules/rego/lib/run-external-tool.mjs +10 -7
- package/rules/rego/opa_check/docs/main.md +1 -1
- package/rules/rego/opa_check/main.mjs +3 -3
- package/rules/rego/regal/docs/main.md +1 -1
- package/rules/rego/regal/main.mjs +3 -3
- package/rules/rust/check/docs/fix-check.md +1 -1
- package/rules/rust/check/docs/main.md +1 -1
- package/rules/rust/check/fix-check.mjs +13 -12
- package/rules/rust/check/main.mjs +19 -13
- package/rules/security/scan/docs/main.md +1 -1
- package/rules/security/scan/main.mjs +9 -9
- package/rules/style/lint/docs/fix-lint.md +1 -1
- package/rules/style/lint/docs/main.md +1 -1
- package/rules/style/lint/fix-lint.mjs +8 -8
- package/rules/style/lint/main.mjs +6 -6
- package/rules/text/cspell-fix/docs/fix-worker.md +1 -1
- package/rules/text/cspell-fix/docs/main.md +1 -1
- package/rules/text/cspell-fix/fix-worker.mjs +1 -1
- package/rules/text/cspell-fix/main.mjs +14 -15
- package/rules/text/oxfmt/docs/fix-oxfmt.md +1 -1
- package/rules/text/oxfmt/docs/main.md +1 -1
- package/rules/text/oxfmt/fix-oxfmt.mjs +3 -3
- package/rules/text/oxfmt/main.mjs +6 -5
- package/rules/text/run-dotenv-linter/docs/fix-run-dotenv-linter.md +1 -2
- package/rules/text/run-dotenv-linter/docs/main.md +1 -1
- package/rules/text/run-dotenv-linter/fix-run-dotenv-linter.mjs +2 -2
- package/rules/text/run-dotenv-linter/main.mjs +19 -24
- package/rules/text/run-shellcheck/docs/fix-run-shellcheck.md +1 -2
- package/rules/text/run-shellcheck/docs/main.md +1 -1
- package/rules/text/run-shellcheck/fix-run-shellcheck.mjs +3 -3
- package/rules/text/run-shellcheck/main.mjs +46 -57
- package/rules/text/run-v8r/docs/main.md +1 -1
- package/rules/text/run-v8r/main.mjs +31 -26
- package/scripts/lib/lint-surface/blocking-inventory.mjs +12 -40
- package/scripts/lib/lint-surface/docs/blocking-inventory.md +1 -2
- package/scripts/utils/docs/spawn-async.md +1 -2
- package/scripts/utils/spawn-async.mjs +4 -1
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* (`data.kind === 'oxfmt-unformatted'`); re-detect підтверджує результат. Запис permanent.
|
|
7
7
|
* Тул відсутній у PATH → no-op.
|
|
8
8
|
*/
|
|
9
|
-
import { spawnSync } from 'node:child_process'
|
|
10
9
|
import { readFileSync } from 'node:fs'
|
|
11
10
|
import { resolve } from 'node:path'
|
|
12
11
|
|
|
13
12
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
13
|
+
import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Вміст файлу або null, якщо не читається.
|
|
@@ -30,7 +30,7 @@ export const patterns = [
|
|
|
30
30
|
{
|
|
31
31
|
id: 'text-oxfmt-write',
|
|
32
32
|
test: violations => violations.some(v => v.data?.kind === 'oxfmt-unformatted' && v.file),
|
|
33
|
-
apply: (violations, ctx) => {
|
|
33
|
+
apply: async (violations, ctx) => {
|
|
34
34
|
const oxfmt = resolveCmd('oxfmt')
|
|
35
35
|
if (!oxfmt) return { touchedFiles: [] }
|
|
36
36
|
const files = [
|
|
@@ -40,7 +40,7 @@ export const patterns = [
|
|
|
40
40
|
|
|
41
41
|
const abs = files.map(f => resolve(ctx.cwd, f))
|
|
42
42
|
const before = new Map(abs.map(a => [a, readOrNull(a)]))
|
|
43
|
-
|
|
43
|
+
await spawnAsync(oxfmt, ['--write', ...files], { cwd: ctx.cwd })
|
|
44
44
|
|
|
45
45
|
const touchedFiles = abs.filter(a => readOrNull(a) !== before.get(a))
|
|
46
46
|
for (const a of touchedFiles) ctx.recordWrite?.(a)
|
|
@@ -6,23 +6,24 @@
|
|
|
6
6
|
* `.oxfmtrc.json` (включно з `ignorePatterns`). Автофікс (`oxfmt --write`) — окремий T0
|
|
7
7
|
* `fix-oxfmt.mjs`, не в detector-і. Тул відсутній у PATH → SKIP (не violation), як інші зовнішні.
|
|
8
8
|
*/
|
|
9
|
-
import { spawnSync } from 'node:child_process'
|
|
10
9
|
import { relative, resolve } from 'node:path'
|
|
11
10
|
|
|
12
11
|
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
13
12
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
13
|
+
import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
|
|
14
14
|
|
|
15
|
-
const JSON_MAX_BUFFER = 64 * 1024 * 1024
|
|
16
15
|
const FMT_EXT_RE = /\.(?:m?[jt]s|c[jt]s|jsx|tsx|vue|css|scss)$/u
|
|
17
16
|
// Безконфліктна formatter-домена: oxfmt-типи, що їх не форматує інше тулінг (md/json/yaml — поза).
|
|
18
17
|
const FMT_GLOB = '**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts,vue,css,scss}'
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
20
|
* Detector: повертає по одному violation на кожен неформатований файл.
|
|
21
|
+
* Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
|
|
22
|
+
* (ADR 260716-1354).
|
|
22
23
|
* @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону (cwd, files)
|
|
23
|
-
* @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат із violation на кожен неформатований файл
|
|
24
|
+
* @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат із violation на кожен неформатований файл
|
|
24
25
|
*/
|
|
25
|
-
export function lint(ctx) {
|
|
26
|
+
export async function lint(ctx) {
|
|
26
27
|
const { cwd, files } = ctx
|
|
27
28
|
const reporter = createViolationReporter(ctx)
|
|
28
29
|
const oxfmt = resolveCmd('oxfmt')
|
|
@@ -31,7 +32,7 @@ export function lint(ctx) {
|
|
|
31
32
|
const targets = files === undefined ? [FMT_GLOB] : files.filter(f => FMT_EXT_RE.test(f))
|
|
32
33
|
if (targets.length === 0) return reporter.result()
|
|
33
34
|
|
|
34
|
-
const r =
|
|
35
|
+
const r = await spawnAsync(oxfmt, ['--list-different', ...targets], { cwd })
|
|
35
36
|
const unformatted = (r.stdout ?? '')
|
|
36
37
|
.split('\n')
|
|
37
38
|
.map(s => s.trim())
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: fix-run-dotenv-linter.mjs
|
|
4
4
|
resource: npm/rules/text/run-dotenv-linter/fix-run-dotenv-linter.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 4fef0c3c
|
|
7
7
|
model: openai-codex/gpt-5.5
|
|
8
|
-
tier: cloud-avg
|
|
9
8
|
score: 90
|
|
10
9
|
issues: internal-name:runDotenvLinter,judge:inaccurate:0.99
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -48,12 +48,12 @@ export const patterns = [
|
|
|
48
48
|
id: 'text-dotenv-fix',
|
|
49
49
|
standalone: true, // §8 Phase 2: dotenv-linter fix сам ре-аналізує, test() не потрібен
|
|
50
50
|
test: violations => violations.some(v => v.reason === 'dotenv-linter'),
|
|
51
|
-
apply: (violations, ctx) => {
|
|
51
|
+
apply: async (violations, ctx) => {
|
|
52
52
|
const files = listEnvFiles(ctx.cwd)
|
|
53
53
|
if (files.length === 0) return { touchedFiles: [] }
|
|
54
54
|
const abs = files.map(f => resolve(ctx.cwd, f))
|
|
55
55
|
const before = new Map(abs.map(a => [a, readOrNull(a)]))
|
|
56
|
-
runDotenvLinter(ctx.cwd, false)
|
|
56
|
+
await runDotenvLinter(ctx.cwd, false)
|
|
57
57
|
const touchedFiles = abs.filter(a => readOrNull(a) !== before.get(a))
|
|
58
58
|
for (const a of touchedFiles) ctx.recordWrite?.(a)
|
|
59
59
|
return touchedFiles.length > 0
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
* фінальний `dotenv-linter check -r --quiet . --exclude …`; будь-яке залишкове порушення — ненульовий
|
|
16
16
|
* код виходу.
|
|
17
17
|
*/
|
|
18
|
-
import { spawnSync } from 'node:child_process'
|
|
19
18
|
import { resolve } from 'node:path'
|
|
20
19
|
|
|
21
20
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
22
21
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
22
|
+
import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
|
|
23
23
|
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
24
24
|
|
|
25
25
|
/** Каталоги/файли, які виключаємо з рекурсивного сканування dotenv-linter. */
|
|
@@ -58,12 +58,14 @@ function buildTargetArgs(scopeFiles) {
|
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* Запускає dotenv-linter з авто-фіксом і фінальною перевіркою.
|
|
61
|
+
* Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
|
|
62
|
+
* (ADR 260716-1354).
|
|
61
63
|
* @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`)
|
|
62
64
|
* @param {boolean} [readOnly] true → пропустити авто-фікс (`fix`), лише `check` (нуль мутацій)
|
|
63
65
|
* @param {string[]} [scopeFiles] явний перелік `.env*`-файлів (delta) — якщо не задано, рекурсивний `-r .`
|
|
64
|
-
* @returns {number} 0 — OK; 1 — інструмент відсутній або є залишкові порушення
|
|
66
|
+
* @returns {Promise<number>} 0 — OK; 1 — інструмент відсутній або є залишкові порушення
|
|
65
67
|
*/
|
|
66
|
-
export function runDotenvLinter(cwd, readOnly, scopeFiles) {
|
|
68
|
+
export async function runDotenvLinter(cwd, readOnly, scopeFiles) {
|
|
67
69
|
const root = resolve(cwd ?? process.cwd())
|
|
68
70
|
const bin = resolveCmd('dotenv-linter')
|
|
69
71
|
if (!bin) {
|
|
@@ -73,29 +75,22 @@ export function runDotenvLinter(cwd, readOnly, scopeFiles) {
|
|
|
73
75
|
|
|
74
76
|
const targets = buildTargetArgs(scopeFiles)
|
|
75
77
|
if (!readOnly) {
|
|
76
|
-
|
|
77
|
-
cwd: root,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
81
|
-
})
|
|
82
|
-
if (fixRun.error) {
|
|
83
|
-
process.stderr.write(`${fixRun.error.message}\n`)
|
|
78
|
+
try {
|
|
79
|
+
await spawnAsync(bin, ['fix', '--no-backup', '--quiet', ...targets], { cwd: root, env: process.env })
|
|
80
|
+
} catch (error) {
|
|
81
|
+
process.stderr.write(`${error.message}\n`)
|
|
84
82
|
return 1
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
})
|
|
94
|
-
if (checkRun.error) {
|
|
95
|
-
process.stderr.write(`${checkRun.error.message}\n`)
|
|
86
|
+
let checkRun
|
|
87
|
+
try {
|
|
88
|
+
checkRun = await spawnAsync(bin, ['check', '--quiet', ...targets], { cwd: root, env: process.env })
|
|
89
|
+
} catch (error) {
|
|
90
|
+
process.stderr.write(`${error.message}\n`)
|
|
96
91
|
return 1
|
|
97
92
|
}
|
|
98
|
-
if (checkRun.
|
|
93
|
+
if (checkRun.exitCode === 0) return 0
|
|
99
94
|
if (checkRun.stdout?.length) process.stdout.write(checkRun.stdout)
|
|
100
95
|
if (checkRun.stderr?.length) process.stderr.write(checkRun.stderr)
|
|
101
96
|
return 1
|
|
@@ -104,19 +99,19 @@ export function runDotenvLinter(cwd, readOnly, scopeFiles) {
|
|
|
104
99
|
/**
|
|
105
100
|
* Detector text/run-dotenv-linter: read-only dotenv-linter по `ctx.files` (delta) або по всіх `.env*` (full).
|
|
106
101
|
* @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону
|
|
107
|
-
* @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат detector-а
|
|
102
|
+
* @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат detector-а
|
|
108
103
|
*/
|
|
109
|
-
export function lint(ctx) {
|
|
104
|
+
export async function lint(ctx) {
|
|
110
105
|
const reporter = createViolationReporter(ctx)
|
|
111
106
|
const { fail } = reporter
|
|
112
107
|
const scopeFiles = ctx.files === undefined ? undefined : ctx.files.filter(f => ENV_BASENAME_RE.test(f))
|
|
113
108
|
if (scopeFiles !== undefined && scopeFiles.length === 0) return reporter.result()
|
|
114
109
|
|
|
115
|
-
const code = runDotenvLinter(ctx.cwd, true, scopeFiles)
|
|
110
|
+
const code = await runDotenvLinter(ctx.cwd, true, scopeFiles)
|
|
116
111
|
if (code !== 0) fail('dotenv-linter знайшов порушення у .env* (text.mdc)', 'dotenv-linter')
|
|
117
112
|
return reporter.result()
|
|
118
113
|
}
|
|
119
114
|
|
|
120
115
|
if (isRunAsCli(import.meta.url)) {
|
|
121
|
-
process.exitCode = runDotenvLinter()
|
|
116
|
+
process.exitCode = await runDotenvLinter()
|
|
122
117
|
}
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: fix-run-shellcheck.mjs
|
|
4
4
|
resource: npm/rules/text/run-shellcheck/fix-run-shellcheck.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 190ecdc3
|
|
7
7
|
model: openai-codex/gpt-5.5
|
|
8
|
-
tier: cloud-avg
|
|
9
8
|
score: 90
|
|
10
9
|
issues: internal-name:runShellcheckText,judge:inaccurate:0.98
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -28,12 +28,12 @@ export const patterns = [
|
|
|
28
28
|
id: 'text-shellcheck-fix',
|
|
29
29
|
standalone: true, // §8 Phase 2: shellcheck diff+patch цикл сам ре-аналізує, test() не потрібен
|
|
30
30
|
test: violations => violations.some(v => v.reason === 'shellcheck'),
|
|
31
|
-
apply: (violations, ctx) => {
|
|
32
|
-
const files = listShellScriptPaths(ctx.cwd)
|
|
31
|
+
apply: async (violations, ctx) => {
|
|
32
|
+
const files = await listShellScriptPaths(ctx.cwd)
|
|
33
33
|
if (files.length === 0) return { touchedFiles: [] }
|
|
34
34
|
const abs = files.map(f => resolve(ctx.cwd, f))
|
|
35
35
|
const before = new Map(abs.map(a => [a, readOrNull(a)]))
|
|
36
|
-
runShellcheckText(ctx.cwd, false)
|
|
36
|
+
await runShellcheckText(ctx.cwd, false)
|
|
37
37
|
const touchedFiles = abs.filter(a => readOrNull(a) !== before.get(a))
|
|
38
38
|
for (const a of touchedFiles) ctx.recordWrite?.(a)
|
|
39
39
|
return touchedFiles.length > 0
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
* Після циклу авто-виправлень виконується звичайний `shellcheck` по всіх зібраних файлах; будь-яке
|
|
16
16
|
* попередження чи помилка — ненульовий код виходу.
|
|
17
17
|
*/
|
|
18
|
-
import { spawnSync } from 'node:child_process'
|
|
19
18
|
import { globSync } from 'node:fs'
|
|
20
19
|
import { resolve } from 'node:path'
|
|
21
20
|
|
|
22
21
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
23
22
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
23
|
+
import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
|
|
24
24
|
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
25
25
|
|
|
26
26
|
/** Розширення shell-скриптів — фільтр delta-списку файлів у `lint(ctx)`. */
|
|
@@ -65,24 +65,18 @@ function printPatchInstallHints() {
|
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Повертає відносні шляхи до shell-скриптів для перевірки.
|
|
68
|
+
* Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
|
|
69
|
+
* (ADR 260716-1354).
|
|
68
70
|
* @param {string} cwd корінь проєкту
|
|
69
|
-
* @returns {string[]} відсортований масив шляхів відносно cwd
|
|
71
|
+
* @returns {Promise<string[]>} відсортований масив шляхів відносно cwd
|
|
70
72
|
*/
|
|
71
|
-
export function listShellScriptPaths(cwd) {
|
|
73
|
+
export async function listShellScriptPaths(cwd) {
|
|
72
74
|
const gitPath = resolveCmd('git')
|
|
73
75
|
if (gitPath) {
|
|
74
|
-
const gitOk =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
})
|
|
79
|
-
if (gitOk.status === 0 && gitOk.stdout.trim() === 'true') {
|
|
80
|
-
const ls = spawnSync(gitPath, ['ls-files', '-z', '--', ':(glob)**/*.sh'], {
|
|
81
|
-
cwd,
|
|
82
|
-
encoding: 'utf8',
|
|
83
|
-
env: process.env
|
|
84
|
-
})
|
|
85
|
-
if (ls.status !== 0) {
|
|
76
|
+
const gitOk = await spawnAsync(gitPath, ['rev-parse', '--is-inside-work-tree'], { cwd, env: process.env })
|
|
77
|
+
if (gitOk.exitCode === 0 && gitOk.stdout.trim() === 'true') {
|
|
78
|
+
const ls = await spawnAsync(gitPath, ['ls-files', '-z', '--', ':(glob)**/*.sh'], { cwd, env: process.env })
|
|
79
|
+
if (ls.exitCode !== 0) {
|
|
86
80
|
return []
|
|
87
81
|
}
|
|
88
82
|
const files = ls.stdout.split('\0').filter(Boolean)
|
|
@@ -102,9 +96,9 @@ export function listShellScriptPaths(cwd) {
|
|
|
102
96
|
* @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`)
|
|
103
97
|
* @param {boolean} [readOnly] true → пропустити авто-фікс (diff+patch), лише фінальна перевірка
|
|
104
98
|
* @param {string[]} [scopeFiles] явний перелік файлів (delta) — якщо не задано, шукає всі `*.sh` через `listShellScriptPaths`
|
|
105
|
-
* @returns {number} 0 — OK; 1 — помилка середовища або залишкові зауваження shellcheck
|
|
99
|
+
* @returns {Promise<number>} 0 — OK; 1 — помилка середовища або залишкові зауваження shellcheck
|
|
106
100
|
*/
|
|
107
|
-
export function runShellcheckText(cwd, readOnly, scopeFiles) {
|
|
101
|
+
export async function runShellcheckText(cwd, readOnly, scopeFiles) {
|
|
108
102
|
const root = resolve(cwd ?? process.cwd())
|
|
109
103
|
const shellcheck = resolveCmd('shellcheck')
|
|
110
104
|
if (!shellcheck) {
|
|
@@ -118,14 +112,14 @@ export function runShellcheckText(cwd, readOnly, scopeFiles) {
|
|
|
118
112
|
return 1
|
|
119
113
|
}
|
|
120
114
|
|
|
121
|
-
const files = scopeFiles ?? listShellScriptPaths(root)
|
|
115
|
+
const files = scopeFiles ?? (await listShellScriptPaths(root))
|
|
122
116
|
if (files.length === 0) {
|
|
123
117
|
return 0
|
|
124
118
|
}
|
|
125
119
|
|
|
126
120
|
if (!readOnly) {
|
|
127
121
|
for (const rel of files) {
|
|
128
|
-
const fixCode = autofixOneFile(shellcheck, /** @type {string} */ (patchBin), root, rel)
|
|
122
|
+
const fixCode = await autofixOneFile(shellcheck, /** @type {string} */ (patchBin), root, rel)
|
|
129
123
|
if (fixCode !== 0) return fixCode
|
|
130
124
|
}
|
|
131
125
|
}
|
|
@@ -136,15 +130,15 @@ export function runShellcheckText(cwd, readOnly, scopeFiles) {
|
|
|
136
130
|
/**
|
|
137
131
|
* Detector text/run-shellcheck: read-only shellcheck по `ctx.files` (delta) або по всіх `*.sh` (full).
|
|
138
132
|
* @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону
|
|
139
|
-
* @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат detector-а
|
|
133
|
+
* @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат detector-а
|
|
140
134
|
*/
|
|
141
|
-
export function lint(ctx) {
|
|
135
|
+
export async function lint(ctx) {
|
|
142
136
|
const reporter = createViolationReporter(ctx)
|
|
143
137
|
const { fail } = reporter
|
|
144
138
|
const scopeFiles = ctx.files === undefined ? undefined : ctx.files.filter(f => SH_EXT_RE.test(f))
|
|
145
139
|
if (scopeFiles !== undefined && scopeFiles.length === 0) return reporter.result()
|
|
146
140
|
|
|
147
|
-
const code = runShellcheckText(ctx.cwd, true, scopeFiles)
|
|
141
|
+
const code = await runShellcheckText(ctx.cwd, true, scopeFiles)
|
|
148
142
|
if (code !== 0) fail('shellcheck знайшов порушення у *.sh (text.mdc)', 'shellcheck')
|
|
149
143
|
return reporter.result()
|
|
150
144
|
}
|
|
@@ -156,22 +150,19 @@ export function lint(ctx) {
|
|
|
156
150
|
* @param {string} patchBin абсолютний шлях до patch
|
|
157
151
|
* @param {string} root абсолютний робочий каталог (cwd для spawn)
|
|
158
152
|
* @param {string} rel відносний шлях файла від `root`
|
|
159
|
-
* @returns {number} 0 — OK; 1 — помилка spawn або patch
|
|
153
|
+
* @returns {Promise<number>} 0 — OK; 1 — помилка spawn або patch
|
|
160
154
|
*/
|
|
161
|
-
function autofixOneFile(shellcheck, patchBin, root, rel) {
|
|
155
|
+
async function autofixOneFile(shellcheck, patchBin, root, rel) {
|
|
162
156
|
for (let round = 0; round < MAX_FIX_ROUNDS_PER_FILE; round++) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
})
|
|
169
|
-
if (diffResult.error) {
|
|
170
|
-
process.stderr.write(`${diffResult.error.message}\n`)
|
|
157
|
+
let diffResult
|
|
158
|
+
try {
|
|
159
|
+
diffResult = await spawnAsync(shellcheck, ['-f', 'diff', rel], { cwd: root, env: process.env })
|
|
160
|
+
} catch (error) {
|
|
161
|
+
process.stderr.write(`${error.message}\n`)
|
|
171
162
|
return 1
|
|
172
163
|
}
|
|
173
164
|
if (shouldStopAutofixLoop(diffResult)) return 0
|
|
174
|
-
const patchCode = applyShellcheckDiff(patchBin, root, rel, diffResult.stdout ?? '')
|
|
165
|
+
const patchCode = await applyShellcheckDiff(patchBin, root, rel, diffResult.stdout ?? '')
|
|
175
166
|
if (patchCode !== 0) return patchCode
|
|
176
167
|
}
|
|
177
168
|
return 0
|
|
@@ -180,11 +171,11 @@ function autofixOneFile(shellcheck, patchBin, root, rel) {
|
|
|
180
171
|
/**
|
|
181
172
|
* Чи треба зупинити цикл авто-фіксів: shellcheck повернув 0, або у stderr є пометка
|
|
182
173
|
* `none were auto-fixable`, або stdout порожній (нема дифу для застосування).
|
|
183
|
-
* @param {{
|
|
174
|
+
* @param {{ exitCode: number | null, stdout?: string | null, stderr?: string | null }} diffResult результат `spawnAsync`
|
|
184
175
|
* @returns {boolean} true — більше нічого фіксити
|
|
185
176
|
*/
|
|
186
177
|
function shouldStopAutofixLoop(diffResult) {
|
|
187
|
-
const code = diffResult.
|
|
178
|
+
const code = diffResult.exitCode ?? 1
|
|
188
179
|
if (code === 0) return true
|
|
189
180
|
const out = (diffResult.stdout ?? '').trim()
|
|
190
181
|
const err = (diffResult.stderr ?? '').trim()
|
|
@@ -198,16 +189,18 @@ function shouldStopAutofixLoop(diffResult) {
|
|
|
198
189
|
* @param {string} root cwd для spawn
|
|
199
190
|
* @param {string} rel відносний шлях для повідомлення про помилку
|
|
200
191
|
* @param {string} diffStdout вміст unified-diff від shellcheck (input для patch)
|
|
201
|
-
* @returns {number} 0 — застосовано; 1 — помилка
|
|
192
|
+
* @returns {Promise<number>} 0 — застосовано; 1 — помилка
|
|
202
193
|
*/
|
|
203
|
-
function applyShellcheckDiff(patchBin, root, rel, diffStdout) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
input: diffStdout,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
194
|
+
async function applyShellcheckDiff(patchBin, root, rel, diffStdout) {
|
|
195
|
+
let patchRun
|
|
196
|
+
try {
|
|
197
|
+
patchRun = await spawnAsync(patchBin, ['-p1'], { cwd: root, input: diffStdout, env: process.env })
|
|
198
|
+
} catch (error) {
|
|
199
|
+
process.stderr.write(`${error.message}\n`)
|
|
200
|
+
process.stderr.write(`run-shellcheck-text: patch не застосував diff для ${rel}\n`)
|
|
201
|
+
return 1
|
|
202
|
+
}
|
|
203
|
+
if (patchRun.exitCode === 0) return 0
|
|
211
204
|
if (patchRun.stderr?.length) process.stderr.write(patchRun.stderr)
|
|
212
205
|
if (patchRun.stdout?.length) process.stderr.write(patchRun.stdout)
|
|
213
206
|
process.stderr.write(`run-shellcheck-text: patch не застосував diff для ${rel}\n`)
|
|
@@ -220,26 +213,22 @@ function applyShellcheckDiff(patchBin, root, rel, diffStdout) {
|
|
|
220
213
|
* @param {string} shellcheck абсолютний шлях до shellcheck
|
|
221
214
|
* @param {string[]} files відносні шляхи файлів для перевірки
|
|
222
215
|
* @param {string} root cwd для spawn
|
|
223
|
-
* @returns {number} 0 — чисто; 1 — помилка spawn або зауваження shellcheck
|
|
216
|
+
* @returns {Promise<number>} 0 — чисто; 1 — помилка spawn або зауваження shellcheck
|
|
224
217
|
*/
|
|
225
|
-
function runFinalShellcheck(shellcheck, files, root) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
232
|
-
})
|
|
233
|
-
if (finalRun.error) {
|
|
234
|
-
process.stderr.write(`${finalRun.error.message}\n`)
|
|
218
|
+
async function runFinalShellcheck(shellcheck, files, root) {
|
|
219
|
+
let finalRun
|
|
220
|
+
try {
|
|
221
|
+
finalRun = await spawnAsync(shellcheck, files, { cwd: root, env: process.env })
|
|
222
|
+
} catch (error) {
|
|
223
|
+
process.stderr.write(`${error.message}\n`)
|
|
235
224
|
return 1
|
|
236
225
|
}
|
|
237
|
-
if (finalRun.
|
|
226
|
+
if (finalRun.exitCode === 0) return 0
|
|
238
227
|
if (finalRun.stdout?.length) process.stdout.write(finalRun.stdout)
|
|
239
228
|
if (finalRun.stderr?.length) process.stderr.write(finalRun.stderr)
|
|
240
229
|
return 1
|
|
241
230
|
}
|
|
242
231
|
|
|
243
232
|
if (isRunAsCli(import.meta.url)) {
|
|
244
|
-
process.exitCode = runShellcheckText()
|
|
233
|
+
process.exitCode = await runShellcheckText()
|
|
245
234
|
}
|
|
@@ -44,7 +44,6 @@
|
|
|
44
44
|
* незалежно від verbosity) і виводить у stdout окреме попередження на кожен такий файл із порадою
|
|
45
45
|
* додати схему в `npm/schemas/v8r-catalog.json`.
|
|
46
46
|
*/
|
|
47
|
-
import { spawnSync } from 'node:child_process'
|
|
48
47
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
49
48
|
import { tmpdir } from 'node:os'
|
|
50
49
|
import { basename, delimiter, dirname, isAbsolute, join } from 'node:path'
|
|
@@ -53,6 +52,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
53
52
|
|
|
54
53
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
55
54
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
55
|
+
import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
|
|
56
56
|
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
57
57
|
|
|
58
58
|
/** Розширення, які валідує v8r — фільтр delta-списку файлів у `lint(ctx)`. */
|
|
@@ -183,31 +183,30 @@ export function stripBunNodeShimDirs(pathValue) {
|
|
|
183
183
|
* (`lint()`) для вбудовування у violation-повідомлення, яке бачить LLM fix-worker: без нього
|
|
184
184
|
* fix-ladder отримує лише "щось не пройшло" й не має шансів вгадати, що саме (спостережено —
|
|
185
185
|
* усі 4 rung-и незмінно падають у timeout на v8r-порушеннях).
|
|
186
|
+
* Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
|
|
187
|
+
* (ADR 260716-1354).
|
|
186
188
|
* @param {string[]} targets glob-и або конкретні шляхи файлів
|
|
187
189
|
* @param {string} configPath шлях до `V8R_CONFIG_FILE`
|
|
188
190
|
* @param {boolean} [verbose] друкувати повний raw stdout/stderr v8r при помилці; інакше — лише
|
|
189
191
|
* рядки `✖ …` без `ℹ`-шуму (Pre-warming the cache, Processing <file>, Found schema in …)
|
|
190
|
-
* @returns {{ exitError: true } | { exitError: false, code: number, detail: string }}
|
|
191
|
-
* spawn або код v8r (0/98 — трактує викликач) + деталь `✖ …`-рядків
|
|
192
|
+
* @returns {Promise<{ exitError: true } | { exitError: false, code: number, detail: string }>}
|
|
193
|
+
* помилка spawn або код v8r (0/98 — трактує викликач) + деталь `✖ …`-рядків
|
|
192
194
|
*/
|
|
193
|
-
function runOneV8rInvocation(targets, configPath, verbose = false) {
|
|
195
|
+
async function runOneV8rInvocation(targets, configPath, verbose = false) {
|
|
194
196
|
const bunPath = resolveCmd('bun') ?? process.execPath
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (result.error) {
|
|
204
|
-
process.stderr.write(`${result.error.message}\n`)
|
|
197
|
+
let result
|
|
198
|
+
try {
|
|
199
|
+
result = await spawnAsync(bunPath, ['x', 'v8r', ...targets], {
|
|
200
|
+
env: { ...env, PATH: stripBunNodeShimDirs(env.PATH), V8R_CONFIG_FILE: configPath }
|
|
201
|
+
})
|
|
202
|
+
} catch (error) {
|
|
203
|
+
process.stderr.write(`${error.message}\n`)
|
|
205
204
|
return { exitError: true }
|
|
206
205
|
}
|
|
207
206
|
|
|
208
207
|
warnAboutRemoteSchemaFallback(result.stderr ?? '')
|
|
209
208
|
|
|
210
|
-
const exitCode = result.
|
|
209
|
+
const exitCode = result.exitCode ?? 1
|
|
211
210
|
let detail = ''
|
|
212
211
|
if (exitCode !== 0 && exitCode !== 98) {
|
|
213
212
|
detail = extractFailureLines(`${result.stdout ?? ''}\n${result.stderr ?? ''}`)
|
|
@@ -225,12 +224,15 @@ function runOneV8rInvocation(targets, configPath, verbose = false) {
|
|
|
225
224
|
* Запускає послідовні виклики v8r по glob-ам (full-режим); не змінює process.exitCode.
|
|
226
225
|
* Один виклик на glob навмисно (не batch) — v8r падає з кодом 98, якщо хоч один переданий
|
|
227
226
|
* glob не знаходить файлів, і тоді решта розширень не перевіряються в тому ж виклику.
|
|
227
|
+
* Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
|
|
228
|
+
* (ADR 260716-1354). Виклики по glob-ам лишаються строго послідовними (не Promise.all) — той
|
|
229
|
+
* самий один-виклик-на-glob підхід, лише блокуючий spawnSync замінено на await у циклі.
|
|
228
230
|
* @param {string[]} [globs] патерни; за замовчуванням DEFAULT_V8R_GLOBS
|
|
229
231
|
* @param {boolean} [verbose] друкувати повний raw вивід v8r при помилці (див. runOneV8rInvocation)
|
|
230
|
-
* @returns {{ code: number, detail: string }} `code`: 0 — OK, 1 — помилка spawn, 2 — немає
|
|
232
|
+
* @returns {Promise<{ code: number, detail: string }>} `code`: 0 — OK, 1 — помилка spawn, 2 — немає
|
|
231
233
|
* каталогу схем, інше — код v8r; `detail` — рядки `✖ …` (порожньо, якщо `code` не про валідацію)
|
|
232
234
|
*/
|
|
233
|
-
export function runV8rWithGlobs(globs = DEFAULT_V8R_GLOBS, verbose = false) {
|
|
235
|
+
export async function runV8rWithGlobs(globs = DEFAULT_V8R_GLOBS, verbose = false) {
|
|
234
236
|
if (!existsSync(V8R_CATALOG_PATH)) {
|
|
235
237
|
process.stderr.write(
|
|
236
238
|
`run-v8r: не знайдено каталог схем за шляхом ${V8R_CATALOG_PATH} (очікується npm/schemas/v8r-catalog.json у пакеті)\n`
|
|
@@ -241,7 +243,7 @@ export function runV8rWithGlobs(globs = DEFAULT_V8R_GLOBS, verbose = false) {
|
|
|
241
243
|
const configPath = writeResolvedV8rConfig()
|
|
242
244
|
|
|
243
245
|
for (const pattern of globs) {
|
|
244
|
-
const r = runOneV8rInvocation([pattern], configPath, verbose)
|
|
246
|
+
const r = await runOneV8rInvocation([pattern], configPath, verbose)
|
|
245
247
|
if (r.exitError) return { code: 1, detail: '' }
|
|
246
248
|
if (r.code !== 0 && r.code !== 98) return { code: r.code, detail: r.detail }
|
|
247
249
|
}
|
|
@@ -251,12 +253,14 @@ export function runV8rWithGlobs(globs = DEFAULT_V8R_GLOBS, verbose = false) {
|
|
|
251
253
|
/**
|
|
252
254
|
* Запускає v8r по конкретному списку файлів (delta-режим) — один виклик, не по одному glob-у,
|
|
253
255
|
* бо кожен переданий шлях уже існує (не glob), тож код 98 "порожній glob" тут не виникає.
|
|
256
|
+
* Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
|
|
257
|
+
* (ADR 260716-1354).
|
|
254
258
|
* @param {string[]} files абсолютні або відносні до cwd v8r-процесу шляхи файлів
|
|
255
259
|
* @param {boolean} [verbose] друкувати повний raw вивід v8r при помилці (див. runOneV8rInvocation)
|
|
256
|
-
* @returns {{ code: number, detail: string }} `code`: 0 — OK, 1 — помилка spawn, 2 — немає
|
|
260
|
+
* @returns {Promise<{ code: number, detail: string }>} `code`: 0 — OK, 1 — помилка spawn, 2 — немає
|
|
257
261
|
* каталогу схем, інше — код v8r; `detail` — рядки `✖ …` (порожньо, якщо `code` не про валідацію)
|
|
258
262
|
*/
|
|
259
|
-
export function runV8rWithFiles(files, verbose = false) {
|
|
263
|
+
export async function runV8rWithFiles(files, verbose = false) {
|
|
260
264
|
if (files.length === 0) return { code: 0, detail: '' }
|
|
261
265
|
if (!existsSync(V8R_CATALOG_PATH)) {
|
|
262
266
|
process.stderr.write(
|
|
@@ -266,7 +270,7 @@ export function runV8rWithFiles(files, verbose = false) {
|
|
|
266
270
|
}
|
|
267
271
|
|
|
268
272
|
const configPath = writeResolvedV8rConfig()
|
|
269
|
-
const r = runOneV8rInvocation(files, configPath, verbose)
|
|
273
|
+
const r = await runOneV8rInvocation(files, configPath, verbose)
|
|
270
274
|
if (r.exitError) return { code: 1, detail: '' }
|
|
271
275
|
return { code: r.code === 98 ? 0 : r.code, detail: r.detail }
|
|
272
276
|
}
|
|
@@ -285,28 +289,29 @@ function v8rFailMessage(detail) {
|
|
|
285
289
|
/**
|
|
286
290
|
* Detector text/run-v8r: read-only v8r по `ctx.files` (delta) або за дефолтними glob-ами (full).
|
|
287
291
|
* @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону
|
|
288
|
-
* @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат detector-а
|
|
292
|
+
* @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат detector-а
|
|
289
293
|
*/
|
|
290
|
-
export function lint(ctx) {
|
|
294
|
+
export async function lint(ctx) {
|
|
291
295
|
const reporter = createViolationReporter(ctx)
|
|
292
296
|
const { fail } = reporter
|
|
293
297
|
|
|
294
298
|
const verbose = ctx.verbose === true
|
|
295
299
|
|
|
296
300
|
if (ctx.files === undefined) {
|
|
297
|
-
const { code, detail } = runV8rWithGlobs(DEFAULT_V8R_GLOBS, verbose)
|
|
301
|
+
const { code, detail } = await runV8rWithGlobs(DEFAULT_V8R_GLOBS, verbose)
|
|
298
302
|
if (code !== 0) fail(v8rFailMessage(detail), 'v8r')
|
|
299
303
|
return reporter.result()
|
|
300
304
|
}
|
|
301
305
|
|
|
302
306
|
const files = ctx.files.filter(f => V8R_EXT_RE.test(f))
|
|
303
307
|
if (files.length === 0) return reporter.result()
|
|
304
|
-
const { code, detail } = runV8rWithFiles(files, verbose)
|
|
308
|
+
const { code, detail } = await runV8rWithFiles(files, verbose)
|
|
305
309
|
if (code !== 0) fail(v8rFailMessage(detail), 'v8r')
|
|
306
310
|
return reporter.result()
|
|
307
311
|
}
|
|
308
312
|
|
|
309
313
|
if (isRunAsCli(import.meta.url)) {
|
|
310
314
|
const globs = process.argv.length > 2 ? process.argv.slice(2) : DEFAULT_V8R_GLOBS
|
|
311
|
-
|
|
315
|
+
const { code } = await runV8rWithGlobs(globs)
|
|
316
|
+
process.exitCode = code
|
|
312
317
|
}
|