@7n/rules 1.21.0 → 1.23.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.
@@ -1,311 +0,0 @@
1
- /** @see ./docs/units-rs.md */
2
-
3
- /**
4
- * Пропускає рядковий літерал `"..."` (з escape-послідовностями).
5
- * @param {string} src вміст файлу
6
- * @param {number} i позиція відкриваючого `"`
7
- * @returns {number} позиція ПІСЛЯ закриваючого `"`
8
- */
9
- function skipString(src, i) {
10
- i++ // відкриваючий "
11
- while (i < src.length) {
12
- if (src[i] === '\\') {
13
- i += 2
14
- continue
15
- }
16
- if (src[i] === '"') return i + 1
17
- i++
18
- }
19
- return i
20
- }
21
-
22
- /**
23
- * Пропускає рядковий (`//`) чи блочний коментар на позиції `i`.
24
- * @param {string} src вміст файлу
25
- * @param {number} i позиція `/` початку коментаря
26
- * @returns {number} позиція ПІСЛЯ коментаря, або `-1` якщо на `i` не коментар
27
- */
28
- function skipComment(src, i) {
29
- if (src[i] !== '/') return -1
30
- if (src[i + 1] === '/') {
31
- const nl = src.indexOf('\n', i)
32
- return nl === -1 ? src.length : nl + 1
33
- }
34
- if (src[i + 1] === '*') {
35
- const end = src.indexOf('*/', i + 2)
36
- return end === -1 ? src.length : end + 2
37
- }
38
- return -1
39
- }
40
-
41
- /**
42
- * Знаходить індекс закриваючої `}` для відкриваючої `{` на позиції `start`.
43
- * Правильно пропускає рядки/блочні коментарі та рядкові літерали.
44
- * @param {string} src вміст файлу
45
- * @param {number} start позиція відкриваючої `{`
46
- * @returns {number} індекс `}` або -1, якщо не знайдено
47
- */
48
- function findClosingBrace(src, start) {
49
- let depth = 0
50
- let i = start
51
- while (i < src.length) {
52
- const ch = src[i]
53
- const afterComment = skipComment(src, i)
54
- if (afterComment !== -1) {
55
- i = afterComment
56
- continue
57
- }
58
- if (ch === '"') {
59
- i = skipString(src, i)
60
- continue
61
- }
62
- if (ch === '{') {
63
- depth++
64
- i++
65
- continue
66
- }
67
- if (ch === '}') {
68
- depth--
69
- if (depth === 0) return i
70
- i++
71
- continue
72
- }
73
- i++
74
- }
75
- return -1
76
- }
77
-
78
- /**
79
- * Видобуває `///` doc-рядки безпосередньо перед рядком `lineIdx`.
80
- * Сканує назад через `///`, `#[...]` та пусті рядки.
81
- * @param {string[]} lines рядки файлу
82
- * @param {number} lineIdx рядок декларації
83
- * @returns {string} склеєний опис або ''
84
- */
85
- function docBefore(lines, lineIdx) {
86
- const doc = []
87
- for (let i = lineIdx - 1; i >= 0; i--) {
88
- const t = lines[i].trim()
89
- if (t.startsWith('///')) {
90
- doc.unshift(t.slice(3).trim())
91
- } else if (t.startsWith('#[') || t.startsWith('#![') || t === '') {
92
- // пропустити атрибути та пусті рядки
93
- } else {
94
- break
95
- }
96
- }
97
- return doc.join(' ').trim()
98
- }
99
-
100
- // Pub-items матчаться у два кроки по trim-нутому рядку (прості регекспи без
101
- // бектрекінгу): спершу опційний pub(...)-префікс, потім сама декларація.
102
- // Також ловить fn без pub (для localSymbols і impl-методів)
103
- const PUB_PREFIX_RE = /^pub(?:\([^)]*\))?\s+/
104
- const ITEM_DECL_RE = /^(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|type)\s+(\w+)/
105
-
106
- // impl Type { або impl<T> Trait for Type { — теж двокроково: голова `impl<...>`,
107
- // далі тип після `for` (trait-impl) або перше слово (inherent impl)
108
- const IMPL_HEAD_RE = /^impl(?:<[^>]*>)?\s+/
109
- const IMPL_FOR_TYPE_RE = /\bfor\s+(\w+)/
110
- const TYPE_NAME_RE = /^(\w+)/
111
-
112
- // Підозрілі exposure-атрибути, що роблять непуб-fn фактично публічними
113
- const EXPOSURE_ATTR_RE = /#\[(?:tauri::command|wasm_bindgen|uniffi::export|pyo3::pyfunction|napi)/
114
-
115
- // Базовий виклик fn-імені (для call-graph всередині юніта)
116
- const CALL_RE = /\b([a-z_]\w*)\s*\(/g
117
-
118
- /**
119
- * Рахує дельту глибини `{}` в рядку, пропускаючи `//`-коментарі й рядкові літерали.
120
- * @param {string} line рядок коду
121
- * @returns {number} приріст глибини (додатний — відкрито більше, ніж закрито)
122
- */
123
- function braceDeltaInLine(line) {
124
- let delta = 0
125
- let j = 0
126
- while (j < line.length) {
127
- const ch = line[j]
128
- if (ch === '/' && line[j + 1] === '/') break
129
- if (ch === '"') {
130
- j++
131
- while (j < line.length && line[j] !== '"') {
132
- if (line[j] === '\\') j++
133
- j++
134
- }
135
- j++
136
- continue
137
- }
138
- if (ch === '{') delta++
139
- else if (ch === '}') delta--
140
- j++
141
- }
142
- return delta
143
- }
144
-
145
- /**
146
- * Оновлює impl-стек за поточним рядком: прибирає закриті impl і, якщо рядок — impl-
147
- * декларація на глибині ≤1, додає новий запис.
148
- * @param {Array<{typeName:string, openDepth:number}>} implStack стек відкритих impl (мутується)
149
- * @param {string} line сирий рядок
150
- * @param {string} trimmed рядок без лідируючих пробілів
151
- * @param {number} depth глибина ПІСЛЯ обробки рядка
152
- * @param {number} depthAtStart глибина ДО обробки рядка
153
- * @returns {void}
154
- */
155
- function updateImplStack(implStack, line, trimmed, depth, depthAtStart) {
156
- while (implStack.length > 0 && implStack.at(-1).openDepth > depth) {
157
- implStack.pop()
158
- }
159
- if (depthAtStart <= 1) {
160
- const headM = trimmed.match(IMPL_HEAD_RE)
161
- if (headM && line.includes('{')) {
162
- const rest = trimmed.slice(headM[0].length)
163
- const typeM = rest.match(IMPL_FOR_TYPE_RE) ?? rest.match(TYPE_NAME_RE)
164
- if (typeM) implStack.push({ typeName: typeM[1], openDepth: depth })
165
- }
166
- }
167
- }
168
-
169
- /**
170
- * Витягує тіло item-а (fn/struct/enum/trait) через `findClosingBrace`.
171
- * @param {string} src вміст файлу
172
- * @param {string[]} lines рядки файлу
173
- * @param {number} li індекс рядка декларації
174
- * @param {number} lineOffset зсув початку рядка в `src`
175
- * @param {string} kind вид item-а
176
- * @returns {{body:string, itemEnd:number}} тіло та зсув кінця item-а
177
- */
178
- function extractItemBody(src, lines, li, lineOffset, kind) {
179
- let body = ''
180
- let itemEnd = lineOffset + lines[li].length
181
- if (kind !== 'type') {
182
- const openBraceIdx = src.indexOf('{', lineOffset)
183
- // Шукаємо `{` не далі ніж через 3 рядки від початку декларації
184
- const threeLines = lines.slice(li, li + 3).join('\n').length
185
- if (openBraceIdx !== -1 && openBraceIdx - lineOffset <= threeLines) {
186
- const closeIdx = findClosingBrace(src, openBraceIdx)
187
- if (closeIdx !== -1) {
188
- itemEnd = closeIdx + 1
189
- body = src.slice(lineOffset, itemEnd)
190
- }
191
- }
192
- }
193
- return { body, itemEnd }
194
- }
195
-
196
- /**
197
- * Заповнює `calls` кожного юніта викликами інших юнітів цього ж файлу.
198
- * @param {Array<{name:string, body:string, calls:string[]}>} units юніти файлу (мутуються)
199
- * @returns {void}
200
- */
201
- function fillCallGraph(units) {
202
- const unitNames = new Set(units.map(u => u.name))
203
- for (const u of units) {
204
- if (!u.body) continue
205
- const calls = new Set()
206
- let cm
207
- const re = new RegExp(CALL_RE.source, 'g')
208
- while ((cm = re.exec(u.body)) !== null) {
209
- if (unitNames.has(cm[1]) && cm[1] !== u.name) calls.add(cm[1])
210
- }
211
- u.calls = [...calls]
212
- }
213
- }
214
-
215
- /**
216
- * Обробляє рядок на глибині ≤1: якщо це декларація item-а — додає юніт у `units`;
217
- * інакше скидає exposure-флаг на не-атрибутних рядках. Повертає новий стан флага.
218
- * @param {object} p параметри
219
- * @param {string} p.src вміст файлу
220
- * @param {string[]} p.lines рядки файлу
221
- * @param {number} p.li індекс рядка
222
- * @param {number} p.lineOffset зсув початку рядка в `src`
223
- * @param {number} p.depthAtStart глибина ДО обробки рядка
224
- * @param {string|null} p.currentImpl тип поточного impl або `null`
225
- * @param {boolean} p.nextFnExposed чи наступний fn exposure-exposed
226
- * @param {Array<object>} p.units акумулятор юнітів (мутується)
227
- * @returns {boolean} новий стан `nextFnExposed`
228
- */
229
- function processItemLine({ src, lines, li, lineOffset, depthAtStart, currentImpl, nextFnExposed, units }) {
230
- const line = lines[li]
231
- const trimmed = line.trimStart()
232
- const pubM = trimmed.match(PUB_PREFIX_RE)
233
- const m = (pubM ? trimmed.slice(pubM[0].length) : trimmed).match(ITEM_DECL_RE)
234
- if (!m) {
235
- // Рядок не є item — скидаємо exposure-флаг якщо не атрибут
236
- const t = line.trim()
237
- if (!t.startsWith('#[') && !t.startsWith('#![') && !t.startsWith('///') && t !== '') return false
238
- return nextFnExposed
239
- }
240
- const kind = m[1]
241
- const isPub = Boolean(pubM) || (kind === 'fn' && nextFnExposed)
242
- const doc = docBefore(lines, li)
243
- // Витягуємо тіло через findClosingBrace для fn/struct/enum/trait
244
- const { body, itemEnd } = extractItemBody(src, lines, li, lineOffset, kind)
245
- units.push({
246
- name: m[2],
247
- kind,
248
- exported: isPub,
249
- implName: depthAtStart === 1 ? currentImpl : null,
250
- span: { start: lineOffset, end: itemEnd },
251
- body,
252
- calls: [],
253
- doc
254
- })
255
- return kind === 'fn' ? false : nextFnExposed
256
- }
257
-
258
- /**
259
- * Юніт-екстрактор для `.rs` файлів.
260
- * Визначає top-level і impl-методи через підрахунок дужок по рядках.
261
- * Відомі обмеження: рядкові літерали з `{`/`}` всередині `{}` можуть дати
262
- * хибну глибину (рідкісно в реальному Rust-коді з rustfmt).
263
- * @param {string} src вміст файлу
264
- * @param {string} [_relPath] резервний (не використовується)
265
- * @returns {Array<{name:string, kind:string, exported:boolean, implName:string|null, span:{start:number,end:number}, body:string, calls:string[], doc:string}>|null} юніти файлу (fn та impl-методи) або `null`, якщо юнітів не знайдено
266
- */
267
- export function extractUnitsRs(src, _relPath) {
268
- const lines = src.split('\n')
269
- const units = []
270
- let depth = 0
271
- let lineOffset = 0
272
- // Стек відкритих impl: { typeName, openDepth }
273
- const implStack = []
274
- // Флаг: наступний fn отримує exposure (через #[tauri::command] тощо)
275
- let nextFnExposed = false
276
-
277
- for (let li = 0; li < lines.length; li++) {
278
- const line = lines[li]
279
- const depthAtStart = depth
280
- depth += braceDeltaInLine(line)
281
-
282
- updateImplStack(implStack, line, line.trimStart(), depth, depthAtStart)
283
- const currentImpl = implStack.at(-1)?.typeName ?? null
284
-
285
- // Перевіряємо exposure-атрибути
286
- if (EXPOSURE_ATTR_RE.test(line)) {
287
- nextFnExposed = true
288
- }
289
-
290
- // Елементи на глибині 0 (top-level) і 1 (всередині impl)
291
- if (depthAtStart <= 1) {
292
- nextFnExposed = processItemLine({
293
- src,
294
- lines,
295
- li,
296
- lineOffset,
297
- depthAtStart,
298
- currentImpl,
299
- nextFnExposed,
300
- units
301
- })
302
- }
303
-
304
- lineOffset += line.length + 1 // +1 для '\n'
305
- }
306
-
307
- // Базовий call-graph: виклики інших юнітів цього файлу
308
- fillCallGraph(units)
309
-
310
- return units.length > 0 ? units : null
311
- }