@7n/rules 1.20.0 → 1.21.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 +13 -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/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/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/docs/resolve-plugins.md +1 -1
- 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
- package/rules/python/applies/concern.json +0 -4
- package/rules/python/applies/docs/main.md +0 -29
- package/rules/python/applies/main.mjs +0 -24
- package/rules/python/docs/index.md +0 -11
- package/rules/python/lib/docs/index.md +0 -9
- package/rules/python/lib/docs/uv-run.md +0 -32
- package/rules/python/lib/uv-run.mjs +0 -48
- package/rules/python/main.json +0 -1
- package/rules/python/main.mdc +0 -8
- package/rules/python/mypy/concern.json +0 -7
- package/rules/python/mypy/docs/main.md +0 -32
- package/rules/python/mypy/main.mjs +0 -36
- package/rules/python/project/concern.json +0 -6
- package/rules/python/project/docs/main.md +0 -43
- package/rules/python/project/main.mjs +0 -115
- package/rules/python/pyproject_toml/concern.json +0 -9
- package/rules/python/pyproject_toml/pyproject_toml.mdc +0 -29
- package/rules/python/pyproject_toml/pyproject_toml.rego +0 -53
- package/rules/python/pyproject_toml/template/pyproject.toml.deny.toml +0 -2
- package/rules/python/pyproject_toml/template/pyproject.toml.snippet.toml +0 -12
- package/rules/python/ruff/concern.json +0 -7
- package/rules/python/ruff/docs/fix-ruff.md +0 -34
- package/rules/python/ruff/docs/main.md +0 -35
- package/rules/python/ruff/fix-ruff.mjs +0 -63
- package/rules/python/ruff/main.mjs +0 -52
- package/rules/python/tooling/concern.json +0 -15
- package/rules/python/tooling/docs/index.md +0 -11
- package/rules/python/tooling/docs/main.md +0 -35
- package/rules/python/tooling/main.mjs +0 -45
- package/rules/python/tooling/tooling.mdc +0 -22
- package/rules/rust/applies/concern.json +0 -4
- package/rules/rust/applies/docs/index.md +0 -11
- package/rules/rust/applies/docs/main.md +0 -30
- package/rules/rust/applies/main.mjs +0 -28
- package/rules/rust/check/check.mdc +0 -16
- package/rules/rust/check/concern.json +0 -7
- package/rules/rust/check/docs/fix-check.md +0 -30
- package/rules/rust/check/docs/index.md +0 -11
- package/rules/rust/check/docs/main.md +0 -37
- package/rules/rust/check/fix-check.mjs +0 -79
- package/rules/rust/check/main.mjs +0 -81
- package/rules/rust/docs/index.md +0 -11
- package/rules/rust/lib/docs/has-cargo-toml.md +0 -134
- package/rules/rust/lib/docs/index.md +0 -11
- package/rules/rust/lib/has-cargo-toml.mjs +0 -37
- package/rules/rust/main.json +0 -1
- package/rules/rust/main.mdc +0 -8
- package/rules/rust/package_json/concern.json +0 -10
- package/rules/rust/package_json/package_json.mdc +0 -12
- package/rules/rust/package_json/package_json.rego +0 -20
- package/rules/rust/vscode_extensions/concern.json +0 -11
- package/rules/rust/vscode_extensions/docs/fix-vscode_extensions.md +0 -25
- package/rules/rust/vscode_extensions/docs/index.md +0 -11
- package/rules/rust/vscode_extensions/fix-vscode_extensions.mjs +0 -1
- package/rules/rust/vscode_extensions/template/extensions.json.snippet.json +0 -1
- package/rules/rust/vscode_extensions/vscode_extensions.mdc +0 -9
- package/rules/rust/vscode_extensions/vscode_extensions.rego +0 -15
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
* typo лишаються → cspell повертає !=0 → exit 1 (людина доправляє одруки вручну).
|
|
17
17
|
*/
|
|
18
18
|
import { env } from 'node:process'
|
|
19
|
-
import { spawnSync } from 'node:child_process'
|
|
20
19
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
21
20
|
import { join } from 'node:path'
|
|
22
21
|
|
|
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
|
/** Слово у рядку cspell: `<file>:<line>:<col> - Unknown word (xxx)`. */
|
|
@@ -44,21 +44,18 @@ export const fixModel = () => env.N_LOCAL_MIN_MODEL || ''
|
|
|
44
44
|
* рядок (`--no-summary` НЕ передається) лишається — з нього парситься `FILES_CHECKED_RE`.
|
|
45
45
|
* @param {string} cwd корінь
|
|
46
46
|
* @param {string} bin шлях до cspell (npx/локальний)
|
|
47
|
+
* Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
|
|
48
|
+
* (ADR 260716-1354).
|
|
47
49
|
* @param {string[]} [files] явний перелік файлів (delta); без нього — `cspell .`
|
|
48
50
|
* @param {boolean} [verbose] показати повний нативний вивід cspell (включно з прогресом)
|
|
49
|
-
* @returns {{ code:number, out:string }} код + обʼєднаний stdout/stderr
|
|
51
|
+
* @returns {Promise<{ code:number, out:string }>} код + обʼєднаний stdout/stderr
|
|
50
52
|
*/
|
|
51
|
-
export function detectCspell(cwd, bin, files, verbose = false) {
|
|
53
|
+
export async function detectCspell(cwd, bin, files, verbose = false) {
|
|
52
54
|
const targets = files === undefined ? ['.'] : files
|
|
53
55
|
const quietArgs = verbose ? [] : ['--no-progress']
|
|
54
|
-
const r =
|
|
55
|
-
cwd,
|
|
56
|
-
encoding: 'utf8',
|
|
57
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
58
|
-
env: process.env
|
|
59
|
-
})
|
|
56
|
+
const r = await spawnAsync(bin, ['cspell', ...quietArgs, ...targets], { cwd, env: process.env })
|
|
60
57
|
const out = `${r.stdout ?? ''}${r.stderr ?? ''}`
|
|
61
|
-
const code = typeof r.
|
|
58
|
+
const code = typeof r.exitCode === 'number' ? r.exitCode : 1
|
|
62
59
|
// cspell повертає !=0 і коли жоден переданий файл не пройшов ignorePaths
|
|
63
60
|
// (`.cspell.json`) — «Files checked: 0» означає «нічого перевіряти», не порушення.
|
|
64
61
|
const checked = FILES_CHECKED_RE.exec(out)
|
|
@@ -138,19 +135,21 @@ export function appendWordsToDict(cwd, words) {
|
|
|
138
135
|
/**
|
|
139
136
|
* cspell-крок lint-text: read-only детект (нуль мутацій). LLM-класифікація знахідок
|
|
140
137
|
* і поповнення `.cspell.json#words` живуть у `fix-worker.mjs` (Central Runner Pipeline).
|
|
138
|
+
* Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
|
|
139
|
+
* (ADR 260716-1354).
|
|
141
140
|
* @param {string[]} [files] явний перелік файлів (delta); без нього — `cspell .`
|
|
142
141
|
* @param {string} [cwd] корінь
|
|
143
142
|
* @param {boolean} [verbose] показати повний нативний вивід cspell (включно з прогресом)
|
|
144
|
-
* @returns {number} 0 — чисто; 1 — лишились знахідки / помилка середовища
|
|
143
|
+
* @returns {Promise<number>} 0 — чисто; 1 — лишились знахідки / помилка середовища
|
|
145
144
|
*/
|
|
146
|
-
export function runCspellText(files, cwd = process.cwd(), verbose = false) {
|
|
145
|
+
export async function runCspellText(files, cwd = process.cwd(), verbose = false) {
|
|
147
146
|
const bin = resolveCmd('npx')
|
|
148
147
|
if (!bin) {
|
|
149
148
|
process.stderr.write('❌ npx не знайдено в PATH (cspell).\n')
|
|
150
149
|
return 1
|
|
151
150
|
}
|
|
152
151
|
|
|
153
|
-
const first = detectCspell(cwd, bin, files, verbose)
|
|
152
|
+
const first = await detectCspell(cwd, bin, files, verbose)
|
|
154
153
|
if (first.code === 0) {
|
|
155
154
|
// «Files checked: 0» при явно переданих файлах — легітимно (усі під ignorePaths),
|
|
156
155
|
// але так само виглядає зламане середовище (спостережено 2026-07-12: cspell-gitignore
|
|
@@ -174,12 +173,12 @@ export function runCspellText(files, cwd = process.cwd(), verbose = false) {
|
|
|
174
173
|
* @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону
|
|
175
174
|
* @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат детектора
|
|
176
175
|
*/
|
|
177
|
-
export function lint(ctx) {
|
|
176
|
+
export async function lint(ctx) {
|
|
178
177
|
const reporter = createViolationReporter(ctx)
|
|
179
178
|
const { fail } = reporter
|
|
180
179
|
if (ctx.files !== undefined && ctx.files.length === 0) return reporter.result()
|
|
181
180
|
|
|
182
|
-
const code = runCspellText(ctx.files, ctx.cwd, ctx.verbose === true)
|
|
181
|
+
const code = await runCspellText(ctx.files, ctx.cwd, ctx.verbose === true)
|
|
183
182
|
if (code !== 0) fail('cspell знайшов порушення правопису (text.mdc)', 'cspell')
|
|
184
183
|
return reporter.result()
|
|
185
184
|
}
|
|
@@ -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
|
}
|