@nitra/cursor 5.3.2 → 5.3.3
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/fix.mjs +1 -2
- package/rules/adr/fix.mjs +1 -2
- package/rules/bun/fix.mjs +1 -2
- package/rules/capacitor/fix.mjs +1 -2
- package/rules/capacitor/js/platforms.mjs +0 -2
- package/rules/changelog/fix.mjs +1 -2
- package/rules/ci4/fix.mjs +1 -2
- package/rules/docker/fix.mjs +1 -2
- package/rules/efes/fix.mjs +1 -2
- package/rules/feedback/fix.mjs +1 -2
- package/rules/ga/fix.mjs +1 -2
- package/rules/graphql/fix.mjs +1 -2
- package/rules/hasura/fix.mjs +1 -2
- package/rules/image-avif/fix.mjs +1 -2
- package/rules/image-compress/fix.mjs +1 -2
- package/rules/js-bun-db/fix.mjs +1 -2
- package/rules/js-bun-redis/fix.mjs +1 -2
- package/rules/js-lint/fix.mjs +1 -2
- package/rules/js-lint-ci/fix.mjs +1 -2
- package/rules/js-mssql/fix.mjs +1 -2
- package/rules/js-run/fix.mjs +1 -2
- package/rules/k8s/fix.mjs +1 -2
- package/rules/k8s/js/manifests.mjs +1 -5
- package/rules/nginx-default-tpl/fix.mjs +1 -2
- package/rules/npm-module/fix.mjs +1 -2
- package/rules/npm-module/js/package_structure.mjs +0 -1
- package/rules/php/fix.mjs +1 -2
- package/rules/python/fix.mjs +1 -2
- package/rules/rego/fix.mjs +1 -2
- package/rules/release/fix.mjs +1 -2
- package/rules/rust/fix.mjs +1 -2
- package/rules/security/fix.mjs +1 -2
- package/rules/style-lint/fix.mjs +1 -2
- package/rules/tauri/fix.mjs +1 -2
- package/rules/test/coverage/coverage.mjs +0 -2
- package/rules/test/fix.mjs +1 -2
- package/rules/text/fix.mjs +1 -2
- package/rules/vue/fix.mjs +1 -2
- package/rules/worktree/fix.mjs +1 -2
- package/scripts/lib/run-rule.mjs +0 -2
- package/scripts/lint-cli.mjs +0 -1
- package/scripts/utils/with-lock.mjs +0 -1
- package/skills/doc-aggregate/js/docgen-scan.mjs +17 -18
- package/skills/doc-files/.changes/260612-0002.md +5 -0
- package/skills/doc-files/.changes/260612-0006.md +5 -0
- package/skills/doc-files/.changes/260612-0008.md +5 -0
- package/skills/doc-files/js/docgen-extract.mjs +136 -0
- package/skills/doc-files/js/docgen-prompts.mjs +2 -2
- package/skills/doc-files/js/docgen-scan.mjs +21 -22
- package/skills/doc-files/js/docs/units-rs.md +35 -0
- package/skills/doc-files/js/units-rs.mjs +213 -0
- package/skills/doc-files/js/units.mjs +4 -3
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/rules/abie/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/adr/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/bun/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/capacitor/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
|
@@ -21,11 +21,9 @@ const IGNORED_DIRS_FOR_PACKAGE_JSON = new Set([
|
|
|
21
21
|
])
|
|
22
22
|
|
|
23
23
|
/** `||` у діапазоні npm-версій */
|
|
24
|
-
// eslint-disable-next-line sonarjs/slow-regex -- короткі **semver**-підрядки у **package.json**
|
|
25
24
|
const NPM_OR_PARTS_RE = /\s*\|\|\s*/
|
|
26
25
|
|
|
27
26
|
/** `a - b` (діапазон діапазонів) */
|
|
28
|
-
// eslint-disable-next-line sonarjs/slow-regex -- форма **X - Y** у **npm**-range
|
|
29
27
|
const NPM_HYPHEN_RANGE_RE = /^(.+?)\s+-\s+(.+)$/
|
|
30
28
|
|
|
31
29
|
const FIRST_VERSION_NUM_RE = /^(?:v)?(\d+)/i
|
package/rules/changelog/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/ci4/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/docker/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/efes/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/feedback/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/ga/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/graphql/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/hasura/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/image-avif/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/js-bun-db/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/js-lint/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/js-lint-ci/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/js-mssql/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/js-run/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/k8s/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
|
@@ -3984,11 +3984,8 @@ const noopFail = msg => msg
|
|
|
3984
3984
|
* @type {readonly { ipBlock: { cidr: string } }[]}
|
|
3985
3985
|
*/
|
|
3986
3986
|
const NETWORK_POLICY_GCLB_INGRESS_FROM = Object.freeze([
|
|
3987
|
-
// eslint-disable-next-line sonarjs/no-hardcoded-ip
|
|
3988
3987
|
{ ipBlock: { cidr: '35.191.0.0/16' } },
|
|
3989
|
-
// eslint-disable-next-line sonarjs/no-hardcoded-ip
|
|
3990
3988
|
{ ipBlock: { cidr: '130.211.0.0/22' } },
|
|
3991
|
-
// eslint-disable-next-line sonarjs/no-hardcoded-ip
|
|
3992
3989
|
{ ipBlock: { cidr: '10.0.0.0/8' } }
|
|
3993
3990
|
])
|
|
3994
3991
|
|
|
@@ -4858,9 +4855,8 @@ function firstValidYamlJsonFromPatchText(patchText) {
|
|
|
4858
4855
|
if (d.errors.length === 0) return d.toJSON()
|
|
4859
4856
|
}
|
|
4860
4857
|
} catch {
|
|
4861
|
-
|
|
4858
|
+
// ignore parse errors
|
|
4862
4859
|
}
|
|
4863
|
-
return undefined
|
|
4864
4860
|
}
|
|
4865
4861
|
|
|
4866
4862
|
/**
|
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/npm-module/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
|
@@ -257,7 +257,6 @@ export function globToRegex(glob) {
|
|
|
257
257
|
// Дозволено: уся функція існує саме для конструкції RegExp з glob-pattern
|
|
258
258
|
// у `files` (значення з npm/package.json, не від кінцевого користувача), і
|
|
259
259
|
// спецсимволи вже екрановано через `REGEX_SPECIAL_IN_GLOB` вище.
|
|
260
|
-
// eslint-disable-next-line security/detect-non-literal-regexp
|
|
261
260
|
return new RegExp(`^${re}$`, 'u')
|
|
262
261
|
}
|
|
263
262
|
|
package/rules/php/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/python/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/rego/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/release/fix.mjs
CHANGED
|
@@ -12,6 +12,5 @@ export function run(ctx) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
|
-
|
|
16
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
15
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
17
16
|
}
|
package/rules/rust/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/security/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/style-lint/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/tauri/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
|
@@ -170,7 +170,6 @@ export function renderMarkdown(rows, allowedGaps = []) {
|
|
|
170
170
|
async function loadProvider(rulesDir, ruleId) {
|
|
171
171
|
const providerPath = join(rulesDir, ruleId, 'coverage', 'coverage.mjs')
|
|
172
172
|
if (!existsSync(providerPath)) return null
|
|
173
|
-
// eslint-disable-next-line no-unsanitized/method -- providerPath з join(rulesDir, ruleId, …), ruleId з конфігу
|
|
174
173
|
const mod = await import(pathToFileURL(providerPath).href)
|
|
175
174
|
if (typeof mod.detect !== 'function' || typeof mod.collect !== 'function') return null
|
|
176
175
|
return mod
|
|
@@ -292,7 +291,6 @@ export async function runCoverageSteps(opts = {}) {
|
|
|
292
291
|
console.log('✓ COVERAGE.md')
|
|
293
292
|
|
|
294
293
|
if (opts.fix) {
|
|
295
|
-
// eslint-disable-next-line no-unsanitized/method -- шлях відносний до пакету, не user-input
|
|
296
294
|
const { fixSurvivedMutants } = await import(new URL('../../../scripts/coverage-fix.mjs', import.meta.url).href)
|
|
297
295
|
await fixSurvivedMutants(allSurvived, cwd)
|
|
298
296
|
}
|
package/rules/test/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/text/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/vue/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/rules/worktree/fix.mjs
CHANGED
|
@@ -14,6 +14,5 @@ export function run(ctx) {
|
|
|
14
14
|
if (isRunAsCli(import.meta.url)) {
|
|
15
15
|
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
16
|
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
-
|
|
18
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
19
18
|
}
|
package/scripts/lib/run-rule.mjs
CHANGED
|
@@ -53,7 +53,6 @@ async function evaluateAppliesGate(bundledRulesDir, rule) {
|
|
|
53
53
|
const concern = rule.jsConcerns.find(c => c.name === APPLIES_CONCERN_NAME)
|
|
54
54
|
if (!concern) return true
|
|
55
55
|
const path = resolveJsCheckPath(bundledRulesDir, rule.id, concern)
|
|
56
|
-
// eslint-disable-next-line no-unsanitized/method -- path з discovered concern, файл з whitelist'у readdir
|
|
57
56
|
const mod = await import(path)
|
|
58
57
|
if (typeof mod.applies !== 'function') return true
|
|
59
58
|
return Boolean(await mod.applies())
|
|
@@ -170,7 +169,6 @@ export async function runRule(rule, bundledRulesDir, walkCache) {
|
|
|
170
169
|
|
|
171
170
|
for (const concern of rule.jsConcerns) {
|
|
172
171
|
const path = resolveJsCheckPath(bundledRulesDir, rule.id, concern)
|
|
173
|
-
// eslint-disable-next-line no-unsanitized/method -- path з discovered concern, файл з whitelist'у readdir
|
|
174
172
|
const mod = await import(path)
|
|
175
173
|
if (typeof mod.check === 'function') {
|
|
176
174
|
const code = await mod.check()
|
package/scripts/lint-cli.mjs
CHANGED
|
@@ -74,7 +74,6 @@ export async function runLint(opts = {}) {
|
|
|
74
74
|
log(`⚠️ lint: правило ${id} має lint-фазу, але немає js/lint.mjs — пропускаю.\n`)
|
|
75
75
|
continue
|
|
76
76
|
}
|
|
77
|
-
// eslint-disable-next-line no-unsanitized/method -- шлях з discovered rule dir
|
|
78
77
|
const mod = await import(lintPath)
|
|
79
78
|
const code = await mod.lint(changed, cwd)
|
|
80
79
|
if (code !== 0) return code
|
|
@@ -128,7 +128,6 @@ export async function withLock(key, runFn, opts = {}) {
|
|
|
128
128
|
|
|
129
129
|
const onSignal = () => {
|
|
130
130
|
release()
|
|
131
|
-
// eslint-disable-next-line n/no-process-exit -- SIGINT/SIGTERM мають завершити процес із кодом 130
|
|
132
131
|
process.exit(130)
|
|
133
132
|
}
|
|
134
133
|
process.once('SIGINT', onSignal)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** @see ./docs/docgen-scan.md */
|
|
2
|
-
|
|
3
|
-
import path from 'node:path'
|
|
2
|
+
import { join, relative, dirname, extname, sep, isAbsolute, resolve } from 'node:path'
|
|
4
3
|
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
5
4
|
|
|
6
5
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
@@ -18,7 +17,7 @@ const TEST_FILE_RE = /\.(?:test|spec)\.[^.]+$/u
|
|
|
18
17
|
* @returns {boolean} true — корінь system-wide docs
|
|
19
18
|
*/
|
|
20
19
|
function isSystemWideDocsRoot(root) {
|
|
21
|
-
return existsSync(
|
|
20
|
+
return existsSync(join(root, 'docs', 'adr')) || existsSync(join(root, 'docs', 'explanation'))
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
/**
|
|
@@ -29,7 +28,7 @@ function isSystemWideDocsRoot(root) {
|
|
|
29
28
|
export function isSourceFile(fileName) {
|
|
30
29
|
if (fileName.endsWith('.d.ts')) return false
|
|
31
30
|
if (TEST_FILE_RE.test(fileName)) return false
|
|
32
|
-
return SOURCE_EXTENSIONS.has(
|
|
31
|
+
return SOURCE_EXTENSIONS.has(extname(fileName))
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
/**
|
|
@@ -49,14 +48,14 @@ export function scanSourceFiles(root) {
|
|
|
49
48
|
return
|
|
50
49
|
}
|
|
51
50
|
for (const entry of entries) {
|
|
52
|
-
const fullPath =
|
|
53
|
-
const relPath =
|
|
51
|
+
const fullPath = join(dir, entry.name)
|
|
52
|
+
const relPath = relative(root, fullPath)
|
|
54
53
|
if (entry.isDirectory()) {
|
|
55
54
|
if (isDocgenIgnored(relPath, 'dir')) continue
|
|
56
55
|
walk(fullPath)
|
|
57
56
|
} else if (entry.isFile() && isSourceFile(entry.name)) {
|
|
58
|
-
if (isSystemWideDocsRoot(root) &&
|
|
59
|
-
const sourcePath = relPath.split(
|
|
57
|
+
if (isSystemWideDocsRoot(root) && dirname(relPath) === '.') continue
|
|
58
|
+
const sourcePath = relPath.split(sep).join('/')
|
|
60
59
|
if (isDocgenIgnored(sourcePath)) continue
|
|
61
60
|
results.push(sourcePath)
|
|
62
61
|
}
|
|
@@ -74,10 +73,10 @@ export function scanSourceFiles(root) {
|
|
|
74
73
|
* @returns {string} slug: `npm/rules/adr` → `npm-rules-adr`, корінь → `root`
|
|
75
74
|
*/
|
|
76
75
|
export function slugForModule(root, moduleRoot) {
|
|
77
|
-
const rel =
|
|
76
|
+
const rel = relative(root, moduleRoot)
|
|
78
77
|
if (rel === '') return 'root'
|
|
79
78
|
return rel
|
|
80
|
-
.split(
|
|
79
|
+
.split(sep)
|
|
81
80
|
.join('-')
|
|
82
81
|
.replaceAll(/[^\w-]+/gu, '-')
|
|
83
82
|
}
|
|
@@ -99,8 +98,8 @@ export function findModuleRoots(root) {
|
|
|
99
98
|
return
|
|
100
99
|
}
|
|
101
100
|
for (const entry of entries) {
|
|
102
|
-
const fullPath =
|
|
103
|
-
const relPath =
|
|
101
|
+
const fullPath = join(dir, entry.name)
|
|
102
|
+
const relPath = relative(root, fullPath)
|
|
104
103
|
if (entry.isDirectory()) {
|
|
105
104
|
if (isDocgenIgnored(relPath, 'dir')) continue
|
|
106
105
|
walk(fullPath)
|
|
@@ -123,8 +122,8 @@ export function findModuleRoots(root) {
|
|
|
123
122
|
export function nearestModuleRoot(filePath, moduleRoots) {
|
|
124
123
|
let best = null
|
|
125
124
|
for (const moduleRoot of moduleRoots) {
|
|
126
|
-
const rel =
|
|
127
|
-
if (rel.startsWith('..') ||
|
|
125
|
+
const rel = relative(moduleRoot, filePath)
|
|
126
|
+
if (rel.startsWith('..') || isAbsolute(rel)) continue
|
|
128
127
|
if (best === null || moduleRoot.length > best.length) best = moduleRoot
|
|
129
128
|
}
|
|
130
129
|
return best
|
|
@@ -141,7 +140,7 @@ export function scanForModules(root) {
|
|
|
141
140
|
const moduleRoots = findModuleRoots(root)
|
|
142
141
|
const byRoot = new Map()
|
|
143
142
|
for (const sourcePath of files) {
|
|
144
|
-
const moduleRoot = nearestModuleRoot(
|
|
143
|
+
const moduleRoot = nearestModuleRoot(join(root, sourcePath), moduleRoots)
|
|
145
144
|
if (moduleRoot === null) continue
|
|
146
145
|
if (!byRoot.has(moduleRoot)) byRoot.set(moduleRoot, [])
|
|
147
146
|
byRoot.get(moduleRoot).push(sourcePath)
|
|
@@ -151,10 +150,10 @@ export function scanForModules(root) {
|
|
|
151
150
|
for (const moduleRoot of moduleRoots) {
|
|
152
151
|
const members = byRoot.get(moduleRoot)
|
|
153
152
|
if (!members || members.length === 0) continue
|
|
154
|
-
const docPath =
|
|
153
|
+
const docPath = join(moduleRoot, 'docs', 'ARCHITECTURE.md')
|
|
155
154
|
results.push({
|
|
156
155
|
moduleRoot,
|
|
157
|
-
relRoot:
|
|
156
|
+
relRoot: relative(root, moduleRoot) || '.',
|
|
158
157
|
slug: slugForModule(root, moduleRoot),
|
|
159
158
|
docPath,
|
|
160
159
|
members: members.toSorted(),
|
|
@@ -171,7 +170,7 @@ export function scanForModules(root) {
|
|
|
171
170
|
*/
|
|
172
171
|
export function resolveRoot(argv) {
|
|
173
172
|
const i = argv.indexOf('--root')
|
|
174
|
-
return i !== -1 && argv[i + 1] ?
|
|
173
|
+
return i !== -1 && argv[i + 1] ? resolve(argv[i + 1]) : process.cwd()
|
|
175
174
|
}
|
|
176
175
|
|
|
177
176
|
/**
|
|
@@ -211,6 +211,141 @@ function extractMarkers(src) {
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
// ── Rust-екстрактор ──────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
// Модульний //! doc-коментар (inner doc)
|
|
217
|
+
const RS_MODULE_DOC_RE = /^(?:[ \t]*\/\/![ \t]?(.*)\n)*/m
|
|
218
|
+
|
|
219
|
+
// pub fn / pub struct / pub enum / pub trait (та fn із exposure-атрибутом)
|
|
220
|
+
const RS_PUB_ITEM_RE = /^[ \t]*(pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|type)\s+(\w+)/gm
|
|
221
|
+
|
|
222
|
+
// Exposure-атрибути (#[tauri::command] тощо)
|
|
223
|
+
const RS_EXPOSURE_ATTR_RE = /#\[(?:tauri::command|wasm_bindgen|uniffi::export|pyo3::pyfunction|napi)/gm
|
|
224
|
+
|
|
225
|
+
// /// line-doc перед елементом
|
|
226
|
+
const RS_LINE_DOC_RE = /(?:[ \t]*\/\/\/[ \t]?.*\n)*/
|
|
227
|
+
|
|
228
|
+
// use crate::module::{A, B} або use std::..;
|
|
229
|
+
const RS_USE_RE = /^[ \t]*use\s+([\w:]+(?:::\{[^}]+\})?(?:::\*)?(?:::\w+)?)\s*;/gm
|
|
230
|
+
|
|
231
|
+
// Файловий запис: fs::write / File::create / remove_file / create_dir / write_all
|
|
232
|
+
const RS_WRITE_RE = /fs::write|File::create|remove_file|create_dir|BufWriter::new|OpenOptions[^;]*\.write\s*\(\s*true/
|
|
233
|
+
|
|
234
|
+
// Обробка помилок (але не просто `?`)
|
|
235
|
+
const RS_CATCH_RE = /\.unwrap_or(?:_else|_default)?|if\s+let\s+Err\s*\(|match\s+\S+.*\{\s*[\s\S]*?Err\s*\(|\.map_err\s*\(|\.ok\s*\(\)/
|
|
236
|
+
|
|
237
|
+
// Функції, що повертають Result або Option
|
|
238
|
+
const RS_RESULT_RE = /->\s*(?:Result|Option)\s*</
|
|
239
|
+
|
|
240
|
+
// Мережа
|
|
241
|
+
const RS_NETWORK_RE = /reqwest|hyper::|TcpStream|UdpSocket|tokio::net/
|
|
242
|
+
|
|
243
|
+
// Кешування
|
|
244
|
+
const RS_CACHE_RE = /\bcache\b|\bCache\b|lazy_static!|OnceCell|OnceLock|DashMap/i
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Видобуває `///` doc-рядки перед рядком `lineIdx` (назад через `#[...]` та пусті рядки).
|
|
248
|
+
* @param {string[]} lines рядки файлу
|
|
249
|
+
* @param {number} lineIdx індекс рядка декларації
|
|
250
|
+
* @returns {string} опис або ''
|
|
251
|
+
*/
|
|
252
|
+
function rsDocBefore(lines, lineIdx) {
|
|
253
|
+
const doc = []
|
|
254
|
+
for (let i = lineIdx - 1; i >= 0; i--) {
|
|
255
|
+
const t = lines[i].trim()
|
|
256
|
+
if (t.startsWith('///')) doc.unshift(t.slice(3).trim())
|
|
257
|
+
else if (t.startsWith('#[') || t.startsWith('#![') || t === '') { /* skip */ }
|
|
258
|
+
else break
|
|
259
|
+
}
|
|
260
|
+
return doc.join(' ').trim()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Витягує факт-лист для `.rs` файлу.
|
|
265
|
+
* @param {string} src вміст файлу
|
|
266
|
+
* @param {string} relPath відносний шлях
|
|
267
|
+
* @returns {object} факт-лист без `unsupported`
|
|
268
|
+
*/
|
|
269
|
+
function extractFactsRust(src, relPath) {
|
|
270
|
+
// header — //! module-level doc
|
|
271
|
+
const headerLines = []
|
|
272
|
+
for (const line of src.split('\n')) {
|
|
273
|
+
const t = line.trim()
|
|
274
|
+
if (t.startsWith('//!')) headerLines.push(t.slice(3).trim())
|
|
275
|
+
else if (t === '' || t.startsWith('//')) continue
|
|
276
|
+
else break
|
|
277
|
+
}
|
|
278
|
+
const header = headerLines.join(' ').trim()
|
|
279
|
+
|
|
280
|
+
// Exposure-атрибути: рядки, після яких fn стає фактично pub
|
|
281
|
+
const srcLines = src.split('\n')
|
|
282
|
+
const exposedLineSet = new Set()
|
|
283
|
+
for (const m of src.matchAll(RS_EXPOSURE_ATTR_RE)) {
|
|
284
|
+
// Знаходимо, який рядок містить цей атрибут
|
|
285
|
+
let pos = 0
|
|
286
|
+
for (let li = 0; li < srcLines.length; li++) {
|
|
287
|
+
if (pos + srcLines[li].length >= m.index) {
|
|
288
|
+
// Шукаємо наступний не-атрибутний рядок з fn
|
|
289
|
+
for (let nli = li + 1; nli < Math.min(li + 5, srcLines.length); nli++) {
|
|
290
|
+
const t = srcLines[nli].trim()
|
|
291
|
+
if (t.startsWith('#[') || t === '') continue
|
|
292
|
+
if (/^(?:pub\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+/.test(t)) exposedLineSet.add(nli)
|
|
293
|
+
break
|
|
294
|
+
}
|
|
295
|
+
break
|
|
296
|
+
}
|
|
297
|
+
pos += srcLines[li].length + 1
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// exports — pub items + exposure-exposed fns
|
|
302
|
+
const exports = []
|
|
303
|
+
let lineOffset = 0
|
|
304
|
+
for (let li = 0; li < srcLines.length; li++) {
|
|
305
|
+
const line = srcLines[li]
|
|
306
|
+
const m = line.match(/^[ \t]*(pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|type)\s+(\w+)/)
|
|
307
|
+
if (m) {
|
|
308
|
+
const isPub = Boolean(m[1]) || exposedLineSet.has(li)
|
|
309
|
+
if (isPub) {
|
|
310
|
+
const desc = rsDocBefore(srcLines, li)
|
|
311
|
+
exports.push({ name: m[3], kind: m[2], desc })
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
lineOffset += line.length + 1
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// localSymbols — приватні fn (не pub і не exposed) — не документуємо як публічний API
|
|
318
|
+
const localSymbols = []
|
|
319
|
+
for (let li = 0; li < srcLines.length; li++) {
|
|
320
|
+
const line = srcLines[li]
|
|
321
|
+
const m = line.match(/^[ \t]*(?:async\s+)?(?:unsafe\s+)?fn\s+(\w+)/)
|
|
322
|
+
if (m && !exports.some(e => e.name === m[1])) localSymbols.push(m[1])
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// imports — use-рядки, класифіковані на std / external / internal
|
|
326
|
+
const stdlib = new Set()
|
|
327
|
+
const external = new Set()
|
|
328
|
+
for (const m of src.matchAll(RS_USE_RE)) {
|
|
329
|
+
const path = m[1]
|
|
330
|
+
const root = path.split('::')[0]
|
|
331
|
+
if (root === 'std' || root === 'core' || root === 'alloc') stdlib.add(path)
|
|
332
|
+
else external.add(path)
|
|
333
|
+
}
|
|
334
|
+
const imports = { stdlib: [...stdlib], external: [...external], internal: [] }
|
|
335
|
+
|
|
336
|
+
// markers
|
|
337
|
+
const markers = {
|
|
338
|
+
readOnly: !RS_WRITE_RE.test(src),
|
|
339
|
+
catchesErrors: RS_CATCH_RE.test(src),
|
|
340
|
+
returnsFalsyOnFail: RS_RESULT_RE.test(src),
|
|
341
|
+
network: RS_NETWORK_RE.test(src),
|
|
342
|
+
caches: RS_CACHE_RE.test(src),
|
|
343
|
+
skips: []
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return { relPath, lang: 'rs', header, exports, imports, internalSymbols: [], localSymbols, markers }
|
|
347
|
+
}
|
|
348
|
+
|
|
214
349
|
/**
|
|
215
350
|
* Головний екстрактор: код файлу → факт-лист.
|
|
216
351
|
* @param {string} src вміст файлу
|
|
@@ -219,6 +354,7 @@ function extractMarkers(src) {
|
|
|
219
354
|
*/
|
|
220
355
|
export function extractFacts(src, relPath) {
|
|
221
356
|
const lang = relPath.split('.').pop()
|
|
357
|
+
if (lang === 'rs') return extractFactsRust(src, relPath)
|
|
222
358
|
if (!['js', 'mjs', 'ts'].includes(lang)) {
|
|
223
359
|
return { relPath, lang, unsupported: true, header: '', exports: [], imports: {}, markers: {} }
|
|
224
360
|
}
|
|
@@ -32,7 +32,7 @@ function factsSummary(facts) {
|
|
|
32
32
|
if (m.skips?.length) lines.push(`Свідомо пропускає шляхи: ${m.skips.join(', ')}`)
|
|
33
33
|
lines.push(`Read-only: ${m.readOnly ? 'так' : 'ні'}`)
|
|
34
34
|
if (m.catchesErrors) lines.push('Перехоплює помилки (fail-safe), не кидає винятків назовні')
|
|
35
|
-
if (m.returnsFalsyOnFail) lines.push('За невдачі повертає false/null замість винятку')
|
|
35
|
+
if (m.returnsFalsyOnFail) lines.push('За невдачі повертає значення помилки (false/null/Err) замість винятку чи паніки')
|
|
36
36
|
lines.push(m.caches ? 'Кешування: так, у межах прогону' : 'Кешування: НЕМАЄ — не згадуй кеш у гарантіях')
|
|
37
37
|
if (m.network) lines.push('Звертається до мережі')
|
|
38
38
|
else lines.push('Робота з мережею: немає')
|
|
@@ -195,7 +195,7 @@ export function guaranteesFromMarkers(facts) {
|
|
|
195
195
|
const lines = []
|
|
196
196
|
if (m.readOnly) lines.push('- Read-only: файл не виконує операцій запису у файлову систему.')
|
|
197
197
|
if (m.catchesErrors) lines.push('- Перехоплює помилки і не пропускає винятків назовні (fail-safe).')
|
|
198
|
-
if (m.returnsFalsyOnFail) lines.push('- За
|
|
198
|
+
if (m.returnsFalsyOnFail) lines.push('- За невдачі повертає значення помилки (`false`/`null`/`Err`) замість генерування винятку чи паніки.')
|
|
199
199
|
if (m.caches) lines.push('- Кешує результати в межах одного прогону.')
|
|
200
200
|
if (m.skips?.length) {
|
|
201
201
|
lines.push(`- Свідомо пропускає шляхи: ${m.skips.map(s => '`' + s + '`').join(', ')}.`)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** @see ./docs/docgen-scan.md */
|
|
2
|
-
|
|
3
|
-
import path from 'node:path'
|
|
2
|
+
import { join, dirname, basename, extname, relative, resolve, sep, isAbsolute, posix } from 'node:path'
|
|
4
3
|
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
5
4
|
import { execFileSync } from 'node:child_process'
|
|
6
5
|
import { once } from 'node:events'
|
|
@@ -27,7 +26,7 @@ const DEFAULT_GATE_MAX = Number(env.N_CURSOR_DOC_FILES_GATE_MAX ?? 50) || 50
|
|
|
27
26
|
* @returns {boolean} true — корінь system-wide docs
|
|
28
27
|
*/
|
|
29
28
|
function isSystemWideDocsRoot(root) {
|
|
30
|
-
return existsSync(
|
|
29
|
+
return existsSync(join(root, 'docs', 'adr')) || existsSync(join(root, 'docs', 'explanation'))
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
/**
|
|
@@ -38,7 +37,7 @@ function isSystemWideDocsRoot(root) {
|
|
|
38
37
|
export function isSourceFile(fileName) {
|
|
39
38
|
if (fileName.endsWith('.d.ts')) return false
|
|
40
39
|
if (TEST_FILE_RE.test(fileName)) return false
|
|
41
|
-
return SOURCE_EXTENSIONS.has(
|
|
40
|
+
return SOURCE_EXTENSIONS.has(extname(fileName))
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
/**
|
|
@@ -48,9 +47,9 @@ export function isSourceFile(fileName) {
|
|
|
48
47
|
* @returns {string} шлях до `<dir>/docs/<stem>.md` у тому ж просторі шляхів
|
|
49
48
|
*/
|
|
50
49
|
export function docPathForSource(sourcePath) {
|
|
51
|
-
const dir =
|
|
52
|
-
const stem =
|
|
53
|
-
return
|
|
50
|
+
const dir = dirname(sourcePath)
|
|
51
|
+
const stem = basename(sourcePath, extname(sourcePath))
|
|
52
|
+
return join(dir, 'docs', `${stem}.md`)
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
/**
|
|
@@ -61,9 +60,9 @@ export function docPathForSource(sourcePath) {
|
|
|
61
60
|
* @returns {boolean} true — кандидат на доку
|
|
62
61
|
*/
|
|
63
62
|
export function isDocCandidate(root, relPath) {
|
|
64
|
-
const fileName =
|
|
63
|
+
const fileName = posix.basename(relPath)
|
|
65
64
|
if (!isSourceFile(fileName)) return false
|
|
66
|
-
if (isSystemWideDocsRoot(root) &&
|
|
65
|
+
if (isSystemWideDocsRoot(root) && posix.dirname(relPath) === '.') return false
|
|
67
66
|
return !isDocgenIgnored(relPath)
|
|
68
67
|
}
|
|
69
68
|
|
|
@@ -75,7 +74,7 @@ export function isDocCandidate(root, relPath) {
|
|
|
75
74
|
*/
|
|
76
75
|
export function describeFile(root, sourcePath) {
|
|
77
76
|
const docPath = docPathForSource(sourcePath)
|
|
78
|
-
const { stale, reason } = staleness(
|
|
77
|
+
const { stale, reason } = staleness(join(root, sourcePath), join(root, docPath))
|
|
79
78
|
return { sourcePath, docPath, stale, reason }
|
|
80
79
|
}
|
|
81
80
|
|
|
@@ -97,14 +96,14 @@ export function scanForDocFiles(root) {
|
|
|
97
96
|
return
|
|
98
97
|
}
|
|
99
98
|
for (const entry of entries) {
|
|
100
|
-
const fullPath =
|
|
101
|
-
const relPath =
|
|
99
|
+
const fullPath = join(dir, entry.name)
|
|
100
|
+
const relPath = relative(root, fullPath)
|
|
102
101
|
if (entry.isDirectory()) {
|
|
103
102
|
if (isDocgenIgnored(relPath, 'dir')) continue
|
|
104
103
|
walk(fullPath)
|
|
105
104
|
} else if (entry.isFile() && isSourceFile(entry.name)) {
|
|
106
|
-
if (isSystemWideDocsRoot(root) &&
|
|
107
|
-
const sourcePath = relPath.split(
|
|
105
|
+
if (isSystemWideDocsRoot(root) && dirname(relPath) === '.') continue
|
|
106
|
+
const sourcePath = relPath.split(sep).join('/')
|
|
108
107
|
if (isDocgenIgnored(sourcePath)) continue
|
|
109
108
|
results.push(describeFile(root, sourcePath))
|
|
110
109
|
}
|
|
@@ -122,7 +121,7 @@ export function scanForDocFiles(root) {
|
|
|
122
121
|
*/
|
|
123
122
|
export function resolveRoot(argv) {
|
|
124
123
|
const i = argv.indexOf('--root')
|
|
125
|
-
return i !== -1 && argv[i + 1] ?
|
|
124
|
+
return i !== -1 && argv[i + 1] ? resolve(argv[i + 1]) : process.cwd()
|
|
126
125
|
}
|
|
127
126
|
|
|
128
127
|
/**
|
|
@@ -190,7 +189,7 @@ function gitChangedSources(root) {
|
|
|
190
189
|
return out
|
|
191
190
|
.split('\n')
|
|
192
191
|
.map(s => s.trim())
|
|
193
|
-
.filter(rel => rel && isDocCandidate(root, rel) && existsSync(
|
|
192
|
+
.filter(rel => rel && isDocCandidate(root, rel) && existsSync(join(root, rel)))
|
|
194
193
|
}
|
|
195
194
|
|
|
196
195
|
/**
|
|
@@ -200,9 +199,9 @@ function gitChangedSources(root) {
|
|
|
200
199
|
* @returns {string|null} posix-шлях від кореня
|
|
201
200
|
*/
|
|
202
201
|
function toRelSource(root, candidate) {
|
|
203
|
-
const rel =
|
|
204
|
-
if (rel.startsWith('..') ||
|
|
205
|
-
return rel.split(
|
|
202
|
+
const rel = relative(root, resolve(root, candidate))
|
|
203
|
+
if (rel.startsWith('..') || isAbsolute(rel)) return null
|
|
204
|
+
return rel.split(sep).join('/')
|
|
206
205
|
}
|
|
207
206
|
|
|
208
207
|
/**
|
|
@@ -216,7 +215,7 @@ function runDegradedReport(root) {
|
|
|
216
215
|
const degraded = []
|
|
217
216
|
for (const f of scanForDocFiles(root)) {
|
|
218
217
|
if (f.stale) continue
|
|
219
|
-
const { score, issues } = readDocQuality(
|
|
218
|
+
const { score, issues } = readDocQuality(join(root, f.docPath))
|
|
220
219
|
if (score !== null && score < QUALITY_THRESHOLD) degraded.push({ ...f, score, issues })
|
|
221
220
|
}
|
|
222
221
|
if (degraded.length === 0) {
|
|
@@ -262,14 +261,14 @@ export async function runDocFilesCheckCli(argv) {
|
|
|
262
261
|
if (hookMode) {
|
|
263
262
|
const fp = extractHookFilePath(await readStdin())
|
|
264
263
|
const rel = fp ? toRelSource(root, fp) : null
|
|
265
|
-
sources = rel && isDocCandidate(root, rel) && existsSync(
|
|
264
|
+
sources = rel && isDocCandidate(root, rel) && existsSync(join(root, rel)) ? [rel] : []
|
|
266
265
|
} else if (gitMode) {
|
|
267
266
|
sources = gitChangedSources(root)
|
|
268
267
|
} else {
|
|
269
268
|
sources = argv
|
|
270
269
|
.filter(a => !a.startsWith('--') && a !== argv[maxIdx + 1])
|
|
271
270
|
.map(a => toRelSource(root, a))
|
|
272
|
-
.filter(rel => rel && isDocCandidate(root, rel) && existsSync(
|
|
271
|
+
.filter(rel => rel && isDocCandidate(root, rel) && existsSync(join(root, rel)))
|
|
273
272
|
}
|
|
274
273
|
|
|
275
274
|
const stale = sources.map(src => describeFile(root, src)).filter(f => f.stale)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
docgen:
|
|
3
|
+
source: npm/skills/doc-files/js/units-rs.mjs
|
|
4
|
+
crc: 11099381
|
|
5
|
+
score: 100
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# units-rs.mjs
|
|
9
|
+
|
|
10
|
+
## Огляд
|
|
11
|
+
|
|
12
|
+
Файл аналізує рядки для визначення структури та елементів модуля. Він ітерує по рядках для визначення глибини, обробляє рядкові літерали для визначення зміщення, змінює глибину при зустрічі відкриваючої чи закриваючої фігу, видаляє відкриті блоки зі стека, перевіряє наявність атрибутів експонування, визначає декларації на верхньому рівні чи в модулях, а також витягує тіло функції та збирає виклики інших юнітів у тілі функції.
|
|
13
|
+
|
|
14
|
+
## Поведінка
|
|
15
|
+
|
|
16
|
+
1. Скан файлу по рядках.
|
|
17
|
+
2. Ітерація по рядках для визначення глибини.
|
|
18
|
+
3. Обробка рядкових літералів для визначення зміщення.
|
|
19
|
+
4. Зміна глибини при зустрічі відкриваючої фігу.
|
|
20
|
+
5. Зміна глибини при зустрічі закриваючої фігу.
|
|
21
|
+
6. Видалення відкритих блоків з стека.
|
|
22
|
+
7. Перевірка наявності атрибутів експонування.
|
|
23
|
+
8. Визначення декларацій на верхньому рівні або в модулях.
|
|
24
|
+
9. Визначення публічних функцій, структур, перерахувань, трейтів, типів.
|
|
25
|
+
10. Витягування тіла функції через пошук закриваючої фігу.
|
|
26
|
+
11. Збір викликів інших юнітів у тілі функції.
|
|
27
|
+
|
|
28
|
+
## Публічний API
|
|
29
|
+
|
|
30
|
+
extractUnitsRs — Визначає top-level і impl-методи через підрахунок дужок по рядках. Обмеження: рядкові літерали з `{`/`}` всередині `{}` можуть дати хибну глибину.
|
|
31
|
+
|
|
32
|
+
## Гарантії поведінки
|
|
33
|
+
|
|
34
|
+
- Read-only: файл не виконує операцій запису у файлову систему.
|
|
35
|
+
- Не звертається до мережі.
|
|
@@ -0,0 +1,213 @@
|
|
|
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] === '\\') { i += 2; continue }
|
|
13
|
+
if (src[i] === '"') return i + 1
|
|
14
|
+
i++
|
|
15
|
+
}
|
|
16
|
+
return i
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Знаходить індекс закриваючої `}` для відкриваючої `{` на позиції `start`.
|
|
21
|
+
* Правильно пропускає рядки/блочні коментарі та рядкові літерали.
|
|
22
|
+
* @param {string} src вміст файлу
|
|
23
|
+
* @param {number} start позиція відкриваючої `{`
|
|
24
|
+
* @returns {number} індекс `}` або -1, якщо не знайдено
|
|
25
|
+
*/
|
|
26
|
+
function findClosingBrace(src, start) {
|
|
27
|
+
let depth = 0
|
|
28
|
+
let i = start
|
|
29
|
+
while (i < src.length) {
|
|
30
|
+
const ch = src[i]
|
|
31
|
+
if (ch === '/' && src[i + 1] === '/') {
|
|
32
|
+
const nl = src.indexOf('\n', i)
|
|
33
|
+
i = nl === -1 ? src.length : nl + 1
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (ch === '/' && src[i + 1] === '*') {
|
|
37
|
+
const end = src.indexOf('*/', i + 2)
|
|
38
|
+
i = end === -1 ? src.length : end + 2
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
if (ch === '"') { i = skipString(src, i); continue }
|
|
42
|
+
if (ch === '{') { depth++; i++; continue }
|
|
43
|
+
if (ch === '}') {
|
|
44
|
+
depth--
|
|
45
|
+
if (depth === 0) return i
|
|
46
|
+
i++
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
i++
|
|
50
|
+
}
|
|
51
|
+
return -1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Видобуває `///` doc-рядки безпосередньо перед рядком `lineIdx`.
|
|
56
|
+
* Сканує назад через `///`, `#[...]` та пусті рядки.
|
|
57
|
+
* @param {string[]} lines рядки файлу
|
|
58
|
+
* @param {number} lineIdx рядок декларації
|
|
59
|
+
* @returns {string} склеєний опис або ''
|
|
60
|
+
*/
|
|
61
|
+
function docBefore(lines, lineIdx) {
|
|
62
|
+
const doc = []
|
|
63
|
+
for (let i = lineIdx - 1; i >= 0; i--) {
|
|
64
|
+
const t = lines[i].trim()
|
|
65
|
+
if (t.startsWith('///')) {
|
|
66
|
+
doc.unshift(t.slice(3).trim())
|
|
67
|
+
} else if (t.startsWith('#[') || t.startsWith('#![') || t === '') {
|
|
68
|
+
// пропустити атрибути та пусті рядки
|
|
69
|
+
} else {
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return doc.join(' ').trim()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Pub-items: pub fn / pub struct / pub enum / pub trait / pub type
|
|
77
|
+
// Також ловить fn без pub (для localSymbols і impl-методів)
|
|
78
|
+
const ITEM_RE = /^[ \t]*(pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|type)\s+(\w+)/
|
|
79
|
+
|
|
80
|
+
// impl Type { або impl<T> Trait for Type {
|
|
81
|
+
const IMPL_RE = /^[ \t]*(?:pub\s+)?impl(?:<[^>]*>)?\s+(?:(?:\w[\w:<>, ]*\s+for\s+))?(\w+)/
|
|
82
|
+
|
|
83
|
+
// Підозрілі exposure-атрибути, що роблять непуб-fn фактично публічними
|
|
84
|
+
const EXPOSURE_ATTR_RE = /#\[(?:tauri::command|wasm_bindgen|uniffi::export|pyo3::pyfunction|napi)/
|
|
85
|
+
|
|
86
|
+
// Базовий виклик fn-імені (для call-graph всередині юніта)
|
|
87
|
+
const CALL_RE = /\b([a-z_]\w*)\s*\(/g
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Юніт-екстрактор для `.rs` файлів.
|
|
91
|
+
* Визначає top-level і impl-методи через підрахунок дужок по рядках.
|
|
92
|
+
* Відомі обмеження: рядкові літерали з `{`/`}` всередині `{}` можуть дати
|
|
93
|
+
* хибну глибину (рідкісно в реальному Rust-коді з rustfmt).
|
|
94
|
+
* @param {string} src вміст файлу
|
|
95
|
+
* @param {string} [_relPath] резервний (не використовується)
|
|
96
|
+
* @returns {Array<{name:string, kind:string, exported:boolean, implName:string|null, span:{start:number,end:number}, body:string, calls:string[], doc:string}>|null}
|
|
97
|
+
*/
|
|
98
|
+
export function extractUnitsRs(src, _relPath) {
|
|
99
|
+
const lines = src.split('\n')
|
|
100
|
+
const units = []
|
|
101
|
+
let depth = 0
|
|
102
|
+
let lineOffset = 0
|
|
103
|
+
// Стек відкритих impl: { typeName, openDepth }
|
|
104
|
+
const implStack = []
|
|
105
|
+
// Флаг: наступний fn отримує exposure (через #[tauri::command] тощо)
|
|
106
|
+
let nextFnExposed = false
|
|
107
|
+
|
|
108
|
+
for (let li = 0; li < lines.length; li++) {
|
|
109
|
+
const line = lines[li]
|
|
110
|
+
const depthAtStart = depth
|
|
111
|
+
|
|
112
|
+
// Підраховуємо `{` і `}` в рядку (пропускаємо рядкові коментарі та рядки)
|
|
113
|
+
let j = 0
|
|
114
|
+
while (j < line.length) {
|
|
115
|
+
const ch = line[j]
|
|
116
|
+
if (ch === '/' && line[j + 1] === '/') break
|
|
117
|
+
if (ch === '"') {
|
|
118
|
+
j++
|
|
119
|
+
while (j < line.length && line[j] !== '"') {
|
|
120
|
+
if (line[j] === '\\') j++
|
|
121
|
+
j++
|
|
122
|
+
}
|
|
123
|
+
j++
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
if (ch === '{') depth++
|
|
127
|
+
else if (ch === '}') depth--
|
|
128
|
+
j++
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Закриті impl-блоки прибираємо зі стека
|
|
132
|
+
while (implStack.length > 0 && implStack[implStack.length - 1].openDepth > depth) {
|
|
133
|
+
implStack.pop()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const currentImpl = implStack.at(-1)?.typeName ?? null
|
|
137
|
+
|
|
138
|
+
// Перевіряємо exposure-атрибути
|
|
139
|
+
if (EXPOSURE_ATTR_RE.test(line)) {
|
|
140
|
+
nextFnExposed = true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Impl-декларація (зазвичай глибина 0, але може бути в mod)
|
|
144
|
+
if (depthAtStart <= 1) {
|
|
145
|
+
const implM = line.match(IMPL_RE)
|
|
146
|
+
if (implM && line.includes('{')) {
|
|
147
|
+
implStack.push({ typeName: implM[1], openDepth: depth })
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Елементи на глибині 0 (top-level) і 1 (всередині impl)
|
|
152
|
+
if (depthAtStart <= 1) {
|
|
153
|
+
const m = line.match(ITEM_RE)
|
|
154
|
+
if (m) {
|
|
155
|
+
const isPub = Boolean(m[1]) || (m[2] === 'fn' && nextFnExposed)
|
|
156
|
+
if (m[2] === 'fn') nextFnExposed = false
|
|
157
|
+
const kind = m[2]
|
|
158
|
+
const name = m[3]
|
|
159
|
+
const doc = docBefore(lines, li)
|
|
160
|
+
|
|
161
|
+
// Витягуємо тіло через findClosingBrace для fn/struct/enum/trait
|
|
162
|
+
let body = ''
|
|
163
|
+
let itemEnd = lineOffset + line.length
|
|
164
|
+
if (kind !== 'type') {
|
|
165
|
+
const openBraceIdx = src.indexOf('{', lineOffset)
|
|
166
|
+
// Шукаємо `{` не далі ніж через 3 рядки від початку декларації
|
|
167
|
+
const threeLines = lines.slice(li, li + 3).join('\n').length
|
|
168
|
+
if (openBraceIdx !== -1 && openBraceIdx - lineOffset <= threeLines) {
|
|
169
|
+
const closeIdx = findClosingBrace(src, openBraceIdx)
|
|
170
|
+
if (closeIdx !== -1) {
|
|
171
|
+
itemEnd = closeIdx + 1
|
|
172
|
+
body = src.slice(lineOffset, itemEnd)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
units.push({
|
|
178
|
+
name,
|
|
179
|
+
kind,
|
|
180
|
+
exported: isPub,
|
|
181
|
+
implName: depthAtStart === 1 ? currentImpl : null,
|
|
182
|
+
span: { start: lineOffset, end: itemEnd },
|
|
183
|
+
body,
|
|
184
|
+
calls: [],
|
|
185
|
+
doc
|
|
186
|
+
})
|
|
187
|
+
} else {
|
|
188
|
+
// Рядок не є item — скидаємо exposure-флаг якщо не атрибут
|
|
189
|
+
const t = line.trim()
|
|
190
|
+
if (!t.startsWith('#[') && !t.startsWith('#![') && !t.startsWith('///') && t !== '') {
|
|
191
|
+
nextFnExposed = false
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
lineOffset += line.length + 1 // +1 для '\n'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Базовий call-graph: виклики інших юнітів цього файлу
|
|
200
|
+
const unitNames = new Set(units.map(u => u.name))
|
|
201
|
+
for (const u of units) {
|
|
202
|
+
if (!u.body) continue
|
|
203
|
+
const calls = new Set()
|
|
204
|
+
let cm
|
|
205
|
+
const re = new RegExp(CALL_RE.source, 'g')
|
|
206
|
+
while ((cm = re.exec(u.body)) !== null) {
|
|
207
|
+
if (unitNames.has(cm[1]) && cm[1] !== u.name) calls.add(cm[1])
|
|
208
|
+
}
|
|
209
|
+
u.calls = [...calls]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return units.length > 0 ? units : null
|
|
213
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/** @see ./docs/units.md */
|
|
2
2
|
|
|
3
3
|
import { extractUnitsJs } from './units-js.mjs'
|
|
4
|
+
import { extractUnitsRs } from './units-rs.mjs'
|
|
4
5
|
|
|
5
6
|
const JS_EXT = new Set(['js', 'mjs', 'ts', 'jsx', 'tsx', 'cts', 'mts'])
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Мовно-агностичний фасад
|
|
9
|
-
* js/mjs/ts → oxc; vue/py
|
|
10
|
-
* відкочується на whole-file шлях, як і раніше).
|
|
9
|
+
* Мовно-агностичний фасад юніт-шару. Диспатчить за розширенням:
|
|
10
|
+
* js/mjs/ts → oxc AST; rs → regex+brace-counting; vue/py → null (whole-file шлях).
|
|
11
11
|
* @param {string} src вміст файлу
|
|
12
12
|
* @param {string} relPath шлях файлу
|
|
13
13
|
* @returns {Array<object>|null} юніти або null, якщо мова ще не підтримана / файл не парситься
|
|
@@ -15,5 +15,6 @@ const JS_EXT = new Set(['js', 'mjs', 'ts', 'jsx', 'tsx', 'cts', 'mts'])
|
|
|
15
15
|
export function extractUnits(src, relPath) {
|
|
16
16
|
const ext = (relPath.split('.').pop() || '').toLowerCase()
|
|
17
17
|
if (JS_EXT.has(ext)) return extractUnitsJs(src, relPath)
|
|
18
|
+
if (ext === 'rs') return extractUnitsRs(src, relPath)
|
|
18
19
|
return null
|
|
19
20
|
}
|