@7n/rules 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/rules/abie/clean_merged_ignore_branches/fix-clean_merged_ignore_branches.mjs +3 -0
- package/rules/docker/lint_docker_yml/fix-lint_docker_yml.mjs +3 -0
- package/rules/ga/clean_ga_workflows/fix-clean_ga_workflows.mjs +3 -0
- package/rules/ga/clean_merged_branch/fix-clean_merged_branch.mjs +3 -0
- package/rules/ga/git_ai/fix-git_ai.mjs +3 -0
- package/rules/ga/lint_ga/fix-lint_ga.mjs +3 -0
- package/rules/ga/vscode_settings/fix-vscode_settings.mjs +3 -0
- package/rules/ga/zizmor_yml/fix-zizmor_yml.mjs +3 -0
- package/rules/js/jscpd_config/fix-jscpd_config.mjs +3 -0
- package/rules/js/lint_js_yml/fix-lint_js_yml.mjs +5 -0
- package/rules/js/package_json/fix-package_json.mjs +3 -0
- package/rules/k8s/lint_k8s_yml/fix-lint_k8s_yml.mjs +3 -0
- package/rules/npm-module/emit_types_config/fix-emit_types_config.mjs +3 -0
- package/rules/npm-module/npm_package_json/fix-npm_package_json.mjs +3 -0
- package/rules/npm-module/npm_publish_yml/fix-npm_publish_yml.mjs +5 -0
- package/rules/npm-module/root_package_json/fix-root_package_json.mjs +3 -0
- package/rules/php/lint_php_yml/fix-lint_php_yml.mjs +3 -0
- package/rules/python/lint_python_yml/fix-lint_python_yml.mjs +3 -0
- package/rules/rego/vscode_settings/fix-vscode_settings.mjs +3 -0
- package/rules/rust/lint_rust_yml/fix-lint_rust_yml.mjs +5 -0
- package/rules/security/lint_security_yml/fix-lint_security_yml.mjs +3 -0
- package/rules/style/lint_style_yml/fix-lint_style_yml.mjs +3 -0
- package/rules/style/package_json/fix-package_json.mjs +3 -0
- package/rules/style/vscode_settings/fix-vscode_settings.mjs +3 -0
- package/rules/text/lint_text/fix-lint_text.mjs +5 -0
- package/rules/text/oxfmtrc/fix-oxfmtrc.mjs +3 -0
- package/rules/text/vscode_settings/fix-vscode_settings.mjs +3 -0
- package/rules/worktree/vscode_settings/fix-vscode_settings.mjs +5 -0
- package/rules/worktree/zed_settings/fix-zed_settings.mjs +3 -0
- package/scripts/lib/fix/template-deep-merge.mjs +182 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Спільний T0-autofix writer для policy-концернів "один target-файл + один канонічний
|
|
3
|
+
* `template/*.snippet.{json,jsonc,yml,yaml}`" (`engine:"template"` і `engine:"rego"`
|
|
4
|
+
* з тим самим snippet-шаблоном). Deep-merge snippet → target: об'єкти мерджаться по
|
|
5
|
+
* ключах, масиви — union за структурним підмножинним збігом (`checkSnippet`-семантика,
|
|
6
|
+
* як у детекторі — жодного окремого визначення "збігу"), листя — перезаписується
|
|
7
|
+
* канонічним значенням. Файл відсутній → копіюється сам snippet (без merge).
|
|
8
|
+
*
|
|
9
|
+
* JSON/JSONC — plain-object merge + `JSON.stringify`. YAML — `yaml` Document API
|
|
10
|
+
* (`setIn`/`addIn`/`hasIn`), щоб зберегти коментарі й форматування наявного файлу;
|
|
11
|
+
* створюється лише те, чого бракує.
|
|
12
|
+
*
|
|
13
|
+
* Кожен викличний concern передає лише `{ id, targetPath }` — сам writer резолвить
|
|
14
|
+
* snippet-файл у `template/` свого concern-а через `ctx.concernDir` (той самий
|
|
15
|
+
* механізм, що й `vscode-ext-add.mjs`).
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { basename, dirname, extname, join } from 'node:path'
|
|
19
|
+
|
|
20
|
+
import { checkSnippet } from '../template.mjs'
|
|
21
|
+
|
|
22
|
+
const SNIPPET_EXTS = ['yml', 'yaml', 'json', 'jsonc']
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Чи `needle` структурно вже присутній у якомусь елементі `actualArray`
|
|
26
|
+
* (та сама subset-семантика, що й у детекторі — жодного окремого визначення "збігу").
|
|
27
|
+
* @param {unknown[]} actualArray наявний масив
|
|
28
|
+
* @param {unknown} needle елемент снippета, наявність якого перевіряємо
|
|
29
|
+
* @returns {boolean} true — вже присутній структурно
|
|
30
|
+
*/
|
|
31
|
+
function containedIn(actualArray, needle) {
|
|
32
|
+
return actualArray.some(a => checkSnippet(a, needle, { targetPath: '', source: '' }).length === 0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Шукає `<basename>.snippet.<ext>` у `template/` concern-а (перебір відомих розширень).
|
|
37
|
+
* @param {string} templateDir абсолютний шлях до `template/` concern-а
|
|
38
|
+
* @param {string} targetBasename basename цільового файлу (напр. `npm-publish.yml`)
|
|
39
|
+
* @returns {string|null} абсолютний шлях до snippet-файлу або null
|
|
40
|
+
*/
|
|
41
|
+
function findSnippetFile(templateDir, targetBasename) {
|
|
42
|
+
if (!existsSync(templateDir)) return null
|
|
43
|
+
for (const ext of SNIPPET_EXTS) {
|
|
44
|
+
const p = join(templateDir, `${targetBasename}.snippet.${ext}`)
|
|
45
|
+
if (existsSync(p)) return p
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Рекурсивний deep-merge snippet у plain JS-значення (JSON/JSONC-гілка).
|
|
52
|
+
* @param {unknown} actual наявне значення (або undefined)
|
|
53
|
+
* @param {unknown} snippet канонічний фрагмент
|
|
54
|
+
* @returns {unknown} злите значення
|
|
55
|
+
*/
|
|
56
|
+
function mergeJsonValue(actual, snippet) {
|
|
57
|
+
if (Array.isArray(snippet)) {
|
|
58
|
+
const arr = Array.isArray(actual) ? [...actual] : []
|
|
59
|
+
for (const needle of snippet) {
|
|
60
|
+
if (!containedIn(arr, needle)) arr.push(needle)
|
|
61
|
+
}
|
|
62
|
+
return arr
|
|
63
|
+
}
|
|
64
|
+
if (snippet !== null && typeof snippet === 'object') {
|
|
65
|
+
const obj = actual !== null && typeof actual === 'object' && !Array.isArray(actual) ? { ...actual } : {}
|
|
66
|
+
for (const [k, v] of Object.entries(snippet)) obj[k] = mergeJsonValue(obj[k], v)
|
|
67
|
+
return obj
|
|
68
|
+
}
|
|
69
|
+
return snippet // leaf — перезаписуємо канонічним значенням
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Deep-merge snippet у YAML `Document` за шляхом (мутує `doc`). Масиви — `addIn` лише
|
|
74
|
+
* відсутніх (структурно) елементів; об'єкти — рекурсія по ключах (`setIn` створює
|
|
75
|
+
* проміжні мапи автоматично); листя — `setIn`.
|
|
76
|
+
* @param {import('yaml').Document} doc YAML-документ (мутується)
|
|
77
|
+
* @param {unknown} snippet канонічний фрагмент на цьому шляху
|
|
78
|
+
* @param {Array<string|number>} path шлях у документі
|
|
79
|
+
* @returns {void}
|
|
80
|
+
*/
|
|
81
|
+
function mergeYamlDoc(doc, snippet, path) {
|
|
82
|
+
if (Array.isArray(snippet)) {
|
|
83
|
+
// `doc.createNode([])` — примусово YAMLSeq-вузол; голий `setIn(path, [])` іноді
|
|
84
|
+
// лишає сирий JS-масив (коли батьківська мапа вже існує), і подальший `addIn` кидає
|
|
85
|
+
// `Expected YAML collection at …` — createNode гарантує коректний тип вузла завжди.
|
|
86
|
+
if (!doc.hasIn(path)) doc.setIn(path, doc.createNode([]))
|
|
87
|
+
const existing = doc.getIn(path)
|
|
88
|
+
const existingJs = existing && typeof existing.toJS === 'function' ? existing.toJS(doc) : []
|
|
89
|
+
for (const needle of snippet) {
|
|
90
|
+
if (!containedIn(existingJs, needle)) doc.addIn(path, needle)
|
|
91
|
+
}
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
if (snippet !== null && typeof snippet === 'object') {
|
|
95
|
+
for (const [k, v] of Object.entries(snippet)) mergeYamlDoc(doc, v, [...path, k])
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
doc.setIn(path, snippet) // leaf — перезаписуємо канонічним значенням
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Створює T0-патерн, що приводить `targetPath` у відповідність `template/*.snippet.*`
|
|
103
|
+
* свого concern-а (deep-merge, idempotent). Один writer — для будь-якого single-target
|
|
104
|
+
* snippet-концерну (`engine:"template"` чи `engine:"rego"` з тим самим snippet-шаблоном).
|
|
105
|
+
* @param {{ id: string, targetPath: string }} opts `id` — унікальний id T0-патерну; `targetPath` — posix-relative шлях цільового файлу від cwd
|
|
106
|
+
* @returns {import('../lint-surface/types.mjs').T0Pattern} T0-патерн для `fix-<concern>.mjs`
|
|
107
|
+
*/
|
|
108
|
+
export function createTemplateFixPattern({ id, targetPath }) {
|
|
109
|
+
return {
|
|
110
|
+
id,
|
|
111
|
+
test: violations => violations.some(v => v.file === targetPath),
|
|
112
|
+
apply: async (violations, ctx) => {
|
|
113
|
+
if (!violations.some(v => v.file === targetPath)) return { touchedFiles: [] }
|
|
114
|
+
if (!ctx.concernDir) return { touchedFiles: [] }
|
|
115
|
+
|
|
116
|
+
const snippetPath = findSnippetFile(join(ctx.concernDir, 'template'), basename(targetPath))
|
|
117
|
+
if (!snippetPath) return { touchedFiles: [] }
|
|
118
|
+
|
|
119
|
+
const absTarget = join(ctx.cwd, targetPath)
|
|
120
|
+
const ext = extname(snippetPath).toLowerCase()
|
|
121
|
+
const isJson = ext === '.json' || ext === '.jsonc'
|
|
122
|
+
|
|
123
|
+
const prevText = existsSync(absTarget) ? readFileSync(absTarget, 'utf8') : null
|
|
124
|
+
|
|
125
|
+
// Файл відсутній → копіюємо snippet як є (без merge — немає з чим мерджити).
|
|
126
|
+
if (prevText === null) {
|
|
127
|
+
const rawSnippet = readFileSync(snippetPath, 'utf8')
|
|
128
|
+
ctx.recordWrite?.(absTarget)
|
|
129
|
+
mkdirSync(dirname(absTarget), { recursive: true })
|
|
130
|
+
writeFileSync(absTarget, rawSnippet, 'utf8')
|
|
131
|
+
return { touchedFiles: [absTarget], message: `${targetPath}: створено зі snippet` }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let nextText
|
|
135
|
+
if (isJson) {
|
|
136
|
+
let snippet
|
|
137
|
+
let actual
|
|
138
|
+
try {
|
|
139
|
+
snippet = JSON.parse(readFileSync(snippetPath, 'utf8'))
|
|
140
|
+
actual = JSON.parse(prevText)
|
|
141
|
+
} catch {
|
|
142
|
+
return { touchedFiles: [] } // невалідний JSON — не чіпаємо детермінованим фіксом
|
|
143
|
+
}
|
|
144
|
+
// Уже відповідає snippet-у (та сама перевірка, що й детектор) → не чіпаємо файл
|
|
145
|
+
// взагалі: без цієї гейтки JSON.stringify переформатував би вже коректний файл
|
|
146
|
+
// (напр. компактний однорядковий snippet → pretty-print) без жодної реальної зміни.
|
|
147
|
+
if (checkSnippet(actual, snippet, { targetPath: '', source: '' }).length === 0) {
|
|
148
|
+
return { touchedFiles: [] }
|
|
149
|
+
}
|
|
150
|
+
const merged = mergeJsonValue(actual, snippet)
|
|
151
|
+
nextText = JSON.stringify(merged, null, 2) + '\n'
|
|
152
|
+
} else {
|
|
153
|
+
const { parse, parseDocument } = await import('yaml')
|
|
154
|
+
let snippet
|
|
155
|
+
let actualPlain
|
|
156
|
+
try {
|
|
157
|
+
snippet = parse(readFileSync(snippetPath, 'utf8'))
|
|
158
|
+
actualPlain = parse(prevText)
|
|
159
|
+
} catch {
|
|
160
|
+
return { touchedFiles: [] } // невалідний YAML — не чіпаємо детермінованим фіксом
|
|
161
|
+
}
|
|
162
|
+
// Уже відповідає snippet-у (та сама перевірка, що й детектор) → не чіпаємо файл
|
|
163
|
+
// взагалі: Document.toString() не завжди byte-identical на вже коректному вмісті
|
|
164
|
+
// (напр. folded block scalars переформатовуються при round-trip без потреби).
|
|
165
|
+
if (checkSnippet(actualPlain, snippet, { targetPath: '', source: '' }).length === 0) {
|
|
166
|
+
return { touchedFiles: [] }
|
|
167
|
+
}
|
|
168
|
+
const doc = parseDocument(prevText)
|
|
169
|
+
if (doc.errors.length > 0) return { touchedFiles: [] }
|
|
170
|
+
if (snippet !== null && typeof snippet === 'object' && !Array.isArray(snippet)) {
|
|
171
|
+
for (const [k, v] of Object.entries(snippet)) mergeYamlDoc(doc, v, [k])
|
|
172
|
+
}
|
|
173
|
+
nextText = doc.toString()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (nextText === prevText) return { touchedFiles: [] }
|
|
177
|
+
ctx.recordWrite?.(absTarget)
|
|
178
|
+
writeFileSync(absTarget, nextText, 'utf8')
|
|
179
|
+
return { touchedFiles: [absTarget], message: `${targetPath}: приведено у відповідність snippet` }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|