@7n/rules-lang-js 0.2.0 → 0.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 +16 -0
- package/doc-files/docs/extractors.md +34 -0
- package/doc-files/docs/index.md +10 -0
- package/doc-files/docs/units-js.md +33 -0
- package/doc-files/extractors.mjs +288 -0
- package/doc-files/units-js.mjs +141 -0
- package/package.json +13 -3
- package/taze/docs/provider.md +1 -1
- package/taze/provider.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1] - 2026-07-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- extractors.test.mjs: імпорт з ../extractors.mjs замість неіснуючого ../main.mjs (хвіст перейменування фази 5b; knip unresolved)
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-07-19
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- doc-files-екстрактори JS-екосистеми (фаза 5b spec lang-plugins-extraction): маніфест декларує розширення js/mjs/ts/vue з OKF-типами (contributes.docFiles.extensions) і handler doc-files; extractFacts (факт-лист js/mjs/ts, .vue → whole-file) та extractUnits (oxc AST юніт-шар) переїхали з ядра — генерація док для JS-файлів тепер вмикається цим плагіном
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- knip duplicates `jsProvider|default`: провайдер тепер експортується лише як default (як у lang-rust/lang-python), named-експорт `jsProvider` прибрано
|
|
18
|
+
|
|
3
19
|
## [0.2.0] - 2026-07-19
|
|
4
20
|
|
|
5
21
|
### Added
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: extractors.mjs
|
|
4
|
+
resource: plugins/lang-js/doc-files/extractors.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 5ba406e8
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Мовний doc-files-екстрактор JS-екосистеми (extension-point `doc-files`, фаза 5b spec lang-plugins-extraction): перетворює вміст js/mjs/ts-файлу на структурований факт-лист для генерації поведінкової документації. Default-експорт — обʼєкт екстрактора `{ id: 'js', extensions: ['.js','.mjs','.ts','.vue'], extractFacts, extractUnits }`, який ядро вантажить динамічно з маніфеста плагіна (`contributes.handlers['doc-files']`) лише на шляху генерації.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
- `extractFacts(src, relPath)` для `js`/`mjs`/`ts` збирає: провідний файловий JSDoc-коментар (намір модуля), експортовані декларації з їхніми JSDoc-описами (`@param`/`@returns` парсяться у структуру), імпорти класифіковані на stdlib/npm/internal, імена внутрішніх імпортованих символів та неекспортованих top-level функцій/класів (щоб модель не подавала їх як публічний API), і поведінкові маркери-евристики.
|
|
21
|
+
- Інші розширення (включно з `.vue`) → факт-лист із `unsupported: true` — генерація йде whole-file шляхом.
|
|
22
|
+
- Маркери навмисно консервативні («фабрикація гірша за мовчання»): `readOnly` — немає ні ФС-запису, ні DB-мутацій (включно з raw-SQL tagged-template з DML-ключовим словом на початку шаблону); `catchesErrors`/`returnsFalsyOnFail` — лише якщо модуль ніде не кидає; `network` свідомо over-detect (хибна гарантія «без мережі» небезпечніша); `caches` — лише за іменованим cache/memo-маркером, а не будь-яким `new Map()`; `skips` — помічені літерали пропущених тек.
|
|
23
|
+
- `extractUnits` — делегує `extractUnitsJs` з `units-js.mjs` (oxc AST, юніт-шар).
|
|
24
|
+
|
|
25
|
+
## Публічний API
|
|
26
|
+
|
|
27
|
+
- default — обʼєкт екстрактора для handler-модуля extension-point `doc-files`.
|
|
28
|
+
- extractFacts — код файлу → факт-лист (`{relPath, lang, header, exports, imports, internalSymbols, localSymbols, markers}` або `{unsupported: true}`).
|
|
29
|
+
|
|
30
|
+
## Гарантії поведінки
|
|
31
|
+
|
|
32
|
+
- Read-only: не пише у ФС, не запускає команд, не ходить у мережу.
|
|
33
|
+
- Не кидає на довільному тексті — розширення без підтримки дає `unsupported`, а не виняток.
|
|
34
|
+
- Регекс-евристики без бектрекінг-вразливих патернів (обмежені квантифікатори).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Directory Index
|
|
3
|
+
title: plugins/lang-js/doc-files
|
|
4
|
+
resource: plugins/lang-js/doc-files/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| ------------------------------- | --------- |
|
|
9
|
+
| [extractors.mjs](extractors.md) | JS Module |
|
|
10
|
+
| [units-js.mjs](units-js.md) | JS Module |
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: units-js.mjs
|
|
4
|
+
resource: plugins/lang-js/doc-files/units-js.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 9a13a64e
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.94
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
`extractUnitsJs` виділяє одиниці верхнього рівня з JS/MJS/TS-файлу та повертає їх у структурованому вигляді для подальшої обробки. Це потрібно, щоб окремо працювати з публічними частинами модуля без ручного розбору всього файлу.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
1. Розбирає вміст JS/MJS/TS-файлу в структуру програми (oxc-парсер із ядра, `@7n/rules/scripts/utils/ast-scan-utils.mjs`); якщо файл не парситься — повертає `null` (сигнал для whole-file шляху генерації).
|
|
21
|
+
2. Збирає лише top-level юніти: `function`, `class` і `const`-присвоєння, де значенням є function body.
|
|
22
|
+
3. Для експортованих декларацій позначає юніт як exported; для звичайних декларацій лишає його без позначки експорту.
|
|
23
|
+
4. Для кожного юніта фіксує межі в джерелі, текст тіла, опис, що стоїть безпосередньо перед декларацією, та список викликів, знайдених у тілі.
|
|
24
|
+
5. У списку викликів лишає тільки імена інших внутрішніх юнітів цього самого файлу; виклики самого себе відкидає.
|
|
25
|
+
6. Повертає зібраний набір юнітів для подальшого аналізу залежностей і документації.
|
|
26
|
+
|
|
27
|
+
## Публічний API
|
|
28
|
+
|
|
29
|
+
- extractUnitsJs — виділяє юніти верхнього рівня в js/mjs/ts: функції, класи й const-функції з тілом, JSDoc, позначку експорту та зв’язки викликів до інших юнітів у тілі
|
|
30
|
+
|
|
31
|
+
## Гарантії поведінки
|
|
32
|
+
|
|
33
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/** @see ./docs/extractors.md */
|
|
2
|
+
import { extractUnitsJs } from './units-js.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Мовний doc-files-екстрактор JS-екосистеми для конвеєра `@7n/rules`
|
|
6
|
+
* (extension-point `doc-files`, фаза 5b spec lang-plugins-extraction: ядро —
|
|
7
|
+
* двигун без мовної специфіки): факт-лист (`extractFacts`) для js/mjs/ts і
|
|
8
|
+
* юніти (`extractUnits`) через oxc AST. Розширення `.js`/`.mjs`/`.ts`/`.vue`
|
|
9
|
+
* та їхні OKF-типи декларуються маніфестом плагіна
|
|
10
|
+
* (`contributes.docFiles.extensions`) — hot-path ядра читає їх синхронно;
|
|
11
|
+
* цей модуль вантажиться лише на шляху генерації.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const BUILTIN_MODULES = new Set([
|
|
15
|
+
'fs',
|
|
16
|
+
'path',
|
|
17
|
+
'crypto',
|
|
18
|
+
'os',
|
|
19
|
+
'util',
|
|
20
|
+
'stream',
|
|
21
|
+
'events',
|
|
22
|
+
'http',
|
|
23
|
+
'https',
|
|
24
|
+
'url',
|
|
25
|
+
'child_process',
|
|
26
|
+
'process',
|
|
27
|
+
'assert',
|
|
28
|
+
'buffer',
|
|
29
|
+
'zlib',
|
|
30
|
+
'readline'
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
const JSDOC_OPEN_RE = /^\s*\/\*\*?/
|
|
34
|
+
const JSDOC_CLOSE_RE = /\*\/\s*$/
|
|
35
|
+
const STAR_PREFIX_RE = /^\s*\*?\s?/
|
|
36
|
+
const PARAM_LINE_RE = /^@param[ \t]{1,8}(?:\{[^}]{0,200}\}[ \t]{1,8})?\[?([\w.]{1,80})\]?[ \t]{0,8}(.{0,400})$/
|
|
37
|
+
const RETURNS_LINE_RE = /^@returns?[ \t]{1,8}(?:\{[^}]{0,200}\}[ \t]{1,8})?(.{0,400})$/
|
|
38
|
+
const FILE_HEADER_RE = /^\s*\/\*\*([\s\S]*?)\*\//
|
|
39
|
+
const PRECEDING_JSDOC_RE = /\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/
|
|
40
|
+
const EXPORT_DECL_RE = /export\s+(?:async\s+)?(function|const|class)\s+(\w+)/g
|
|
41
|
+
// Top-level function/class декларації (колонка 0) — для R6: службові функції,
|
|
42
|
+
// які не експортуються, не мають протікати у Поведінку/API як «публічні».
|
|
43
|
+
const TOP_FN_DECL_RE = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\*?|class)\s+(\w+)/gm
|
|
44
|
+
const IMPORT_FROM_RE = /^import[ \t]{1,8}[\s\S]{0,300}?from\s{1,8}['"]([^'"]+)['"]/gm
|
|
45
|
+
const NODE_PREFIX_RE = /^node:/
|
|
46
|
+
const INTERNAL_IMPORT_RE = /import[ \t]{1,8}([^'"]{0,300}?)from[ \t]{1,8}['"]\.[^'"]{1,300}['"]/g
|
|
47
|
+
const NAMED_BRACES_RE = /\{([^}]{1,400})\}/
|
|
48
|
+
const IDENT_RE = /^[\w$]{1,80}$/
|
|
49
|
+
const IMPORT_AS_RE = /[ \t]{1,8}as[ \t]{1,8}.{0,200}/
|
|
50
|
+
const WRITE_FS_RE = /\b(writeFile|mkdir|rmdir|unlink|appendFile|createWriteStream|rm\()/
|
|
51
|
+
const CATCH_RE = /catch\s*\(/
|
|
52
|
+
const TRY_RE = /\btry\s*\{/
|
|
53
|
+
// Falsy-return як «fail-safe» — лише коли воно в catch/error-гілці (інакше це
|
|
54
|
+
// звичайний guard `if (!x) return null`, не обробка помилки). Уникає over-claim.
|
|
55
|
+
const FALSY_RETURN_RE = /catch[\s\S]{0,400}?return\s+(false|null|''|"")/
|
|
56
|
+
// Мережа: окрім явного fetch/http, ловимо абстраговані клієнти (graphql/db/rpc/
|
|
57
|
+
// octokit/.request/.query). Хибний false-negative тут = небезпечна гарантія
|
|
58
|
+
// «без мережі», тож свідомо схиляємось до over-detection (м'якший бік помилки).
|
|
59
|
+
const NETWORK_RE =
|
|
60
|
+
/\bfetch\(|https?:\/\/|\bhttps?\.|axios|\bgot\(|graphql|\.request\(|\.query\(|\.mutate\(|octokit|node-fetch|undici|\bgrpc\b|websocket/i
|
|
61
|
+
// Будь-який `throw` назовні → НЕ можна гарантувати «fail-safe / без винятків».
|
|
62
|
+
const THROW_RE = /\bthrow\s/
|
|
63
|
+
// Запис у БД / зовнішню мутацію → НЕ read-only (навіть якщо нема ФС-запису).
|
|
64
|
+
// Розбито на кілька простіших патернів (та сама семантика через OR у `isMutation`),
|
|
65
|
+
// щоб уникнути надмірної складності одного великого regex.
|
|
66
|
+
const MUTATION_CALL_RE = /\b(insert|update|delete|upsert|drop|destroy|save)[A-Za-z]*\s*[(,]/
|
|
67
|
+
const MUTATION_NAME_RE = /[Mm]utation\b|\bmut[A-Z]\w*/
|
|
68
|
+
const MUTATION_METHOD_RE = /\.(save|create|update|delete|insert|destroy|mutate)\(/
|
|
69
|
+
// Raw-SQL tagged-template виклики (напр. `pgWrite\`UPDATE ...\``) — DML-ключове
|
|
70
|
+
// слово стоїть на початку тіла шаблону, не перед `(`, тож JS-орієнтовані
|
|
71
|
+
// патерни вище його не ловлять. Сигнал мінімальний, але навмисний: тег-функція
|
|
72
|
+
// (ідентифікатор впритул перед `` ` ``) + DML-keyword одразу після відкриття —
|
|
73
|
+
// уникає false positive на звичайних рядках/коментарях, де немає теg-виклику.
|
|
74
|
+
const SQL_TAGGED_MUTATION_RE = /\b\w+`\s*(?:UPDATE|INSERT|MERGE\s+INTO|DELETE\s+FROM|UPSERT)\b/i
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} src вміст файлу
|
|
77
|
+
* @returns {boolean} чи є ознаки мутації БД / зовнішнього стану
|
|
78
|
+
*/
|
|
79
|
+
const isMutation = src =>
|
|
80
|
+
MUTATION_CALL_RE.test(src) ||
|
|
81
|
+
MUTATION_NAME_RE.test(src) ||
|
|
82
|
+
MUTATION_METHOD_RE.test(src) ||
|
|
83
|
+
SQL_TAGGED_MUTATION_RE.test(src)
|
|
84
|
+
// Кеш — лише за ІМЕНОВАНИМ маркером (`cache`/`Cache`/`memoize`), не за будь-яким
|
|
85
|
+
// `new Map()`: акумулятор (напр. `byPath = new Map()`) — не кеш, а хибна гарантія
|
|
86
|
+
// «Кешує результати» гірша за пропуск (фабрикація > мовчання).
|
|
87
|
+
const CACHE_RE = /cache|memoi[sz]e/i
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Прибирає `/** */`-обрамлення й `*`-префікси, повертає чистий текст рядками.
|
|
91
|
+
* @param {string} raw сирий JSDoc-блок з обрамленням
|
|
92
|
+
* @returns {string} очищений текст без обрамлення й префіксів
|
|
93
|
+
*/
|
|
94
|
+
function cleanJsDoc(raw) {
|
|
95
|
+
return raw
|
|
96
|
+
.replace(JSDOC_OPEN_RE, '')
|
|
97
|
+
.replace(JSDOC_CLOSE_RE, '')
|
|
98
|
+
.split('\n')
|
|
99
|
+
.map(l => l.replace(STAR_PREFIX_RE, '').trimEnd())
|
|
100
|
+
.join('\n')
|
|
101
|
+
.trim()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Опис (без @-тегів) + параметри з `@param` як «name — опис».
|
|
106
|
+
* @param {string} raw сирий JSDoc-блок
|
|
107
|
+
* @returns {{desc:string, params:Array<{name:string, desc:string}>, ret:string}} розпарсений опис, параметри й опис повернення
|
|
108
|
+
*/
|
|
109
|
+
function parseJsDoc(raw) {
|
|
110
|
+
const text = cleanJsDoc(raw)
|
|
111
|
+
const lines = text.split('\n')
|
|
112
|
+
const descLines = []
|
|
113
|
+
const params = []
|
|
114
|
+
let ret = ''
|
|
115
|
+
for (const l of lines) {
|
|
116
|
+
const pm = l.match(PARAM_LINE_RE)
|
|
117
|
+
if (pm) {
|
|
118
|
+
const desc = pm[2].trim()
|
|
119
|
+
// «опис.» — JSDoc-заглушка без сенсу; не тягнемо її як факт
|
|
120
|
+
params.push({ name: pm[1], desc: desc === 'опис.' ? '' : desc })
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
const rm = l.match(RETURNS_LINE_RE)
|
|
124
|
+
if (rm) {
|
|
125
|
+
ret = rm[1].trim()
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
if (l.startsWith('@')) continue
|
|
129
|
+
descLines.push(l)
|
|
130
|
+
}
|
|
131
|
+
return { desc: descLines.join('\n').trim(), params, ret }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Провідний блок-коментар файлу (намір), якщо він перед першим import/кодом.
|
|
136
|
+
* @param {string} src вміст файлу
|
|
137
|
+
* @returns {string} текст header-коментаря або порожній рядок
|
|
138
|
+
*/
|
|
139
|
+
function extractFileHeader(src) {
|
|
140
|
+
const m = src.match(FILE_HEADER_RE)
|
|
141
|
+
if (!m) return ''
|
|
142
|
+
// має бути на самому початку (до import/код)
|
|
143
|
+
if (src.slice(0, m.index).trim() !== '') return ''
|
|
144
|
+
return parseJsDoc(m[0]).desc
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Блок-коментар, що стоїть ВПРИТУЛ перед позицією (лише пробіли між ними).
|
|
149
|
+
* `(?:(?!\*/)[\s\S])*` гарантує, що тіло не містить `*/`, тож захоплюється рівно один
|
|
150
|
+
* найближчий блок — без жадібного «перестрибування» через імпорти/код.
|
|
151
|
+
* @param {string} prefix вміст файлу до позиції експорту
|
|
152
|
+
* @returns {string|null} JSDoc-блок або null якщо немає
|
|
153
|
+
*/
|
|
154
|
+
function precedingJsDoc(prefix) {
|
|
155
|
+
const m = prefix.match(PRECEDING_JSDOC_RE)
|
|
156
|
+
return m ? m[0] : null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Експорти + JSDoc, що безпосередньо передує кожному.
|
|
161
|
+
* @param {string} src вміст файлу
|
|
162
|
+
* @returns {Array<object>} список експортів із метаданими
|
|
163
|
+
*/
|
|
164
|
+
function extractExports(src) {
|
|
165
|
+
const out = []
|
|
166
|
+
for (const m of src.matchAll(EXPORT_DECL_RE)) {
|
|
167
|
+
const [, kind, name] = m
|
|
168
|
+
const jsdocRaw = precedingJsDoc(src.slice(0, m.index))
|
|
169
|
+
out.push({ name, kind, ...(jsdocRaw ? parseJsDoc(jsdocRaw) : { desc: '', params: [], ret: '' }) })
|
|
170
|
+
}
|
|
171
|
+
return out
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Імпорти, класифіковані на stdlib / npm / internal.
|
|
176
|
+
* @param {string} src вміст файлу
|
|
177
|
+
* @returns {{stdlib:Array<string>, npm:Array<string>, internal:Array<string>}} розкласифіковані шляхи імпортів
|
|
178
|
+
*/
|
|
179
|
+
function extractImports(src) {
|
|
180
|
+
const internal = new Set(),
|
|
181
|
+
npm = new Set(),
|
|
182
|
+
stdlib = new Set()
|
|
183
|
+
for (const m of src.matchAll(IMPORT_FROM_RE)) {
|
|
184
|
+
const s = m[1]
|
|
185
|
+
if (s.startsWith('node:') || BUILTIN_MODULES.has(s.split('/', 1)[0])) stdlib.add(s.replace(NODE_PREFIX_RE, ''))
|
|
186
|
+
else if (s.startsWith('.') || s.startsWith('/')) internal.add(s)
|
|
187
|
+
else npm.add(s)
|
|
188
|
+
}
|
|
189
|
+
return { stdlib: [...stdlib], npm: [...npm], internal: [...internal] }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Імена символів, імпортованих із внутрішніх модулів — їх модель не має згадувати.
|
|
194
|
+
* @param {string} src вміст файлу
|
|
195
|
+
* @returns {Array<string>} список імен внутрішніх символів
|
|
196
|
+
*/
|
|
197
|
+
function extractInternalSymbols(src) {
|
|
198
|
+
const out = new Set()
|
|
199
|
+
for (const m of src.matchAll(INTERNAL_IMPORT_RE)) {
|
|
200
|
+
const clause = m[1]
|
|
201
|
+
const named = clause.match(NAMED_BRACES_RE)
|
|
202
|
+
if (named) {
|
|
203
|
+
for (const n of named[1].split(',')) {
|
|
204
|
+
const name = n.replace(IMPORT_AS_RE, '').trim()
|
|
205
|
+
if (name) out.add(name)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const defName = clause.replace(NAMED_BRACES_RE, '').replaceAll(',', ' ').trim().split(' ', 1)[0]
|
|
209
|
+
if (defName && IDENT_RE.test(defName)) out.add(defName)
|
|
210
|
+
}
|
|
211
|
+
return [...out]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Імена top-level функцій/класів, які НЕ експортуються (службові помічники).
|
|
216
|
+
* Модель не має подавати їх як «публічні функції» у Поведінці/API (R6).
|
|
217
|
+
* Const-стрілки свідомо не ловимо — менше false-positive на змістовних константах.
|
|
218
|
+
* @param {string} src вміст файлу
|
|
219
|
+
* @returns {Array<string>} список імен неекспортованих функцій/класів
|
|
220
|
+
*/
|
|
221
|
+
function extractLocalSymbols(src) {
|
|
222
|
+
const exported = new Set(Array.from(src.matchAll(EXPORT_DECL_RE), m => m[2]))
|
|
223
|
+
const out = new Set()
|
|
224
|
+
for (const m of src.matchAll(TOP_FN_DECL_RE)) {
|
|
225
|
+
if (!exported.has(m[1])) out.add(m[1])
|
|
226
|
+
}
|
|
227
|
+
return [...out]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Поведінкові маркери — евристики регулярками.
|
|
232
|
+
* @param {string} src вміст файлу
|
|
233
|
+
* @returns {object} набір прапорців-евристик
|
|
234
|
+
*/
|
|
235
|
+
function extractMarkers(src) {
|
|
236
|
+
// помітні «пропуски»: dir/segment-літерали у фільтрах
|
|
237
|
+
const skips = new Set()
|
|
238
|
+
for (const lit of ['.github', '.git', 'node_modules', 'base/', 'ua/', '.firebase']) {
|
|
239
|
+
if (src.includes(`'${lit}`) || src.includes(`"${lit}`) || src.includes(`/${lit}`)) skips.add(lit)
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
// «Фабрикація > мовчання»: прапорець true лише за high-confidence; інакше
|
|
243
|
+
// guaranteesFromMarkers/factsSummary його ОПУСКАЮТЬ (не стверджують протилежне).
|
|
244
|
+
readOnly: !WRITE_FS_RE.test(src) && !isMutation(src), // ні ФС-запису, ні DB-мутацій
|
|
245
|
+
catchesErrors: (CATCH_RE.test(src) || TRY_RE.test(src)) && !THROW_RE.test(src), // fail-safe лише якщо НЕ кидає
|
|
246
|
+
returnsFalsyOnFail: FALSY_RETURN_RE.test(src) && !THROW_RE.test(src),
|
|
247
|
+
network: NETWORK_RE.test(src),
|
|
248
|
+
caches: CACHE_RE.test(src),
|
|
249
|
+
skips: [...skips]
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Головний екстрактор: код файлу → факт-лист.
|
|
254
|
+
* @param {string} src вміст файлу
|
|
255
|
+
* @param {string} relPath шлях (для контексту/мови екстрактора)
|
|
256
|
+
* @returns {{relPath:string, lang:string, header:string, exports:Array, imports:object, markers:object}} структура фактів про файл
|
|
257
|
+
*/
|
|
258
|
+
export function extractFacts(src, relPath) {
|
|
259
|
+
const lang = relPath.split('.').pop()
|
|
260
|
+
if (!['js', 'mjs', 'ts'].includes(lang)) {
|
|
261
|
+
return { relPath, lang, unsupported: true, header: '', exports: [], imports: {}, markers: {} }
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
relPath,
|
|
265
|
+
lang,
|
|
266
|
+
header: extractFileHeader(src),
|
|
267
|
+
exports: extractExports(src),
|
|
268
|
+
imports: extractImports(src),
|
|
269
|
+
internalSymbols: extractInternalSymbols(src),
|
|
270
|
+
localSymbols: extractLocalSymbols(src),
|
|
271
|
+
markers: extractMarkers(src)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Default-експорт для handler-модуля extension-point `doc-files`.
|
|
277
|
+
* `.vue` у списку розширень (файл — кандидат на доку), але `extractFacts` для нього
|
|
278
|
+
* повертає `unsupported` — генерація йде whole-file шляхом, як і до винесення.
|
|
279
|
+
* @type {{ id: string, extensions: string[], extractFacts: typeof extractFacts, extractUnits: typeof extractUnitsJs }}
|
|
280
|
+
*/
|
|
281
|
+
const jsDocFilesExtractor = {
|
|
282
|
+
id: 'js',
|
|
283
|
+
extensions: ['.js', '.mjs', '.ts', '.vue'],
|
|
284
|
+
extractFacts,
|
|
285
|
+
extractUnits: extractUnitsJs
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export default jsDocFilesExtractor
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/** @see ./docs/units-js.md */
|
|
2
|
+
|
|
3
|
+
import { parseProgramOrNull, walkAstWithAncestors } from '@7n/rules/scripts/utils/ast-scan-utils.mjs'
|
|
4
|
+
|
|
5
|
+
// JSDoc-блок, що стоїть впритул перед позицією (лише пробіли між ними).
|
|
6
|
+
const JSDOC_BEFORE_RE = /\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/
|
|
7
|
+
const JSDOC_OPEN_RE = /^\s*\/\*\*?/
|
|
8
|
+
const JSDOC_CLOSE_RE = /\*\/\s*$/
|
|
9
|
+
const STAR_PREFIX_RE = /^\s*\*?\s?/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Очищає JSDoc від обрамлення `/** */` і `*`-префіксів.
|
|
13
|
+
* @param {string} raw сирий блок або порожній рядок
|
|
14
|
+
* @returns {string} текст опису без тегів-обрамлення
|
|
15
|
+
*/
|
|
16
|
+
function cleanDoc(raw) {
|
|
17
|
+
if (!raw) return ''
|
|
18
|
+
return raw
|
|
19
|
+
.replace(JSDOC_OPEN_RE, '')
|
|
20
|
+
.replace(JSDOC_CLOSE_RE, '')
|
|
21
|
+
.split('\n')
|
|
22
|
+
.map(l => l.replace(STAR_PREFIX_RE, '').trimEnd())
|
|
23
|
+
.join('\n')
|
|
24
|
+
.trim()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* JSDoc, що передує позиції `start` у джерелі (або порожній рядок).
|
|
29
|
+
* @param {string} src вміст файлу
|
|
30
|
+
* @param {number} start зміщення початку декларації
|
|
31
|
+
* @returns {string} очищений опис
|
|
32
|
+
*/
|
|
33
|
+
function precedingDoc(src, start) {
|
|
34
|
+
const m = src.slice(0, start).match(JSDOC_BEFORE_RE)
|
|
35
|
+
return cleanDoc(m ? m[0] : '')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Імʼя функції, що викликається (проста Identifier або `obj.method`).
|
|
40
|
+
* @param {Record<string, unknown>} node CallExpression
|
|
41
|
+
* @returns {string|null} імʼя callee або null
|
|
42
|
+
*/
|
|
43
|
+
function calleeName(node) {
|
|
44
|
+
const c = node.callee
|
|
45
|
+
if (!c || typeof c !== 'object') return null
|
|
46
|
+
if (c.type === 'Identifier') return c.name
|
|
47
|
+
if (c.type === 'MemberExpression' && !c.computed && c.property?.type === 'Identifier') return c.property.name
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Множина імен, що викликаються у тілі вузла (сирі callee — фільтрація на ребра
|
|
53
|
+
* call-graph робиться у `extractUnitsJs` після збору всіх імен юнітів).
|
|
54
|
+
* @param {unknown} node AST-вузол юніта
|
|
55
|
+
* @returns {Set<string>} імена викликів
|
|
56
|
+
*/
|
|
57
|
+
function collectCalls(node) {
|
|
58
|
+
const names = new Set()
|
|
59
|
+
walkAstWithAncestors(node, [], n => {
|
|
60
|
+
if (n.type !== 'CallExpression') {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const name = calleeName(n)
|
|
65
|
+
if (name) names.add(name)
|
|
66
|
+
})
|
|
67
|
+
return names
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Будує юніт із декларації, додає у `units`. Розпізнає function/class та
|
|
72
|
+
* const-функції (`const x = () => {}` / `function expression`).
|
|
73
|
+
* @param {Record<string, unknown>} decl декларація (function/class/variable)
|
|
74
|
+
* @param {boolean} exported чи експортується
|
|
75
|
+
* @param {number} docStart зміщення для пошуку JSDoc (зовнішній export-вузол)
|
|
76
|
+
* @param {string} src вміст файлу
|
|
77
|
+
* @param {Array<object>} units акумулятор
|
|
78
|
+
* @returns {void}
|
|
79
|
+
*/
|
|
80
|
+
function pushUnits(decl, exported, docStart, src, units) {
|
|
81
|
+
if (!decl || typeof decl !== 'object') return
|
|
82
|
+
const doc = precedingDoc(src, docStart)
|
|
83
|
+
if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
|
|
84
|
+
const name = decl.id?.name
|
|
85
|
+
if (!name) return
|
|
86
|
+
units.push({
|
|
87
|
+
name,
|
|
88
|
+
kind: decl.type === 'ClassDeclaration' ? 'class' : 'function',
|
|
89
|
+
exported,
|
|
90
|
+
span: { start: decl.start, end: decl.end },
|
|
91
|
+
body: src.slice(decl.start, decl.end),
|
|
92
|
+
calls: collectCalls(decl),
|
|
93
|
+
doc
|
|
94
|
+
})
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
if (decl.type === 'VariableDeclaration') {
|
|
98
|
+
for (const d of decl.declarations ?? []) {
|
|
99
|
+
const init = d.init
|
|
100
|
+
const isFn = init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')
|
|
101
|
+
if (!isFn || d.id?.type !== 'Identifier') continue
|
|
102
|
+
units.push({
|
|
103
|
+
name: d.id.name,
|
|
104
|
+
kind: 'const',
|
|
105
|
+
exported,
|
|
106
|
+
span: { start: init.start, end: init.end },
|
|
107
|
+
body: src.slice(init.start, init.end),
|
|
108
|
+
calls: collectCalls(init),
|
|
109
|
+
doc
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Юніт-шар для js/mjs/ts: top-level функції/класи/const-функції з тілом, JSDoc,
|
|
117
|
+
* прапором експорту і ребрами call-graph (виклики ІНШИХ юнітів у тілі).
|
|
118
|
+
* @param {string} src вміст файлу
|
|
119
|
+
* @param {string} [relPath] шлях (для вибору мови oxc)
|
|
120
|
+
* @returns {Array<{name:string, kind:string, exported:boolean, span:{start:number,end:number}, body:string, calls:string[], doc:string}>|null} юніти або null, якщо файл не парситься
|
|
121
|
+
*/
|
|
122
|
+
export function extractUnitsJs(src, relPath = 'scan.ts') {
|
|
123
|
+
const program = parseProgramOrNull(src, relPath)
|
|
124
|
+
if (!program || !Array.isArray(program.body)) return null
|
|
125
|
+
|
|
126
|
+
const units = []
|
|
127
|
+
for (const node of program.body) {
|
|
128
|
+
const isExport =
|
|
129
|
+
(node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') && node.declaration
|
|
130
|
+
if (isExport) {
|
|
131
|
+
pushUnits(node.declaration, true, node.start, src, units)
|
|
132
|
+
} else {
|
|
133
|
+
pushUnits(node, false, node.start, src, units)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Ребра call-graph: лишаємо тільки виклики інших внутрішніх юнітів
|
|
138
|
+
const names = new Set(units.map(u => u.name))
|
|
139
|
+
for (const u of units) u.calls = [...u.calls].filter(n => names.has(n) && n !== u.name)
|
|
140
|
+
return units
|
|
141
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/rules-lang-js",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Плагін @7n/rules: JS/npm/bun-екосистема — taze-провайдер (package.json, bunx taze)",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Плагін @7n/rules: JS/npm/bun-екосистема — taze-провайдер (package.json, bunx taze) і doc-files-екстрактори (oxc AST)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"javascript",
|
|
7
7
|
"bun",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"type": "module",
|
|
23
23
|
"files": [
|
|
24
|
+
"doc-files",
|
|
24
25
|
"taze",
|
|
25
26
|
"skills",
|
|
26
27
|
"CHANGELOG.md",
|
|
@@ -36,7 +37,16 @@
|
|
|
36
37
|
"contributes": {
|
|
37
38
|
"rules": false,
|
|
38
39
|
"handlers": {
|
|
39
|
-
"taze": "./taze/provider.mjs"
|
|
40
|
+
"taze": "./taze/provider.mjs",
|
|
41
|
+
"doc-files": "./doc-files/extractors.mjs"
|
|
42
|
+
},
|
|
43
|
+
"docFiles": {
|
|
44
|
+
"extensions": {
|
|
45
|
+
".js": "JS Module",
|
|
46
|
+
".mjs": "JS Module",
|
|
47
|
+
".ts": "TS Module",
|
|
48
|
+
".vue": "Vue Component"
|
|
49
|
+
}
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
52
|
},
|
package/taze/docs/provider.md
CHANGED
package/taze/provider.mjs
CHANGED
|
@@ -95,7 +95,7 @@ function runCommand(cmd, args, cwd, spawnFn) {
|
|
|
95
95
|
* `diff.major` записи мапляться workspace → manifest (контракт порту).
|
|
96
96
|
* @type {import('@7n/rules/plugin-api').EcosystemProvider}
|
|
97
97
|
*/
|
|
98
|
-
|
|
98
|
+
const jsProvider = {
|
|
99
99
|
id: 'js-bun',
|
|
100
100
|
title: 'npm/bun-пакети',
|
|
101
101
|
manifestNoun: 'package.json',
|