@7n/rules 1.18.0 → 1.19.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
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.19.0] - 2026-07-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- js/eslint T0-патерн js-eslint-mechanical-text-fix — текстові заміни для suggestion-only правил поза покриттям oxlint/eslint --fix (наразі unicorn/prefer-number-is-safe-integer); зменшує обсяг порушень, що йдуть у LLM-ладдер
|
|
8
|
+
|
|
9
|
+
## [1.18.1] - 2026-07-18
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `resolveChangedBase` стійкий до stale-ref: merge-base рахується для обох кандидатів (`origin/main` і локальна `main`) і береться новіший (descendant) — у git-worktree застрягла локальна `main` більше не дає застарілу базу з фантомними «зміненими» файлами давно влитих PR (T0-фіксер створював фейкові change-фрагменти для незачеплених воркспейсів); фолбек на єдиний доступний ref офлайн/без remote збережено
|
|
14
|
+
|
|
3
15
|
## [1.18.0] - 2026-07-18
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/package.json
CHANGED
|
@@ -3,30 +3,48 @@ type: JS Module
|
|
|
3
3
|
title: fix-eslint.mjs
|
|
4
4
|
resource: npm/rules/js/eslint/fix-eslint.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 31afb96e
|
|
7
|
+
model: manual
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
## Огляд
|
|
10
11
|
|
|
11
|
-
T0-autofix для `js/eslint
|
|
12
|
-
(`oxlint --fix` + `eslint --fix`) по файлах, де
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
T0-autofix для `js/eslint`, два патерни в порядку застосування. `js-eslint-autofix`:
|
|
13
|
+
детермінований прогін лінтерів у fix-режимі (`oxlint --fix` + `eslint --fix`) по файлах, де
|
|
14
|
+
детектор знайшов порушення — виправляє лише авто-fixable правила. `js-eslint-mechanical-text-fix`:
|
|
15
|
+
для правил, які `--fix` НЕ покриває (suggestion-only у власній реалізації інструментів, перевірено
|
|
16
|
+
емпірично на реальних lint-прогонах), але заміна на позначеному `data.line` текстуально однозначна
|
|
17
|
+
й безпечна без AST-парсингу. Усе, що лишається непокритим — детектору на повторну перевірку, а
|
|
18
|
+
далі LLM-ладдеру. Запис незворотний (поза rollback).
|
|
15
19
|
|
|
16
20
|
## Поведінка
|
|
17
21
|
|
|
22
|
+
`js-eslint-autofix`:
|
|
18
23
|
- Збирає унікальні файли з порушень концерну, лишає лише js-сімейство (`.mjs/.cjs/.js/.jsx/.ts/.tsx/.vue`).
|
|
19
24
|
- Послідовно застосовує `oxlint --fix` (CLI) і `eslint --fix` (через API з `outputFixes`).
|
|
20
25
|
- До списку змінених потрапляють лише файли, чий вміст фактично змінився (порівняння до/після).
|
|
21
26
|
|
|
27
|
+
`js-eslint-mechanical-text-fix`:
|
|
28
|
+
- `MECHANICAL_TEXT_FIXES` — реєстр `{ reasons, replace }`: `reasons` покриває обидва формати
|
|
29
|
+
`violation.reason` (eslint `"plugin/rule"`, oxlint `"plugin(rule)"` — той самий rule різні тули
|
|
30
|
+
віддають по-різному); `replace(line)` — чиста функція рядка, `null` якщо шаблон не знайдено.
|
|
31
|
+
- Групує порушення за файлом, для кожного цільового `data.line` (1-indexed) застосовує
|
|
32
|
+
відповідну `replace`; рядок без очікуваного шаблону (файл змінився з моменту detect-у) —
|
|
33
|
+
пропускається без гадання.
|
|
34
|
+
- Наразі покриває `unicorn/prefer-number-is-safe-integer` (`Number.isInteger` → `Number.isSafeInteger`).
|
|
35
|
+
|
|
22
36
|
## Публічний API
|
|
23
37
|
|
|
24
|
-
- `patterns` — масив T0-патернів (`id` / `test` / `apply`), що його споживає центральний fix-pipeline
|
|
25
|
-
|
|
38
|
+
- `patterns` — масив T0-патернів (`id` / `test` / `apply`), що його споживає центральний fix-pipeline:
|
|
39
|
+
`js-eslint-autofix` (лінтери в fix-режимі), `js-eslint-mechanical-text-fix` (текстові заміни для
|
|
40
|
+
suggestion-only правил).
|
|
26
41
|
|
|
27
42
|
## Гарантії поведінки
|
|
28
43
|
|
|
29
|
-
-
|
|
30
|
-
-
|
|
44
|
+
- `js-eslint-autofix`: якщо серед порушень немає js-файлів — лінтери не запускаються, змін немає.
|
|
45
|
+
- `js-eslint-mechanical-text-fix`: пише лише файли з реальною зміною рядка (шаблон збігся); не
|
|
46
|
+
чіпає рядки поза `MECHANICAL_TEXT_FIXES`-реєстром.
|
|
47
|
+
- Обидва патерни повертають лише фактично змінені файли; перед записом кожен реєструється через
|
|
48
|
+
`recordWrite`.
|
|
31
49
|
- Межа CI: модуль належить fix-фазі; у `--no-fix` (CI) не запускається — узгоджено із
|
|
32
50
|
забороною `oxlint --fix`/`eslint --fix` у CI.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* узгоджено із забороною `oxlint --fix`/`eslint --fix` у CI (js.mdc / lint_js_yml).
|
|
12
12
|
*/
|
|
13
13
|
import { spawnSync } from 'node:child_process'
|
|
14
|
-
import { readFileSync } from 'node:fs'
|
|
14
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
15
15
|
import { resolve } from 'node:path'
|
|
16
16
|
|
|
17
17
|
import { ESLint } from 'eslint'
|
|
@@ -58,6 +58,59 @@ async function runLinterFix(jsFiles, cwd) {
|
|
|
58
58
|
return touched
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Реєстр "механічних" правил `js/eslint`: `oxlint --fix`/`eslint --fix` (T0
|
|
63
|
+
* `js-eslint-autofix` вище) їх НЕ покриває (suggestion-only у власній реалізації
|
|
64
|
+
* інструментів — перевірено емпірично: лишаються порушеними після `--fix` на
|
|
65
|
+
* реальних прогонах), але текстуальна заміна на позначеному рядку однозначна й
|
|
66
|
+
* безпечна без повного AST-парсингу — проста заміна ідентифікатора/API-виклику.
|
|
67
|
+
* `reasons` — обидва формати `reason` з `main.mjs` (`f.rule`): eslint (`ruleId`,
|
|
68
|
+
* `"plugin/rule"`) і oxlint (`d.code`, `"plugin(rule)"`) — те саме правило різні
|
|
69
|
+
* тули віддають по-різному.
|
|
70
|
+
* @type {Array<{ reasons: string[], replace: (line: string) => string|null }>}
|
|
71
|
+
*/
|
|
72
|
+
const MECHANICAL_TEXT_FIXES = [
|
|
73
|
+
{
|
|
74
|
+
// Number.isInteger(x) не ловить x поза Number.MIN/MAX_SAFE_INTEGER — сама заміна
|
|
75
|
+
// імені методу семантично точна для рядка, що ЛИШЕ так і викликає isInteger.
|
|
76
|
+
reasons: ['unicorn/prefer-number-is-safe-integer', 'unicorn(prefer-number-is-safe-integer)'],
|
|
77
|
+
replace: line =>
|
|
78
|
+
line.includes('Number.isInteger') ? line.replaceAll('Number.isInteger', 'Number.isSafeInteger') : null
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Знаходить механічний фікс для reason-у порушення (обидва формати tool-у).
|
|
84
|
+
* @param {string} reason `violation.reason`
|
|
85
|
+
* @returns {((line: string) => string|null)|null} replace-функція або null
|
|
86
|
+
*/
|
|
87
|
+
function mechanicalFixFor(reason) {
|
|
88
|
+
return MECHANICAL_TEXT_FIXES.find(f => f.reasons.includes(reason))?.replace ?? null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Застосовує механічні текстові заміни по конкретних рядках (`data.line`, 1-indexed)
|
|
93
|
+
* файлу. Рядок без збігу очікуваного шаблону (файл змінився з моменту detect-у) —
|
|
94
|
+
* пропускається, не гадаємо.
|
|
95
|
+
* @param {string} content вміст файлу
|
|
96
|
+
* @param {Array<{ line: number, replace: (line: string) => string|null }>} targets рядки й replace-функції
|
|
97
|
+
* @returns {string|null} новий вміст або null, якщо жоден рядок не змінився
|
|
98
|
+
*/
|
|
99
|
+
function applyMechanicalLineFixes(content, targets) {
|
|
100
|
+
const lines = content.split('\n')
|
|
101
|
+
let changed = false
|
|
102
|
+
for (const { line, replace } of targets) {
|
|
103
|
+
const idx = line - 1
|
|
104
|
+
if (idx < 0 || idx >= lines.length) continue
|
|
105
|
+
const next = replace(lines[idx])
|
|
106
|
+
if (next !== null && next !== lines[idx]) {
|
|
107
|
+
lines[idx] = next
|
|
108
|
+
changed = true
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return changed ? lines.join('\n') : null
|
|
112
|
+
}
|
|
113
|
+
|
|
61
114
|
/** @type {import('../../../scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
|
|
62
115
|
export const patterns = [
|
|
63
116
|
{
|
|
@@ -73,5 +126,35 @@ export const patterns = [
|
|
|
73
126
|
? { touchedFiles, message: `oxlint/eslint --fix: ${touchedFiles.length} файл(ів)` }
|
|
74
127
|
: { touchedFiles: [] }
|
|
75
128
|
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'js-eslint-mechanical-text-fix',
|
|
132
|
+
test: violations => violations.some(v => v.file && v.data?.line && mechanicalFixFor(v.reason)),
|
|
133
|
+
apply: (violations, ctx) => {
|
|
134
|
+
/** @type {Map<string, Array<{ line: number, replace: (line: string) => string|null }>>} */
|
|
135
|
+
const byFile = new Map()
|
|
136
|
+
for (const v of violations) {
|
|
137
|
+
const replace = v.file && v.data?.line ? mechanicalFixFor(v.reason) : null
|
|
138
|
+
if (!replace) continue
|
|
139
|
+
const arr = byFile.get(v.file)
|
|
140
|
+
const target = { line: v.data.line, replace }
|
|
141
|
+
if (arr) arr.push(target)
|
|
142
|
+
else byFile.set(v.file, [target])
|
|
143
|
+
}
|
|
144
|
+
const touchedFiles = []
|
|
145
|
+
for (const [rel, targets] of byFile) {
|
|
146
|
+
const abs = resolve(ctx.cwd, rel)
|
|
147
|
+
const content = readOrNull(abs)
|
|
148
|
+
if (content === null) continue
|
|
149
|
+
const next = applyMechanicalLineFixes(content, targets)
|
|
150
|
+
if (next === null) continue
|
|
151
|
+
ctx.recordWrite?.(abs)
|
|
152
|
+
writeFileSync(abs, next)
|
|
153
|
+
touchedFiles.push(abs)
|
|
154
|
+
}
|
|
155
|
+
return touchedFiles.length > 0
|
|
156
|
+
? { touchedFiles, message: `механічні текстові заміни: ${touchedFiles.length} файл(ів)` }
|
|
157
|
+
: { touchedFiles: [] }
|
|
158
|
+
}
|
|
76
159
|
}
|
|
77
160
|
]
|
|
@@ -47,21 +47,32 @@ function dropWorktreeCheckouts(paths) {
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Визначає git base для scoped-перевірок без зовнішнього runtime-стану.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* Кандидати — `origin/main` і локальна `main`: рахуємо merge-base з HEAD для обох
|
|
51
|
+
* і беремо **новіший** (descendant) з двох. Це захищає від stale-ref з будь-якого
|
|
52
|
+
* боку: у git-worktree локальна `main` закріплена за іншим деревом і часто відстає
|
|
53
|
+
* (застаріла база → фантомні «змінені» файли з давно влитих PR), а без свіжого
|
|
54
|
+
* fetch може відставати вже `origin/main`. Якщо доступний лише один ref (офлайн,
|
|
55
|
+
* без remote) — його merge-base; якщо жодного — null, і caller порівнює лише
|
|
56
|
+
* робоче дерево з HEAD. Повернений sha завжди досяжний (це merge-base існуючого
|
|
57
|
+
* ref), тож fail-closed перевірка в `collectChangedFilesSince` не спрацює хибно.
|
|
58
|
+
* Явний `baseRef` (CI: `--base origin/main` після fetch) вимикає вибір —
|
|
53
59
|
* merge-base рахується лише проти нього.
|
|
54
60
|
* @param {string} [cwd] корінь репо
|
|
55
|
-
* @param {string|null} [baseRef] явний ref бази замість
|
|
61
|
+
* @param {string|null} [baseRef] явний ref бази замість вибору origin/main|main
|
|
56
62
|
* @returns {string|null} merge-base commit або null
|
|
57
63
|
*/
|
|
58
64
|
export function resolveChangedBase(cwd = process.cwd(), baseRef = null) {
|
|
59
|
-
|
|
65
|
+
const mergeBaseWith = ref => {
|
|
60
66
|
const result = spawnSync('git', ['merge-base', 'HEAD', ref], { cwd, encoding: 'utf8' })
|
|
61
|
-
|
|
62
|
-
if (base) return base
|
|
67
|
+
return result.status === 0 && !result.error ? result.stdout.trim() : ''
|
|
63
68
|
}
|
|
64
|
-
return null
|
|
69
|
+
if (baseRef) return mergeBaseWith(baseRef) || null
|
|
70
|
+
const [primary, secondary] = ['origin/main', 'main'].map(ref => mergeBaseWith(ref)).filter(Boolean)
|
|
71
|
+
if (!primary || !secondary || primary === secondary) return primary ?? null
|
|
72
|
+
// Обидва ref-и дали різні merge-base: новіший — той, що є нащадком іншого.
|
|
73
|
+
// Якщо гілки merge-base розійшлися (екзотика), лишаємо пріоритет origin/main.
|
|
74
|
+
const ancestry = spawnSync('git', ['merge-base', '--is-ancestor', primary, secondary], { cwd })
|
|
75
|
+
return ancestry.status === 0 && !ancestry.error ? secondary : primary
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
/**
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: changed-files.mjs
|
|
4
4
|
resource: npm/scripts/lib/changed-files.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 1c7d9027
|
|
7
7
|
score: 100
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -19,8 +19,9 @@ collectChangedFilesSince Збирає змінені та незакомічен
|
|
|
19
19
|
|
|
20
20
|
- collectChangedFiles: Збирає список змінених та невідстежених файлів з робочого дерева відносно робочого дерева.
|
|
21
21
|
- resolveChangedBase: Визначає базовий комміт для обмежених перевірок без використання зовнішнього стану.
|
|
22
|
-
-
|
|
23
|
-
-
|
|
22
|
+
- Кандидати — `origin/main` і локальна `main`: merge-base з HEAD рахується для обох, береться новіший (descendant). Це захищає від stale-ref: у git-worktree локальна `main` часто відстає, без свіжого fetch може відставати `origin/main`.
|
|
23
|
+
- Явний другий аргумент `baseRef` (CI: `--base origin/main`) вимикає вибір — merge-base рахується лише проти нього.
|
|
24
|
+
- Якщо доступний лише один ref (офлайн/без remote) — його merge-base; якщо ref-и відсутні, повертає null, і викликаючий порівнює лише робоче дерево з HEAD.
|
|
24
25
|
- collectChangedFilesSince: Збирає список змінених та невідстежених файлів відносно базового комміту.
|
|
25
26
|
- git diff <base>: Порівнює комміт `base` з поточним робочим деревом (включаючи закомічені та незакомічені модифікації).
|
|
26
27
|
- Якщо `base` відсутній, використовується `collectChangedFiles`.
|