@7n/rules 1.3.0 → 1.3.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 +6 -0
- package/package.json +1 -1
- package/scripts/lib/fix/template-deep-merge.mjs +58 -47
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -25,7 +25,7 @@ const SNIPPET_EXTS = ['yml', 'yaml', 'json', 'jsonc']
|
|
|
25
25
|
* Чи `needle` структурно вже присутній у якомусь елементі `actualArray`
|
|
26
26
|
* (та сама subset-семантика, що й у детекторі — жодного окремого визначення "збігу").
|
|
27
27
|
* @param {unknown[]} actualArray наявний масив
|
|
28
|
-
* @param {unknown} needle елемент
|
|
28
|
+
* @param {unknown} needle елемент snippet-а, наявність якого перевіряємо
|
|
29
29
|
* @returns {boolean} true — вже присутній структурно
|
|
30
30
|
*/
|
|
31
31
|
function containedIn(actualArray, needle) {
|
|
@@ -98,6 +98,57 @@ function mergeYamlDoc(doc, snippet, path) {
|
|
|
98
98
|
doc.setIn(path, snippet) // leaf — перезаписуємо канонічним значенням
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Рахує наступний вміст JSON/JSONC-target. `null` — невалідний вхід (не чіпаємо);
|
|
103
|
+
* незмінений `prevText` — уже відповідає snippet-у (idempotent, без reformat).
|
|
104
|
+
* @param {string} prevText наявний вміст target-файлу
|
|
105
|
+
* @param {string} snippetPath абсолютний шлях до snippet-файлу
|
|
106
|
+
* @returns {string|null} новий вміст, незмінений `prevText`, або `null`
|
|
107
|
+
*/
|
|
108
|
+
function computeJsonNextText(prevText, snippetPath) {
|
|
109
|
+
let snippet
|
|
110
|
+
let actual
|
|
111
|
+
try {
|
|
112
|
+
snippet = JSON.parse(readFileSync(snippetPath, 'utf8'))
|
|
113
|
+
actual = JSON.parse(prevText)
|
|
114
|
+
} catch {
|
|
115
|
+
return null // невалідний JSON — не чіпаємо детермінованим фіксом
|
|
116
|
+
}
|
|
117
|
+
// Уже відповідає snippet-у (та сама перевірка, що й детектор) → без reformat:
|
|
118
|
+
// JSON.stringify інакше переформатував би вже коректний файл (напр. компактний
|
|
119
|
+
// однорядковий snippet → pretty-print) без жодної реальної зміни.
|
|
120
|
+
if (checkSnippet(actual, snippet, { targetPath: '', source: '' }).length === 0) return prevText
|
|
121
|
+
return JSON.stringify(mergeJsonValue(actual, snippet), null, 2) + '\n'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Рахує наступний вміст YAML-target через `yaml` Document API. `null` — невалідний
|
|
126
|
+
* вхід; незмінений `prevText` — уже відповідає snippet-у (idempotent, без reformat).
|
|
127
|
+
* @param {string} prevText наявний вміст target-файлу
|
|
128
|
+
* @param {string} snippetPath абсолютний шлях до snippet-файлу
|
|
129
|
+
* @returns {Promise<string|null>} новий вміст, незмінений `prevText`, або `null`
|
|
130
|
+
*/
|
|
131
|
+
async function computeYamlNextText(prevText, snippetPath) {
|
|
132
|
+
const { parse, parseDocument } = await import('yaml')
|
|
133
|
+
let snippet
|
|
134
|
+
let actualPlain
|
|
135
|
+
try {
|
|
136
|
+
snippet = parse(readFileSync(snippetPath, 'utf8'))
|
|
137
|
+
actualPlain = parse(prevText)
|
|
138
|
+
} catch {
|
|
139
|
+
return null // невалідний YAML — не чіпаємо детермінованим фіксом
|
|
140
|
+
}
|
|
141
|
+
// Уже відповідає snippet-у → без reformat: Document.toString() не завжди
|
|
142
|
+
// byte-identical на вже коректному вмісті (напр. folded block scalars).
|
|
143
|
+
if (checkSnippet(actualPlain, snippet, { targetPath: '', source: '' }).length === 0) return prevText
|
|
144
|
+
const doc = parseDocument(prevText)
|
|
145
|
+
if (doc.errors.length > 0) return null
|
|
146
|
+
if (snippet !== null && typeof snippet === 'object' && !Array.isArray(snippet)) {
|
|
147
|
+
for (const [k, v] of Object.entries(snippet)) mergeYamlDoc(doc, v, [k])
|
|
148
|
+
}
|
|
149
|
+
return doc.toString()
|
|
150
|
+
}
|
|
151
|
+
|
|
101
152
|
/**
|
|
102
153
|
* Створює T0-патерн, що приводить `targetPath` у відповідність `template/*.snippet.*`
|
|
103
154
|
* свого concern-а (deep-merge, idempotent). Один writer — для будь-якого single-target
|
|
@@ -110,16 +161,13 @@ export function createTemplateFixPattern({ id, targetPath }) {
|
|
|
110
161
|
id,
|
|
111
162
|
test: violations => violations.some(v => v.file === targetPath),
|
|
112
163
|
apply: async (violations, ctx) => {
|
|
113
|
-
if (
|
|
164
|
+
if (violations.every(v => v.file !== targetPath)) return { touchedFiles: [] }
|
|
114
165
|
if (!ctx.concernDir) return { touchedFiles: [] }
|
|
115
166
|
|
|
116
167
|
const snippetPath = findSnippetFile(join(ctx.concernDir, 'template'), basename(targetPath))
|
|
117
168
|
if (!snippetPath) return { touchedFiles: [] }
|
|
118
169
|
|
|
119
170
|
const absTarget = join(ctx.cwd, targetPath)
|
|
120
|
-
const ext = extname(snippetPath).toLowerCase()
|
|
121
|
-
const isJson = ext === '.json' || ext === '.jsonc'
|
|
122
|
-
|
|
123
171
|
const prevText = existsSync(absTarget) ? readFileSync(absTarget, 'utf8') : null
|
|
124
172
|
|
|
125
173
|
// Файл відсутній → копіюємо snippet як є (без merge — немає з чим мерджити).
|
|
@@ -131,49 +179,12 @@ export function createTemplateFixPattern({ id, targetPath }) {
|
|
|
131
179
|
return { touchedFiles: [absTarget], message: `${targetPath}: створено зі snippet` }
|
|
132
180
|
}
|
|
133
181
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
}
|
|
182
|
+
const isJson = ['.json', '.jsonc'].includes(extname(snippetPath).toLowerCase())
|
|
183
|
+
const nextText = isJson
|
|
184
|
+
? computeJsonNextText(prevText, snippetPath)
|
|
185
|
+
: await computeYamlNextText(prevText, snippetPath)
|
|
175
186
|
|
|
176
|
-
if (nextText === prevText) return { touchedFiles: [] }
|
|
187
|
+
if (nextText === null || nextText === prevText) return { touchedFiles: [] }
|
|
177
188
|
ctx.recordWrite?.(absTarget)
|
|
178
189
|
writeFileSync(absTarget, nextText, 'utf8')
|
|
179
190
|
return { touchedFiles: [absTarget], message: `${targetPath}: приведено у відповідність snippet` }
|