canship 0.7.1 → 0.8.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.
- package/README-zh-CN.md +58 -141
- package/README.md +58 -141
- package/dist/cli.js +3809 -665
- package/dist/index.d.ts +78 -8
- package/dist/index.js +1127 -72
- package/docs/framework-support-zh-CN.md +95 -0
- package/docs/framework-support.md +95 -0
- package/docs/reference-zh-CN.md +221 -0
- package/docs/reference.md +221 -0
- package/package.json +13 -2
- package/schemas/config-v1.schema.json +114 -0
- package/schemas/scan-report-v1.schema.json +19 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// <define:__CANSHIP_BUILD_INFO__>
|
|
4
|
+
var define_CANSHIP_BUILD_INFO_default = { version: "0.8.0", channel: "release", revision: "0700a3d119573b1d13c5f5b60f4e93ab87244b35", dirty: false };
|
|
5
|
+
|
|
3
6
|
// src/index.ts
|
|
4
7
|
import { statSync as statSync3 } from "fs";
|
|
5
8
|
import { resolve as resolve2 } from "path";
|
|
@@ -749,7 +752,7 @@ function listViaGit(root, gitExecutable) {
|
|
|
749
752
|
return null;
|
|
750
753
|
}
|
|
751
754
|
}
|
|
752
|
-
function walkTree(root, skipped, wantAll, maxEntries) {
|
|
755
|
+
function walkTree(root, skipped, wantAll, maxEntries, excludePath) {
|
|
753
756
|
const all = [];
|
|
754
757
|
const found = [];
|
|
755
758
|
let visited = 0;
|
|
@@ -793,9 +796,10 @@ function walkTree(root, skipped, wantAll, maxEntries) {
|
|
|
793
796
|
visited++;
|
|
794
797
|
const full = join2(dir, entry.name);
|
|
795
798
|
const rel = relative2(root, full).split(sep2).join("/");
|
|
796
|
-
|
|
799
|
+
if (excludePath?.(rel)) continue;
|
|
800
|
+
let metadata2;
|
|
797
801
|
try {
|
|
798
|
-
|
|
802
|
+
metadata2 = lstatSync2(full);
|
|
799
803
|
} catch (err) {
|
|
800
804
|
if (!isMissing(err)) skipped.push({
|
|
801
805
|
path: rel,
|
|
@@ -804,18 +808,18 @@ function walkTree(root, skipped, wantAll, maxEntries) {
|
|
|
804
808
|
});
|
|
805
809
|
continue;
|
|
806
810
|
}
|
|
807
|
-
if (
|
|
811
|
+
if (metadata2.isSymbolicLink()) {
|
|
808
812
|
if (!SKIP_DIRS.has(entry.name)) {
|
|
809
813
|
skipped.push({ path: rel, reason: "symlink", detail: "symbolic links are not followed" });
|
|
810
814
|
}
|
|
811
815
|
continue;
|
|
812
816
|
}
|
|
813
|
-
if (
|
|
817
|
+
if (metadata2.isDirectory()) {
|
|
814
818
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
815
819
|
walk(full, depth + 1);
|
|
816
820
|
continue;
|
|
817
821
|
}
|
|
818
|
-
if (!
|
|
822
|
+
if (!metadata2.isFile()) continue;
|
|
819
823
|
if (wantAll) all.push(rel);
|
|
820
824
|
if (isCredentialName(entry.name) || isWorthProbing(rel)) found.push(rel);
|
|
821
825
|
}
|
|
@@ -902,16 +906,17 @@ function readablePath(root, path, skipped) {
|
|
|
902
906
|
}
|
|
903
907
|
return true;
|
|
904
908
|
}
|
|
905
|
-
function collectFiles(root, isGitRepo, gitExecutable = resolveGitExecutable(root), limits = {}, honorIgnoreMarkers = true) {
|
|
909
|
+
function collectFiles(root, isGitRepo, gitExecutable = resolveGitExecutable(root), limits = {}, honorIgnoreMarkers = true, excludePath) {
|
|
906
910
|
root = realpathSync2(root);
|
|
907
911
|
const skipped = [];
|
|
908
912
|
const ignored = [];
|
|
909
913
|
const fromGit = isGitRepo ? listViaGit(root, gitExecutable) : null;
|
|
910
|
-
const walked = walkTree(root, skipped, fromGit === null, limits.maxEntries ?? MAX_WALK_ENTRIES);
|
|
914
|
+
const walked = walkTree(root, skipped, fromGit === null, limits.maxEntries ?? MAX_WALK_ENTRIES, excludePath);
|
|
911
915
|
const listed = fromGit?.files ?? walked.all;
|
|
912
916
|
const candidates = /* @__PURE__ */ new Set();
|
|
913
917
|
let vendored = 0;
|
|
914
918
|
for (const path of fromGit?.nestedRepositories ?? []) {
|
|
919
|
+
if (excludePath?.(path)) continue;
|
|
915
920
|
if (isVendored(path)) {
|
|
916
921
|
vendored++;
|
|
917
922
|
continue;
|
|
@@ -923,11 +928,13 @@ function collectFiles(root, isGitRepo, gitExecutable = resolveGitExecutable(root
|
|
|
923
928
|
});
|
|
924
929
|
}
|
|
925
930
|
for (const path of listed) {
|
|
931
|
+
if (excludePath?.(path)) continue;
|
|
926
932
|
if (isVendored(path)) vendored++;
|
|
927
933
|
else candidates.add(path);
|
|
928
934
|
}
|
|
929
935
|
const forced = /* @__PURE__ */ new Set();
|
|
930
936
|
for (const hidden of walked.forced) {
|
|
937
|
+
if (excludePath?.(hidden)) continue;
|
|
931
938
|
candidates.add(hidden);
|
|
932
939
|
forced.add(hidden);
|
|
933
940
|
}
|
|
@@ -1870,7 +1877,7 @@ function gitOrThrow(root, gitExecutable, args) {
|
|
|
1870
1877
|
return out;
|
|
1871
1878
|
}
|
|
1872
1879
|
function repoPrefix(root, gitExecutable) {
|
|
1873
|
-
return (
|
|
1880
|
+
return gitOrThrow(root, gitExecutable, ["rev-parse", "--show-prefix"]).trim();
|
|
1874
1881
|
}
|
|
1875
1882
|
function trackedEnvFiles(root, gitExecutable) {
|
|
1876
1883
|
const out = gitOrThrow(root, gitExecutable, ["ls-files", "-z"]);
|
|
@@ -1979,6 +1986,7 @@ var gitleakRule = {
|
|
|
1979
1986
|
const remoteNote = remote ? `This repository has a remote configured, so these commits have most likely been pushed. Bots scrape public commits within minutes \u2014 assume every key in this file is already in someone else's hands.` : `This repository has no remote yet, so the damage may still be contained. Fix it before you push.`;
|
|
1980
1987
|
const reportedTracked = /* @__PURE__ */ new Set();
|
|
1981
1988
|
for (const path of tracked) {
|
|
1989
|
+
if (ctx.excludePath?.(path)) continue;
|
|
1982
1990
|
const scanned = ctx.files.find((f) => f.path === path);
|
|
1983
1991
|
const evidence = scanned ? evidenceIn(scanned.lines) : "hint";
|
|
1984
1992
|
if (evidence === "none") continue;
|
|
@@ -2009,6 +2017,7 @@ var gitleakRule = {
|
|
|
2009
2017
|
for (const entry of historical) {
|
|
2010
2018
|
if (reportedTracked.has(entry.localPath)) continue;
|
|
2011
2019
|
const path = entry.localPath;
|
|
2020
|
+
if (ctx.excludePath?.(path)) continue;
|
|
2012
2021
|
const stillTracked = tracked.has(entry.localPath);
|
|
2013
2022
|
const history = historicalEvidence(ctx.root, ctx.gitExecutable, entry);
|
|
2014
2023
|
if (history && history.unread > 0) {
|
|
@@ -3092,7 +3101,7 @@ var firebaseRulesRule = {
|
|
|
3092
3101
|
};
|
|
3093
3102
|
|
|
3094
3103
|
// src/rules/apiauth.ts
|
|
3095
|
-
import { posix } from "path";
|
|
3104
|
+
import { posix as posix2 } from "path";
|
|
3096
3105
|
|
|
3097
3106
|
// src/rules/bindings.ts
|
|
3098
3107
|
var cache = /* @__PURE__ */ new WeakMap();
|
|
@@ -3464,12 +3473,364 @@ function identityRequirement(value, allowParameters) {
|
|
|
3464
3473
|
return null;
|
|
3465
3474
|
}
|
|
3466
3475
|
|
|
3476
|
+
// src/rules/routers.ts
|
|
3477
|
+
import { createHash as createHash2 } from "crypto";
|
|
3478
|
+
|
|
3479
|
+
// src/rules/factory-modules.ts
|
|
3480
|
+
import { posix } from "path";
|
|
3481
|
+
var unknown = (limited2 = false) => ({ handled: true, file: null, ...limited2 ? { limited: true } : {} });
|
|
3482
|
+
var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3483
|
+
var safePath = (path) => Boolean(path) && path.length <= 512 && !/[\\\u0000-\u0020:%?#]/.test(path) && !posix.isAbsolute(path);
|
|
3484
|
+
var contained = (path) => path !== ".." && !path.startsWith("../") && !posix.isAbsolute(path);
|
|
3485
|
+
function metadata(file) {
|
|
3486
|
+
if (file.content.length > 65536) return null;
|
|
3487
|
+
let source = commentsMaskedOf(file);
|
|
3488
|
+
const code = noiseMaskedOf(file);
|
|
3489
|
+
const commas = [...code.matchAll(/,\s*[}\]]/g)].map((match) => match.index);
|
|
3490
|
+
if (commas.length) {
|
|
3491
|
+
const characters = source.split("");
|
|
3492
|
+
for (const at of commas) characters[at] = " ";
|
|
3493
|
+
source = characters.join("");
|
|
3494
|
+
}
|
|
3495
|
+
try {
|
|
3496
|
+
const value = JSON.parse(source);
|
|
3497
|
+
return object(value) ? value : null;
|
|
3498
|
+
} catch {
|
|
3499
|
+
return null;
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
function exportTarget(value, limit, depth = 0) {
|
|
3503
|
+
if (depth >= 8) {
|
|
3504
|
+
limit();
|
|
3505
|
+
return null;
|
|
3506
|
+
}
|
|
3507
|
+
if (typeof value === "string") return value.startsWith("./") && safePath(value) && !value.includes("*") && value.split("/").slice(1).every((part) => part !== "." && part !== ".." && part !== "node_modules") ? value : null;
|
|
3508
|
+
if (!object(value)) return null;
|
|
3509
|
+
if (Object.keys(value).length > 32) {
|
|
3510
|
+
limit();
|
|
3511
|
+
return null;
|
|
3512
|
+
}
|
|
3513
|
+
const entries = Object.entries(value).filter(([key]) => key !== "types" && !key.startsWith("types@"));
|
|
3514
|
+
if (!entries.length || entries.some(([key]) => key.startsWith("."))) return null;
|
|
3515
|
+
const targets = entries.map(([, item]) => exportTarget(item, limit, depth + 1));
|
|
3516
|
+
return targets.every((target) => target !== null && target === targets[0]) ? targets[0] : null;
|
|
3517
|
+
}
|
|
3518
|
+
function factoryModuleResolver(files) {
|
|
3519
|
+
const byPath = new Map(files.map((file) => [file.path, file]));
|
|
3520
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
3521
|
+
let metadataLimited = false;
|
|
3522
|
+
const json = (file) => {
|
|
3523
|
+
if (file.content.length > 65536) {
|
|
3524
|
+
metadataLimited = true;
|
|
3525
|
+
return null;
|
|
3526
|
+
}
|
|
3527
|
+
if (!parsed.has(file)) parsed.set(file, metadata(file));
|
|
3528
|
+
return parsed.get(file);
|
|
3529
|
+
};
|
|
3530
|
+
const parents = (file) => {
|
|
3531
|
+
const found = [];
|
|
3532
|
+
for (let dir = posix.dirname(file.path); ; dir = posix.dirname(dir)) {
|
|
3533
|
+
found.push(dir);
|
|
3534
|
+
if (dir === "." || posix.dirname(dir) === dir) break;
|
|
3535
|
+
}
|
|
3536
|
+
return found;
|
|
3537
|
+
};
|
|
3538
|
+
const nearest = (file, names) => {
|
|
3539
|
+
for (const dir of parents(file)) for (const name of names) {
|
|
3540
|
+
const found = byPath.get(posix.join(dir, name));
|
|
3541
|
+
if (found) return found;
|
|
3542
|
+
}
|
|
3543
|
+
return void 0;
|
|
3544
|
+
};
|
|
3545
|
+
const sourceAt = (path, from) => {
|
|
3546
|
+
if (!contained(path)) return null;
|
|
3547
|
+
const relative3 = posix.relative(posix.dirname(from.path), path);
|
|
3548
|
+
return bindingModule(relative3.startsWith(".") ? relative3 : "./" + relative3, from, files, "");
|
|
3549
|
+
};
|
|
3550
|
+
const configPath = (spec, from) => {
|
|
3551
|
+
const config = nearest(from, ["tsconfig.json", "jsconfig.json"]);
|
|
3552
|
+
if (!config) return null;
|
|
3553
|
+
if (config.content.length > 65536) return unknown(true);
|
|
3554
|
+
const data = json(config);
|
|
3555
|
+
if (!data || "extends" in data || "references" in data) return unknown();
|
|
3556
|
+
const options = data.compilerOptions;
|
|
3557
|
+
if (options === void 0) return null;
|
|
3558
|
+
if (!object(options)) return unknown();
|
|
3559
|
+
if (options.paths === void 0) return null;
|
|
3560
|
+
if (!object(options.paths)) return unknown();
|
|
3561
|
+
if (Object.keys(options.paths).length > 128) return unknown(true);
|
|
3562
|
+
const matches = [];
|
|
3563
|
+
for (const [pattern, target2] of Object.entries(options.paths)) {
|
|
3564
|
+
if (pattern.length > 512) return unknown(true);
|
|
3565
|
+
if (pattern.split("*").length > 2) return unknown();
|
|
3566
|
+
const [prefix, suffix = ""] = pattern.split("*");
|
|
3567
|
+
const exact = !pattern.includes("*");
|
|
3568
|
+
if (exact ? pattern !== spec : !spec.startsWith(prefix) || !spec.endsWith(suffix) || spec.length < prefix.length + suffix.length) continue;
|
|
3569
|
+
matches.push({ target: target2, prefix, suffix, exact, middle: exact ? "" : spec.slice(prefix.length, spec.length - suffix.length) });
|
|
3570
|
+
}
|
|
3571
|
+
if (!matches.length) return null;
|
|
3572
|
+
matches.sort((a, b) => Number(b.exact) - Number(a.exact) || b.prefix.length - a.prefix.length);
|
|
3573
|
+
const selected = matches[0];
|
|
3574
|
+
if (matches[1] && matches[1].exact === selected.exact && matches[1].prefix.length === selected.prefix.length) return unknown();
|
|
3575
|
+
if (!Array.isArray(selected.target) || selected.target.length !== 1 || typeof selected.target[0] !== "string") return unknown();
|
|
3576
|
+
const target = selected.target[0];
|
|
3577
|
+
if (!safePath(target) || target.split("*").length > 2 || selected.exact && target.includes("*")) return unknown();
|
|
3578
|
+
const base = options.baseUrl ?? ".";
|
|
3579
|
+
if (typeof base !== "string" || !safePath(base) || base.includes("*")) return unknown();
|
|
3580
|
+
const path = posix.normalize(posix.join(posix.dirname(config.path), base, target.replace("*", selected.middle)));
|
|
3581
|
+
return { handled: true, file: sourceAt(path, from) };
|
|
3582
|
+
};
|
|
3583
|
+
const manifests = files.filter((file) => /(?:^|\/)package\.json$/.test(file.path));
|
|
3584
|
+
const workspace = (spec, from) => {
|
|
3585
|
+
const name = spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0];
|
|
3586
|
+
const subpath = spec === name ? "." : "." + spec.slice(name.length);
|
|
3587
|
+
const owner = nearest(from, ["package.json"]);
|
|
3588
|
+
if (!owner) return null;
|
|
3589
|
+
if (owner.content.length > 65536) return unknown(true);
|
|
3590
|
+
const ownerData = json(owner);
|
|
3591
|
+
const deps = ownerData?.dependencies;
|
|
3592
|
+
const link = object(deps) ? deps[name] : void 0;
|
|
3593
|
+
if (typeof link !== "string" || !/^workspace:(?:\*|\^|~)$/.test(link)) return null;
|
|
3594
|
+
const roots = parents(owner).map((dir2) => byPath.get(posix.join(dir2, "package.json"))).filter((file) => Boolean(file));
|
|
3595
|
+
if (roots.some((file) => file.content.length > 65536)) return unknown(true);
|
|
3596
|
+
const root = roots.find((file) => json(file)?.workspaces !== void 0);
|
|
3597
|
+
if (!root) return unknown();
|
|
3598
|
+
const raw = json(root).workspaces;
|
|
3599
|
+
const patterns = Array.isArray(raw) ? raw : object(raw) ? raw.packages : null;
|
|
3600
|
+
if (!Array.isArray(patterns)) return unknown();
|
|
3601
|
+
if (patterns.length > 64) return unknown(true);
|
|
3602
|
+
if (patterns.some((item) => typeof item !== "string" || !safePath(item) || item.split("*").length > 2)) return unknown();
|
|
3603
|
+
const rootDir = posix.dirname(root.path);
|
|
3604
|
+
const matched = manifests.filter((file) => {
|
|
3605
|
+
const dir2 = posix.relative(rootDir, posix.dirname(file.path));
|
|
3606
|
+
return Boolean(dir2) && contained(dir2) && patterns.some((pattern) => {
|
|
3607
|
+
const [before, after = ""] = pattern.replace(/\/$/, "").split("*");
|
|
3608
|
+
return pattern.includes("*") ? dir2.length >= before.length + after.length && dir2.startsWith(before) && dir2.endsWith(after) && !dir2.slice(before.length, dir2.length - after.length).includes("/") : dir2 === before;
|
|
3609
|
+
}) && json(file)?.name === name;
|
|
3610
|
+
});
|
|
3611
|
+
if (matched.length !== 1) return unknown();
|
|
3612
|
+
const manifest = matched[0], data = json(manifest);
|
|
3613
|
+
let target;
|
|
3614
|
+
if ("exports" in data) {
|
|
3615
|
+
const value = data.exports;
|
|
3616
|
+
if (object(value) && Object.keys(value).some((key) => key.startsWith(".")) && Object.keys(value).some((key) => !key.startsWith("."))) return unknown();
|
|
3617
|
+
const entry = object(value) && Object.keys(value).some((key) => key.startsWith(".")) ? value[subpath] : subpath === "." ? value : null;
|
|
3618
|
+
let limited2 = false;
|
|
3619
|
+
target = exportTarget(entry, () => {
|
|
3620
|
+
limited2 = true;
|
|
3621
|
+
});
|
|
3622
|
+
if (limited2) return unknown(true);
|
|
3623
|
+
} else target = subpath === "." && typeof data.main === "string" && (!data.module || data.module === data.main) ? data.main : null;
|
|
3624
|
+
if (!target || !safePath(target)) return unknown();
|
|
3625
|
+
const dir = posix.dirname(manifest.path), path = posix.normalize(posix.join(dir, target));
|
|
3626
|
+
if (!contained(posix.relative(dir, path))) return unknown();
|
|
3627
|
+
return { handled: true, file: sourceAt(path, from) };
|
|
3628
|
+
};
|
|
3629
|
+
const resolve3 = (spec, from) => {
|
|
3630
|
+
if (!spec || spec.length > 512 || /[\\\u0000-\u0020:%?]/.test(spec) || posix.isAbsolute(spec)) return unknown();
|
|
3631
|
+
if (spec.startsWith(".")) return { handled: true, file: bindingModule(spec, from, files, "") };
|
|
3632
|
+
const configured = configPath(spec, from);
|
|
3633
|
+
if (configured) return configured;
|
|
3634
|
+
const local = workspace(spec, from);
|
|
3635
|
+
if (local) return local;
|
|
3636
|
+
const scope = /^(.*?\/)?(?:src|app|lib|server|routes|api|controllers|plugins)\//.exec(from.path)?.[1] ?? "";
|
|
3637
|
+
const file = bindingModule(spec, from, files, scope);
|
|
3638
|
+
return { handled: Boolean(file), file };
|
|
3639
|
+
};
|
|
3640
|
+
return (spec, from) => {
|
|
3641
|
+
metadataLimited = false;
|
|
3642
|
+
const result = resolve3(spec, from);
|
|
3643
|
+
if (result.file && (!/\.[mc]?[jt]sx?$/.test(result.file.path) || /\.d\.[cm]?ts$/.test(result.file.path))) return unknown();
|
|
3644
|
+
return metadataLimited ? unknown(true) : result;
|
|
3645
|
+
};
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
// src/rules/registration-context.ts
|
|
3649
|
+
function registrationContext(code, pairs, bodies) {
|
|
3650
|
+
const regions = [];
|
|
3651
|
+
const openers = new Map([...pairs].map(([open, close]) => [close, open]));
|
|
3652
|
+
const controlBraces = /* @__PURE__ */ new Set();
|
|
3653
|
+
let limited2 = false;
|
|
3654
|
+
const space = (at) => {
|
|
3655
|
+
while (at < code.length && /\s/.test(code[at])) at++;
|
|
3656
|
+
return at;
|
|
3657
|
+
};
|
|
3658
|
+
const add = (start, end, dead = false, owner2 = false) => {
|
|
3659
|
+
if (regions.some((region) => region.start === start && region.end === end && region.dead === dead && region.owner === owner2)) return;
|
|
3660
|
+
if (regions.length >= 512) {
|
|
3661
|
+
limited2 = true;
|
|
3662
|
+
return;
|
|
3663
|
+
}
|
|
3664
|
+
regions.push({ start, end, dead, owner: owner2 });
|
|
3665
|
+
};
|
|
3666
|
+
const literal2 = (text) => {
|
|
3667
|
+
const match = /^\s*(!\s*)?(true|false)\s*$/.exec(text);
|
|
3668
|
+
return match ? match[2] === "true" !== Boolean(match[1]) : null;
|
|
3669
|
+
};
|
|
3670
|
+
const simpleEnd = (start) => {
|
|
3671
|
+
for (let at = start; at < code.length; at++) {
|
|
3672
|
+
const quote = code[at];
|
|
3673
|
+
if (quote === '"' || quote === "'" || quote === "`") {
|
|
3674
|
+
let end2 = at + 1;
|
|
3675
|
+
for (; end2 < code.length; end2++) {
|
|
3676
|
+
if (quote === "`" && code[end2] === "$" && code[end2 + 1] === "{") {
|
|
3677
|
+
const close = pairs.get(end2 + 1);
|
|
3678
|
+
if (close !== void 0) {
|
|
3679
|
+
end2 = close;
|
|
3680
|
+
continue;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
if (code[end2] === quote) break;
|
|
3684
|
+
}
|
|
3685
|
+
if (end2 === code.length) limited2 = true;
|
|
3686
|
+
at = end2;
|
|
3687
|
+
continue;
|
|
3688
|
+
}
|
|
3689
|
+
const end = pairs.get(at);
|
|
3690
|
+
if (end !== void 0) {
|
|
3691
|
+
at = end;
|
|
3692
|
+
continue;
|
|
3693
|
+
}
|
|
3694
|
+
if (/[;,}\n]/.test(code[at])) return at + (code[at] === ";" ? 1 : 0);
|
|
3695
|
+
}
|
|
3696
|
+
return code.length;
|
|
3697
|
+
};
|
|
3698
|
+
const statementEnd3 = (rawStart, depth = 0) => {
|
|
3699
|
+
const start = space(rawStart);
|
|
3700
|
+
if (depth >= 32) {
|
|
3701
|
+
limited2 = true;
|
|
3702
|
+
return code.length;
|
|
3703
|
+
}
|
|
3704
|
+
if (code[start] === "{") {
|
|
3705
|
+
const end = pairs.get(start);
|
|
3706
|
+
if (end === void 0) limited2 = true;
|
|
3707
|
+
return (end ?? code.length - 1) + 1;
|
|
3708
|
+
}
|
|
3709
|
+
const head = /^(if|for(?:\s+await)?|while|switch|with)\s*\(/.exec(code.slice(start, start + 100));
|
|
3710
|
+
if (head) {
|
|
3711
|
+
const close = pairs.get(start + head[0].length - 1);
|
|
3712
|
+
if (close === void 0) {
|
|
3713
|
+
limited2 = true;
|
|
3714
|
+
return code.length;
|
|
3715
|
+
}
|
|
3716
|
+
let end = statementEnd3(close + 1, depth + 1);
|
|
3717
|
+
const tail = space(end);
|
|
3718
|
+
if (head[1] === "if" && /^else\b/.test(code.slice(tail, tail + 8))) end = statementEnd3(tail + 4, depth + 1);
|
|
3719
|
+
return end;
|
|
3720
|
+
}
|
|
3721
|
+
return simpleEnd(start);
|
|
3722
|
+
};
|
|
3723
|
+
for (const body of bodies) add(body.start, body.end + 1, false, true);
|
|
3724
|
+
for (const match of code.matchAll(/=>\s*(?!\s*\{)/g)) {
|
|
3725
|
+
const start = space(match.index + 2);
|
|
3726
|
+
if (code[start] !== "{") add(start, simpleEnd(start), false, true);
|
|
3727
|
+
}
|
|
3728
|
+
for (const match of code.matchAll(/(?<![\w$.])(if|for(?:\s+await)?|while|switch|catch|with)\s*\(/g)) {
|
|
3729
|
+
const open = match.index + match[0].length - 1, close = pairs.get(open);
|
|
3730
|
+
if (close === void 0) {
|
|
3731
|
+
limited2 = true;
|
|
3732
|
+
continue;
|
|
3733
|
+
}
|
|
3734
|
+
const start = space(close + 1), end = statementEnd3(start);
|
|
3735
|
+
if (code[start] === "{") controlBraces.add(start);
|
|
3736
|
+
const condition = literal2(code.slice(open + 1, close));
|
|
3737
|
+
if (match[1] === "if") {
|
|
3738
|
+
if (condition !== true) add(start, end, condition === false);
|
|
3739
|
+
const next = space(end);
|
|
3740
|
+
if (/^else\b/.test(code.slice(next, next + 8))) {
|
|
3741
|
+
const alternate = space(next + 4), alternateEnd = statementEnd3(alternate);
|
|
3742
|
+
if (code[alternate] === "{") controlBraces.add(alternate);
|
|
3743
|
+
if (condition !== false) add(alternate, alternateEnd, condition === true);
|
|
3744
|
+
}
|
|
3745
|
+
} else {
|
|
3746
|
+
add(start, end, match[1] === "while" && condition === false);
|
|
3747
|
+
if (match[1] === "switch" && code[start] === "{") {
|
|
3748
|
+
const labels = [];
|
|
3749
|
+
for (let at = start + 1; at < end - 1; at++) {
|
|
3750
|
+
const nested = pairs.get(at);
|
|
3751
|
+
if (nested !== void 0) {
|
|
3752
|
+
at = nested;
|
|
3753
|
+
continue;
|
|
3754
|
+
}
|
|
3755
|
+
if ((at === 0 || !/[\w$.]/.test(code[at - 1])) && /^(?:case|default)\b/.test(code.slice(at, at + 10))) labels.push(at);
|
|
3756
|
+
}
|
|
3757
|
+
labels.forEach((at, index) => add(at, labels[index + 1] ?? end - 1));
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
for (const match of code.matchAll(/(?<![\w$.])(?:try|finally|do|catch)\s*\{/g)) {
|
|
3762
|
+
const start = match.index + match[0].length - 1;
|
|
3763
|
+
controlBraces.add(start);
|
|
3764
|
+
add(start, (pairs.get(start) ?? code.length - 1) + 1);
|
|
3765
|
+
}
|
|
3766
|
+
for (const [start, end] of pairs) {
|
|
3767
|
+
if (code[start] !== "{" || controlBraces.has(start) || bodies.some((body) => body.start === start)) continue;
|
|
3768
|
+
if (/\)\s*(?::[\w\s.<>,[\]|?]+)?$/.test(code.slice(Math.max(0, start - 200), start))) add(start, end + 1, false, true);
|
|
3769
|
+
}
|
|
3770
|
+
const contains = (region, at) => region.start <= at && at < region.end;
|
|
3771
|
+
const owner = (at) => regions.filter((region) => region.owner && contains(region, at)).sort((a, b) => a.end - a.start - (b.end - b.start))[0];
|
|
3772
|
+
const inline = (at) => {
|
|
3773
|
+
let start = at;
|
|
3774
|
+
for (let cursor = at - 1; cursor >= 0; cursor--) {
|
|
3775
|
+
if (at - cursor > 4e3) {
|
|
3776
|
+
limited2 = true;
|
|
3777
|
+
return "conditional";
|
|
3778
|
+
}
|
|
3779
|
+
const open = openers.get(cursor);
|
|
3780
|
+
if (open !== void 0) {
|
|
3781
|
+
cursor = open;
|
|
3782
|
+
start = open;
|
|
3783
|
+
continue;
|
|
3784
|
+
}
|
|
3785
|
+
if (code[cursor] === ";" || code[cursor] === "{") {
|
|
3786
|
+
start = cursor + 1;
|
|
3787
|
+
break;
|
|
3788
|
+
}
|
|
3789
|
+
if (code[cursor] === "\n" && !/[&|?:=,(.\\]$/.test(code.slice(Math.max(0, cursor - 100), cursor).trimEnd())) {
|
|
3790
|
+
start = cursor + 1;
|
|
3791
|
+
break;
|
|
3792
|
+
}
|
|
3793
|
+
start = cursor;
|
|
3794
|
+
}
|
|
3795
|
+
let prefix = "";
|
|
3796
|
+
for (let cursor = start; cursor < at; cursor++) {
|
|
3797
|
+
const close = pairs.get(cursor);
|
|
3798
|
+
if (close !== void 0 && close < at) {
|
|
3799
|
+
prefix += " ";
|
|
3800
|
+
cursor = close;
|
|
3801
|
+
continue;
|
|
3802
|
+
}
|
|
3803
|
+
prefix += code[cursor];
|
|
3804
|
+
}
|
|
3805
|
+
prefix = prefix.trim();
|
|
3806
|
+
const simple = /^(?:const\s+[\w$]+\s*=\s*)?(!?\s*(?:true|false))\s*(&&|\|\|)\s*$/.exec(prefix);
|
|
3807
|
+
if (simple) {
|
|
3808
|
+
const truth = literal2(simple[1]);
|
|
3809
|
+
return (simple[2] === "&&" ? truth : !truth) ? "always" : "dead";
|
|
3810
|
+
}
|
|
3811
|
+
return /&&|\|\||\?/.test(prefix) ? "conditional" : "always";
|
|
3812
|
+
};
|
|
3813
|
+
const reachable = (at) => limited2 || !regions.some((region) => region.dead && contains(region, at)) && inline(at) !== "dead";
|
|
3814
|
+
return {
|
|
3815
|
+
get limited() {
|
|
3816
|
+
return limited2;
|
|
3817
|
+
},
|
|
3818
|
+
reachable,
|
|
3819
|
+
covers(guard, operation) {
|
|
3820
|
+
if (limited2 || !reachable(guard) || inline(guard) !== "always") return false;
|
|
3821
|
+
const guardOwner = owner(guard);
|
|
3822
|
+
if (guardOwner && guardOwner !== owner(operation)) return false;
|
|
3823
|
+
return regions.filter((region) => !region.dead && contains(region, guard)).every((region) => contains(region, operation));
|
|
3824
|
+
}
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3467
3828
|
// src/rules/routers.ts
|
|
3468
3829
|
var NODE_FRAMEWORKS = /* @__PURE__ */ new Set(["express", "hono", "fastify"]);
|
|
3469
3830
|
var METHODS = "get|post|put|patch|delete|all|options|head";
|
|
3470
3831
|
var GENERICS = String.raw`(?:<[^()]{0,300}>\s*)?`;
|
|
3471
3832
|
var FASTIFY_HOOKS = /^(?:onRequest|preParsing|preValidation|preHandler)$/;
|
|
3472
|
-
var HONO_CALL = `${METHODS}|on|use|route`;
|
|
3833
|
+
var HONO_CALL = `${METHODS}|on|use|route|openapiRoutes|openapi`;
|
|
3473
3834
|
var analysedCache = /* @__PURE__ */ new WeakMap();
|
|
3474
3835
|
function analyse(file) {
|
|
3475
3836
|
const cached = analysedCache.get(file);
|
|
@@ -3508,8 +3869,9 @@ function argsOf(a, open, close) {
|
|
|
3508
3869
|
function flatten(a, args) {
|
|
3509
3870
|
return args.flatMap((arg) => {
|
|
3510
3871
|
if (!arg.text.startsWith("[")) return [arg];
|
|
3511
|
-
const
|
|
3512
|
-
|
|
3872
|
+
const owner = arg.file ? analyse(arg.file) : a;
|
|
3873
|
+
const close = owner.pairs.get(arg.at);
|
|
3874
|
+
return close === void 0 ? [arg] : argsOf(owner, arg.at, close).map((item) => ({ ...item, file: owner.file }));
|
|
3513
3875
|
});
|
|
3514
3876
|
}
|
|
3515
3877
|
function scopeOf(path) {
|
|
@@ -3542,7 +3904,7 @@ function entriesOf(a, arg) {
|
|
|
3542
3904
|
return entries;
|
|
3543
3905
|
}
|
|
3544
3906
|
function frameworkNamesOf(a) {
|
|
3545
|
-
const names = { express: /* @__PURE__ */ new Set(), router: /* @__PURE__ */ new Set(), hono: /* @__PURE__ */ new Set(), fastify: /* @__PURE__ */ new Set(), types: /* @__PURE__ */ new Set() };
|
|
3907
|
+
const names = { express: /* @__PURE__ */ new Set(), router: /* @__PURE__ */ new Set(), hono: /* @__PURE__ */ new Set(), openapi: /* @__PURE__ */ new Set(), fastify: /* @__PURE__ */ new Set(), types: /* @__PURE__ */ new Set() };
|
|
3546
3908
|
const named = (list, wanted, into, fallback) => {
|
|
3547
3909
|
for (const part of list.split(",")) {
|
|
3548
3910
|
const m = /^\s*(?:type\s+)?([A-Za-z_$][\w$]*)\s*(?:(?:as|:)\s*([A-Za-z_$][\w$]*))?\s*$/.exec(part);
|
|
@@ -3552,6 +3914,7 @@ function frameworkNamesOf(a) {
|
|
|
3552
3914
|
}
|
|
3553
3915
|
};
|
|
3554
3916
|
for (const m of a.source.matchAll(/\bimport\s+(type\s+)?(?:([A-Za-z_$][\w$]*)\s*,?\s*)?(?:\{([^{}]*)\}|\*\s+as\s+([A-Za-z_$][\w$]*))?\s*from\s*['"](express|hono(?:\/tiny|\/quick)?|@hono\/zod-openapi|fastify)['"]/g)) {
|
|
3917
|
+
if (a.code.slice(m.index, m.index + 6) !== "import") continue;
|
|
3555
3918
|
const typeOnly = Boolean(m[1]);
|
|
3556
3919
|
if (m[5] === "express") {
|
|
3557
3920
|
if (!typeOnly && m[2]) names.express.add(m[2]);
|
|
@@ -3564,20 +3927,27 @@ function frameworkNamesOf(a) {
|
|
|
3564
3927
|
names.types.add("fastify");
|
|
3565
3928
|
} else {
|
|
3566
3929
|
if (!typeOnly && m[3]) named(m[3], /^(?:Hono|OpenAPIHono)$/, names.hono);
|
|
3930
|
+
if (!typeOnly && m[3] && m[5] === "@hono/zod-openapi") named(m[3], /^OpenAPIHono$/, names.openapi);
|
|
3567
3931
|
names.types.add("hono");
|
|
3568
3932
|
}
|
|
3569
3933
|
}
|
|
3570
3934
|
for (const m of a.source.matchAll(/\b(?:const|let|var)\s+(?:([A-Za-z_$][\w$]*)|\{([^{}]*)\})\s*=\s*require\s*\(\s*['"](express|hono(?:\/tiny|\/quick)?|@hono\/zod-openapi|fastify)['"]\s*\)(?!\s*\()/g)) {
|
|
3935
|
+
if (!/^(?:const|let|var)\b/.test(a.code.slice(m.index))) continue;
|
|
3571
3936
|
if (m[3] === "express") {
|
|
3572
3937
|
if (m[1]) names.express.add(m[1]);
|
|
3573
3938
|
if (m[2]) named(m[2], /^Router$/, names.router);
|
|
3574
3939
|
} else if (m[3] === "fastify") {
|
|
3575
3940
|
if (m[1]) names.fastify.add(m[1]);
|
|
3576
3941
|
if (m[2]) named(m[2], /^fastify$/, names.fastify);
|
|
3577
|
-
} else if (m[2])
|
|
3942
|
+
} else if (m[2]) {
|
|
3943
|
+
named(m[2], /^(?:Hono|OpenAPIHono)$/, names.hono);
|
|
3944
|
+
if (m[3] === "@hono/zod-openapi") named(m[2], /^OpenAPIHono$/, names.openapi);
|
|
3945
|
+
}
|
|
3578
3946
|
names.types.add(m[3] === "express" || m[3] === "fastify" ? m[3] : "hono");
|
|
3579
3947
|
}
|
|
3580
|
-
for (const m of a.source.matchAll(/\brequire\s*\(\s*['"](express|fastify)['"]\s*\)/g))
|
|
3948
|
+
for (const m of a.source.matchAll(/\brequire\s*\(\s*['"](express|fastify)['"]\s*\)/g)) {
|
|
3949
|
+
if (a.code.slice(m.index, m.index + 7) === "require") names.types.add(m[1]);
|
|
3950
|
+
}
|
|
3581
3951
|
return names.types.size ? names : null;
|
|
3582
3952
|
}
|
|
3583
3953
|
function instancesOf(a, names) {
|
|
@@ -3586,6 +3956,7 @@ function instancesOf(a, names) {
|
|
|
3586
3956
|
if (!found.some((other) => other.name === instance.name)) found.push(instance);
|
|
3587
3957
|
};
|
|
3588
3958
|
for (const m of a.source.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]{0,100})?=\s*(?:await\s+)?(new\s+)?([A-Za-z_$][\w$]*)\s*(?:\.\s*(Router)\s*)?(<[^()]{0,300}>\s*)?\(/g)) {
|
|
3959
|
+
if (!/^(?:const|let|var)\b/.test(a.code.slice(m.index))) continue;
|
|
3589
3960
|
const [, name, isNew, callee, router] = m;
|
|
3590
3961
|
if (!isNew && (names.express.has(callee) && (router === "Router" || router === void 0) || names.router.has(callee) && router === void 0)) {
|
|
3591
3962
|
add({ name, framework: "express", prefix: "", weak: false });
|
|
@@ -3602,12 +3973,13 @@ function instancesOf(a, names) {
|
|
|
3602
3973
|
prefix = (arg && pathOf(a, arg)) ?? "";
|
|
3603
3974
|
if (baseClose !== void 0) close = baseClose;
|
|
3604
3975
|
}
|
|
3605
|
-
add({ name, framework: "hono", prefix, weak: false, chainFrom: close });
|
|
3976
|
+
add({ name, framework: "hono", prefix, weak: false, chainFrom: close, openapi: names.openapi.has(callee) });
|
|
3606
3977
|
} else if (!isNew && names.fastify.has(callee) && router === void 0) {
|
|
3607
3978
|
add({ name, framework: "fastify", prefix: "", weak: false });
|
|
3608
3979
|
}
|
|
3609
3980
|
}
|
|
3610
3981
|
for (const m of a.source.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?require\s*\(\s*['"](express|fastify)['"]\s*\)\s*(?:\.\s*(Router)\s*)?\(/g)) {
|
|
3982
|
+
if (!/^(?:const|let|var)\b/.test(a.code.slice(m.index))) continue;
|
|
3611
3983
|
if (m[2] === "express" || !m[3]) add({ name: m[1], framework: m[2], prefix: "", weak: false });
|
|
3612
3984
|
}
|
|
3613
3985
|
const typed = [
|
|
@@ -3618,11 +3990,180 @@ function instancesOf(a, names) {
|
|
|
3618
3990
|
for (const [framework, type] of typed) {
|
|
3619
3991
|
if (!names.types.has(framework)) continue;
|
|
3620
3992
|
for (const m of a.code.matchAll(new RegExp(String.raw`([A-Za-z_$][\w$]*)\s*\??:\s*(?:[A-Za-z_$][\w$]*\s*\.\s*)?${type}`, "g"))) {
|
|
3621
|
-
add({ name: m[1], framework, prefix: "", weak: true });
|
|
3993
|
+
add({ name: m[1], framework, prefix: "", weak: true, openapi: framework === "hono" && /\bOpenAPIHono\b/.test(m[0]) });
|
|
3622
3994
|
}
|
|
3623
3995
|
}
|
|
3624
3996
|
return found;
|
|
3625
3997
|
}
|
|
3998
|
+
function factoryBindingClean(a, name, initializer = -1) {
|
|
3999
|
+
const escaped = name.replace(/\$/g, "\\$");
|
|
4000
|
+
if ([...a.code.matchAll(new RegExp(`\\b(?:const|let|var|function|class)\\s+${escaped}(?![\\w$])`, "g"))].length > 1) return false;
|
|
4001
|
+
if (initializer < 0) {
|
|
4002
|
+
const declaration = new RegExp(`\\bconst\\s+(${escaped})\\s*(?::[^=;]{0,200})?=`).exec(a.code);
|
|
4003
|
+
if (declaration) initializer = declaration.index + /^const\s+/.exec(declaration[0])[0].length;
|
|
4004
|
+
}
|
|
4005
|
+
if (new RegExp(`[(,]\\s*${escaped}\\s*(?=[:,)=])|(?<![\\w$.])${escaped}\\s*=>`).test(a.code)) return false;
|
|
4006
|
+
for (const match of a.code.matchAll(/\b(?:const|let|var)\s*([\[{])/g)) {
|
|
4007
|
+
const open = match.index + match[0].length - 1, close = a.pairs.get(open);
|
|
4008
|
+
if (close !== void 0 && new RegExp(`(?<![\\w$])${escaped}(?![\\w$])`).test(a.code.slice(open, close + 1))) return false;
|
|
4009
|
+
}
|
|
4010
|
+
const writes = new RegExp(`(?<![\\w$.])${escaped}\\s*(?:=(?![=>])|[+*/%&|^-]=|\\?\\?=|&&=|\\|\\|=|\\+\\+|--)`, "g");
|
|
4011
|
+
for (const match of a.code.matchAll(writes)) if (match.index !== initializer) return false;
|
|
4012
|
+
return !new RegExp(`(?<![\\w$.])${escaped}(?:\\s*\\.\\s*[\\w$]+|\\s*\\[[^\\]]{0,200}\\])+\\s*=(?!=)|\\bObject\\s*\\.\\s*(?:assign|defineProperty|defineProperties)\\s*\\(\\s*${escaped}(?![\\w$])`).test(a.code);
|
|
4013
|
+
}
|
|
4014
|
+
function projectFactoryResolver(files, gaps) {
|
|
4015
|
+
const cache3 = /* @__PURE__ */ new Map();
|
|
4016
|
+
const module = factoryModuleResolver(files);
|
|
4017
|
+
let limited2 = false;
|
|
4018
|
+
const topLevel = (a, at) => ![...a.pairs].some(([open, close]) => a.code[open] === "{" && open < at && at < close);
|
|
4019
|
+
const argAt = (a, at, end) => {
|
|
4020
|
+
const lead = a.code.slice(at, end).length - a.code.slice(at, end).trimStart().length;
|
|
4021
|
+
at += lead;
|
|
4022
|
+
const text = a.code.slice(at, end).trimEnd();
|
|
4023
|
+
return { at, text, source: a.source.slice(at, at + text.length) };
|
|
4024
|
+
};
|
|
4025
|
+
const basePath = (a, close, shape) => {
|
|
4026
|
+
for (let i = 0; i < 8; i++) {
|
|
4027
|
+
const next = /^\s*\.\s*basePath\s*\(/.exec(a.code.slice(close + 1, close + 200));
|
|
4028
|
+
if (!next) return { shape, close };
|
|
4029
|
+
if (shape.framework !== "hono") return null;
|
|
4030
|
+
const open = close + next[0].length, end = a.pairs.get(open);
|
|
4031
|
+
const args = end === void 0 ? [] : argsOf(a, open, end);
|
|
4032
|
+
const path = args.length === 1 ? literalOf(args[0]) : null;
|
|
4033
|
+
if (end === void 0 || path === null) return null;
|
|
4034
|
+
shape = { ...shape, prefix: joinPath(shape.prefix, path) };
|
|
4035
|
+
close = end;
|
|
4036
|
+
}
|
|
4037
|
+
if (/^\s*\.\s*basePath\s*\(/.test(a.code.slice(close + 1, close + 200))) {
|
|
4038
|
+
limited2 = true;
|
|
4039
|
+
return null;
|
|
4040
|
+
}
|
|
4041
|
+
return { shape, close };
|
|
4042
|
+
};
|
|
4043
|
+
const expression = (a, arg, depth) => {
|
|
4044
|
+
if (depth >= 8 || arg.text.length > 4e3) {
|
|
4045
|
+
limited2 = true;
|
|
4046
|
+
return null;
|
|
4047
|
+
}
|
|
4048
|
+
if (arg.text[0] === "(" && a.pairs.get(arg.at) === arg.at + arg.text.length - 1) return expression(a, argAt(a, arg.at + 1, arg.at + arg.text.length - 1), depth + 1);
|
|
4049
|
+
const call = /^(new\s+)?([A-Za-z_$][\w$]*)(?:\s*\.\s*(Router))?\s*(?:<[^()]{0,300}>\s*)?\(/.exec(arg.text);
|
|
4050
|
+
if (!call) return null;
|
|
4051
|
+
const [, isNew, name, member] = call;
|
|
4052
|
+
const open = arg.at + call[0].length - 1, close = a.pairs.get(open);
|
|
4053
|
+
if (close === void 0 || close >= arg.at + arg.text.length || !factoryBindingClean(a, name)) return null;
|
|
4054
|
+
const imports = bindingsOf(a.file).imports.filter((item) => item.local === name);
|
|
4055
|
+
const imported = imports.length === 1 ? imports[0] : void 0;
|
|
4056
|
+
let shape = null;
|
|
4057
|
+
const origin = imported?.spec ? module(imported.spec, a.file) : null;
|
|
4058
|
+
if (origin?.limited) limited2 = true;
|
|
4059
|
+
if (imported?.spec && !origin?.handled && !new RegExp(`\\b(?:const|let|var|function|class)\\s+${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(a.code)) {
|
|
4060
|
+
if (isNew && !member && ["hono", "hono/tiny", "hono/quick"].includes(imported.spec ?? "") && imported.imported === "Hono") shape = { framework: "hono", prefix: "", openapi: false };
|
|
4061
|
+
if (isNew && !member && imported.spec === "@hono/zod-openapi" && imported.imported === "OpenAPIHono") shape = { framework: "hono", prefix: "", openapi: true };
|
|
4062
|
+
if (!isNew && imported.spec === "express" && (imported.imported === "default" && (!member || member === "Router") || imported.imported === "Router" && !member)) shape = { framework: "express", prefix: "", openapi: false };
|
|
4063
|
+
if (!isNew && !member && imported.spec === "fastify" && ["default", "fastify"].includes(imported.imported)) shape = { framework: "fastify", prefix: "", openapi: false };
|
|
4064
|
+
}
|
|
4065
|
+
if (!shape && !isNew && !member && !a.source.slice(open + 1, close).trim()) shape = resolve3(a, name, false, depth + 1);
|
|
4066
|
+
if (!shape) return null;
|
|
4067
|
+
const chained = basePath(a, close, shape);
|
|
4068
|
+
return chained && !arg.text.slice(chained.close - arg.at + 1).trim() ? chained.shape : null;
|
|
4069
|
+
};
|
|
4070
|
+
const functionValue = (a, value, depth) => {
|
|
4071
|
+
if (depth >= 8) {
|
|
4072
|
+
limited2 = true;
|
|
4073
|
+
return null;
|
|
4074
|
+
}
|
|
4075
|
+
if (/^[A-Za-z_$][\w$]*$/.test(value.text)) return resolve3(a, value.text, false, depth + 1);
|
|
4076
|
+
const signature = /^(?:function(?:\s+[A-Za-z_$][\w$]*)?\s*\(\s*\)\s*(?::[^{};=]{1,200})?\s*|\(\s*\)\s*(?::[^{};=]{1,200})?\s*=>\s*)/.exec(value.text);
|
|
4077
|
+
if (!signature) return null;
|
|
4078
|
+
let body = argAt(a, value.at + signature[0].length, value.at + value.text.length);
|
|
4079
|
+
if (body.text.startsWith("{")) {
|
|
4080
|
+
const end = a.pairs.get(body.at);
|
|
4081
|
+
if (end !== body.at + body.text.length - 1) return null;
|
|
4082
|
+
const returned = /^\s*return\b[ \t]*/.exec(a.code.slice(body.at + 1, end));
|
|
4083
|
+
if (!returned) return null;
|
|
4084
|
+
const from = body.at + 1 + returned[0].length;
|
|
4085
|
+
if (/[\r\n]/.test(a.code[from] ?? "")) return null;
|
|
4086
|
+
body = argAt(a, from, end);
|
|
4087
|
+
if (body.text.endsWith(";")) body = argAt(a, body.at, body.at + body.text.length - 1);
|
|
4088
|
+
}
|
|
4089
|
+
return expression(a, body, depth);
|
|
4090
|
+
};
|
|
4091
|
+
const resolve3 = (a, name, exported, depth) => {
|
|
4092
|
+
if (depth >= 8) {
|
|
4093
|
+
limited2 = true;
|
|
4094
|
+
return null;
|
|
4095
|
+
}
|
|
4096
|
+
const key = `${a.file.path}\0${name}\0${exported}\0${depth}`;
|
|
4097
|
+
if (cache3.has(key)) return cache3.get(key);
|
|
4098
|
+
const follow = (spec, symbol) => {
|
|
4099
|
+
const resolved = module(spec, a.file);
|
|
4100
|
+
if (resolved.limited) limited2 = true;
|
|
4101
|
+
const target = resolved.file;
|
|
4102
|
+
return target ? resolve3(analyse(target), symbol, true, depth + 1) : null;
|
|
4103
|
+
};
|
|
4104
|
+
const bindings = bindingsOf(a.file), escaped = name.replace(/\$/g, "\\$");
|
|
4105
|
+
let result = null;
|
|
4106
|
+
if (exported) {
|
|
4107
|
+
if (name === "default") {
|
|
4108
|
+
const match = /\bexport\s+default\s+/.exec(a.code);
|
|
4109
|
+
if (match && topLevel(a, match.index)) {
|
|
4110
|
+
const at = match.index + match[0].length;
|
|
4111
|
+
const named = /^function\s+([A-Za-z_$][\w$]*)/.exec(a.code.slice(at));
|
|
4112
|
+
if (!named || factoryBindingClean(a, named[1])) result = functionValue(a, argAt(a, at, expressionEnd(a, at)), depth);
|
|
4113
|
+
}
|
|
4114
|
+
} else if (new RegExp(`\\bexport\\s+(?:const|function)\\s+${escaped}(?![\\w$])`).test(a.code)) result = resolve3(a, name, false, depth);
|
|
4115
|
+
else {
|
|
4116
|
+
const matches = bindings.exports.filter((item) => item.local === name);
|
|
4117
|
+
if (matches.length === 1) {
|
|
4118
|
+
const entry = matches[0];
|
|
4119
|
+
result = entry.spec ? follow(entry.spec, entry.imported) : resolve3(a, entry.imported, false, depth + 1);
|
|
4120
|
+
} else if (!matches.length && name !== "default" && bindings.stars.length === 1) result = follow(bindings.stars[0], name);
|
|
4121
|
+
}
|
|
4122
|
+
} else {
|
|
4123
|
+
const declarations = [...a.code.matchAll(new RegExp(`\\b(const|function)\\s+${escaped}(?![\\w$])`, "g"))];
|
|
4124
|
+
const declaration = declarations.length === 1 ? declarations[0] : void 0;
|
|
4125
|
+
if (declaration && topLevel(a, declaration.index)) {
|
|
4126
|
+
const at = declaration.index;
|
|
4127
|
+
if (declaration[1] === "function" && factoryBindingClean(a, name)) result = functionValue(a, argAt(a, at, expressionEnd(a, at)), depth);
|
|
4128
|
+
if (declaration[1] === "const") {
|
|
4129
|
+
const prefix = new RegExp(`^const\\s+${escaped}\\s*(?::[^=;]{0,200})?=\\s*`).exec(a.code.slice(at));
|
|
4130
|
+
const nameAt = at + /^const\s+/.exec(declaration[0])[0].length;
|
|
4131
|
+
if (prefix && factoryBindingClean(a, name, nameAt)) {
|
|
4132
|
+
const valueAt = at + prefix[0].length;
|
|
4133
|
+
result = functionValue(a, argAt(a, valueAt, expressionEnd(a, valueAt)), depth);
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
} else if (!declarations.length && factoryBindingClean(a, name)) {
|
|
4137
|
+
const imports = bindings.imports.filter((item) => item.local === name && item.spec);
|
|
4138
|
+
if (imports.length === 1) result = follow(imports[0].spec, imports[0].imported);
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
if (result) cache3.set(key, result);
|
|
4142
|
+
return result;
|
|
4143
|
+
};
|
|
4144
|
+
return (a, known) => {
|
|
4145
|
+
const found = [];
|
|
4146
|
+
const receivers = new Set([...a.code.matchAll(new RegExp(`(?<![\\w$.])([A-Za-z_$][\\w$]*)\\s*\\.\\s*(?:${METHODS}|on|openapi|openapiRoutes|route|register)\\s*${GENERICS}\\(`, "g"))].map((match) => match[1]));
|
|
4147
|
+
let inspected = 0;
|
|
4148
|
+
for (const m of a.code.matchAll(/\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\s*\(\s*\)/g)) {
|
|
4149
|
+
const [, name, factory] = m;
|
|
4150
|
+
if (!receivers.has(name) || known.has(name)) continue;
|
|
4151
|
+
if (++inspected > 256) {
|
|
4152
|
+
gaps.set(a.file, m.index);
|
|
4153
|
+
break;
|
|
4154
|
+
}
|
|
4155
|
+
const open = a.code.indexOf("(", m.index), close = m.index + m[0].length - 1;
|
|
4156
|
+
if (!topLevel(a, m.index) || a.source.slice(open + 1, close).trim() || !factoryBindingClean(a, factory) || !factoryBindingClean(a, name)) continue;
|
|
4157
|
+
limited2 = false;
|
|
4158
|
+
const shape = resolve3(a, factory, false, 0);
|
|
4159
|
+
const chained = shape ? basePath(a, m.index + m[0].length - 1, shape) : null;
|
|
4160
|
+
if (limited2) gaps.set(a.file, m.index);
|
|
4161
|
+
const end = expressionEnd(a, open);
|
|
4162
|
+
if (chained && !a.code.slice(chained.close + 1, end).trim()) found.push({ name, ...chained.shape, weak: false, chainFrom: chained.close });
|
|
4163
|
+
}
|
|
4164
|
+
return found;
|
|
4165
|
+
};
|
|
4166
|
+
}
|
|
3626
4167
|
function literalOf(arg) {
|
|
3627
4168
|
const m = /^(['"`])([^'"`$]*)\1$/.exec(arg.source);
|
|
3628
4169
|
return m ? m[2] : null;
|
|
@@ -3716,11 +4257,199 @@ function expressUses(a, name) {
|
|
|
3716
4257
|
}
|
|
3717
4258
|
return found.sort((x, y) => x.at - y.at);
|
|
3718
4259
|
}
|
|
3719
|
-
function
|
|
4260
|
+
function openapiMiddleware(a, arg, depth = 0) {
|
|
4261
|
+
if (depth >= 8) return [];
|
|
4262
|
+
if (arg.text.startsWith("[")) {
|
|
4263
|
+
const close = a.pairs.get(arg.at);
|
|
4264
|
+
if (close === void 0 || !/^\s*(?:as\s+const)?\s*$/.test(arg.text.slice(close - arg.at + 1))) return [];
|
|
4265
|
+
return argsOf(a, arg.at, close).flatMap((item) => openapiMiddleware(a, item, depth + 1));
|
|
4266
|
+
}
|
|
4267
|
+
if (/^[A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*$/.test(arg.text) || FUNCTION_EXPRESSION.test(arg.text)) return [arg];
|
|
4268
|
+
const call = /^[A-Za-z_$][\w$.]*\s*\(/.exec(arg.text);
|
|
4269
|
+
return call && a.pairs.get(arg.at + call[0].length - 1) === arg.at + arg.text.length - 1 ? [arg] : [];
|
|
4270
|
+
}
|
|
4271
|
+
function openapiEntries(a, arg, files, depth = 0) {
|
|
4272
|
+
const value = openapiValue(a, arg, files, depth);
|
|
4273
|
+
if (!value || !value.text.startsWith("{")) return /* @__PURE__ */ new Map();
|
|
4274
|
+
const owner = analyse(value.file);
|
|
4275
|
+
const close = owner.pairs.get(value.at);
|
|
4276
|
+
if (argsOf(owner, value.at, close).some((part) => /^(?:\.\.\.|\[|(?:get|set)\s+)/.test(part.text))) return /* @__PURE__ */ new Map();
|
|
4277
|
+
return new Map([...entriesOf(owner, value)].map(([key, entry]) => [key, {
|
|
4278
|
+
...entry,
|
|
4279
|
+
value: entry.value ? { ...entry.value, file: owner.file } : null
|
|
4280
|
+
}]));
|
|
4281
|
+
}
|
|
4282
|
+
function openapiValue(a, arg, files, depth = 0) {
|
|
4283
|
+
if (depth >= 8) return null;
|
|
4284
|
+
const root = /^([A-Za-z_$][\w$]*)(?:\s*[.(]|$)/.exec(arg.text)?.[1];
|
|
4285
|
+
if (root) {
|
|
4286
|
+
const token = new RegExp(`(?<![\\w$])${root.replace(/\$/g, "\\$")}(?![\\w$])`);
|
|
4287
|
+
if (a.bodies.some((body) => body.start < arg.at && arg.at < body.end && token.test(a.code.slice(body.declaration, body.start)))) return null;
|
|
4288
|
+
for (const match of a.code.matchAll(/\b(?:const|let|var)\s*([\[{])/g)) {
|
|
4289
|
+
const open = match.index + match[0].length - 1;
|
|
4290
|
+
const close = a.pairs.get(open);
|
|
4291
|
+
if (close !== void 0 && token.test(a.code.slice(open, close + 1))) return null;
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
if (/^[{[]/.test(arg.text)) {
|
|
4295
|
+
const close = a.pairs.get(arg.at);
|
|
4296
|
+
if (close === void 0 || !/^\s*(?:as\s+const|satisfies\s+[\w$.]+)?\s*$/.test(arg.text.slice(close - arg.at + 1))) return null;
|
|
4297
|
+
return { ...arg, file: a.file };
|
|
4298
|
+
}
|
|
4299
|
+
const call = /^([A-Za-z_$][\w$]*)\s*\(/.exec(arg.text);
|
|
4300
|
+
if (call) {
|
|
4301
|
+
const binding = bindingsOf(a.file).imports.find((item) => item.local === call[1]);
|
|
4302
|
+
if (!binding || !["createRoute", "defineOpenAPIRoute"].includes(binding.imported) || binding.spec !== "@hono/zod-openapi") return null;
|
|
4303
|
+
if (new RegExp(`\\b(?:const|let|var|function)\\s+${call[1].replace(/\$/g, "\\$")}(?![\\w$])`).test(a.code)) return null;
|
|
4304
|
+
const open = arg.at + call[0].length - 1;
|
|
4305
|
+
const close = a.pairs.get(open);
|
|
4306
|
+
if (close !== arg.at + arg.text.length - 1) return null;
|
|
4307
|
+
const args = argsOf(a, open, close);
|
|
4308
|
+
return args.length === 1 ? openapiValue(a, args[0], files, depth + 1) : null;
|
|
4309
|
+
}
|
|
4310
|
+
if (root && !unchangedOpenapiReference(a, root)) return null;
|
|
4311
|
+
const member = /^([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)$/.exec(arg.text);
|
|
4312
|
+
if (member) {
|
|
4313
|
+
const escaped = member[1].replace(/\$/g, "\\$");
|
|
4314
|
+
if (new RegExp(`\\b(?:const|let|var|function|class)\\s+${escaped}(?![\\w$])`).test(a.code)) return null;
|
|
4315
|
+
const namespace = new RegExp(`\\bimport\\s+\\*\\s+as\\s+${escaped}\\s+from\\s*['"]([^'"]+)['"]`).exec(a.source);
|
|
4316
|
+
const module = namespace && a.code.slice(namespace.index, namespace.index + 6) === "import" ? moduleFor(namespace[1], a.file, files) : null;
|
|
4317
|
+
return module ? openapiExportValue(analyse(module), member[2], files, depth + 1) : null;
|
|
4318
|
+
}
|
|
4319
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(arg.text)) return null;
|
|
4320
|
+
const name = arg.text.replace(/\$/g, "\\$");
|
|
4321
|
+
const declarations = [...a.code.matchAll(new RegExp(`\\b(const|let|var)\\s+${name}\\s*(?::[^=;]{0,200})?=(?![=>])\\s*`, "g"))];
|
|
4322
|
+
if (declarations.length === 0) {
|
|
4323
|
+
const imports = bindingsOf(a.file).imports.filter((item) => item.local === arg.text && item.spec);
|
|
4324
|
+
const imported = imports.length === 1 ? imports[0] : void 0;
|
|
4325
|
+
const module = imported ? moduleFor(imported.spec, a.file, files) : null;
|
|
4326
|
+
return module ? openapiExportValue(analyse(module), imported.imported, files, depth + 1) : null;
|
|
4327
|
+
}
|
|
4328
|
+
if (declarations.length !== 1 || declarations[0][1] !== "const") return null;
|
|
4329
|
+
const declaration = declarations[0];
|
|
4330
|
+
if (declaration.index >= arg.at) return null;
|
|
4331
|
+
for (const [open, close] of a.pairs) {
|
|
4332
|
+
if (a.code[open] === "{" && open < declaration.index && declaration.index < close && !(open < arg.at && arg.at < close)) return null;
|
|
4333
|
+
}
|
|
4334
|
+
const from = declaration.index + declaration[0].length;
|
|
4335
|
+
const end = expressionEnd(a, from);
|
|
4336
|
+
const text = a.code.slice(from, end).trimEnd();
|
|
4337
|
+
return openapiValue(a, { at: from, text, source: a.source.slice(from, from + text.length) }, files, depth + 1);
|
|
4338
|
+
}
|
|
4339
|
+
function unchangedOpenapiReference(a, name) {
|
|
4340
|
+
const aliases = /* @__PURE__ */ new Set([name]);
|
|
4341
|
+
const assignedAliases = [...a.code.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\s*(?=[;,\n]|$)/g)];
|
|
4342
|
+
for (let pass = 0; pass < 8; pass++) {
|
|
4343
|
+
const size = aliases.size;
|
|
4344
|
+
for (const match of assignedAliases) if (aliases.has(match[2])) aliases.add(match[1]);
|
|
4345
|
+
if (aliases.size > 64) return false;
|
|
4346
|
+
if (aliases.size === size) break;
|
|
4347
|
+
if (pass === 7) return false;
|
|
4348
|
+
}
|
|
4349
|
+
for (const alias of aliases) {
|
|
4350
|
+
const escaped = alias.replace(/\$/g, "\\$");
|
|
4351
|
+
const mutable = new RegExp(`(?<![\\w$.])${escaped}\\s*(?:\\.\\s*[\\w$]+|\\[[^\\]]{0,200}\\])+(?:\\s*(?:=|\\?\\?=|\\|\\|=|&&=)|\\s*\\()|\\bdelete\\s+${escaped}(?![\\w$])|\\bObject\\s*\\.\\s*(?:assign|defineProperty|defineProperties)\\s*\\(\\s*${escaped}(?![\\w$])`);
|
|
4352
|
+
if (mutable.test(a.code)) return false;
|
|
4353
|
+
}
|
|
4354
|
+
return true;
|
|
4355
|
+
}
|
|
4356
|
+
function openapiExportValue(a, name, files, depth) {
|
|
4357
|
+
if (depth >= 8) return null;
|
|
4358
|
+
if (name === "default") {
|
|
4359
|
+
const declaration = /\bexport\s+default\s+/.exec(a.code);
|
|
4360
|
+
if (declaration) {
|
|
4361
|
+
const at = declaration.index + declaration[0].length;
|
|
4362
|
+
const text = a.code.slice(at, expressionEnd(a, at)).trimEnd();
|
|
4363
|
+
return openapiValue(a, { at, text, source: a.source.slice(at, at + text.length) }, files, depth + 1);
|
|
4364
|
+
}
|
|
4365
|
+
} else if (new RegExp(`\\bexport\\s+const\\s+${name.replace(/\$/g, "\\$")}\\s*(?=[:=])`).test(a.code)) {
|
|
4366
|
+
return openapiValue(a, { text: name, source: name, at: a.code.length }, files, depth + 1);
|
|
4367
|
+
}
|
|
4368
|
+
const bindings = bindingsOf(a.file);
|
|
4369
|
+
const matches = bindings.exports.filter((item) => item.local === name);
|
|
4370
|
+
if (matches.length > 1) return null;
|
|
4371
|
+
const binding = matches[0];
|
|
4372
|
+
if (binding?.spec) {
|
|
4373
|
+
const module = moduleFor(binding.spec, a.file, files);
|
|
4374
|
+
return module ? openapiExportValue(analyse(module), binding.imported, files, depth + 1) : null;
|
|
4375
|
+
}
|
|
4376
|
+
if (binding) return openapiValue(a, { text: binding.imported, source: binding.imported, at: a.code.length }, files, depth + 1);
|
|
4377
|
+
if (name !== "default" && bindings.stars.length === 1) {
|
|
4378
|
+
const module = moduleFor(bindings.stars[0], a.file, files);
|
|
4379
|
+
if (module) return openapiExportValue(analyse(module), name, files, depth + 1);
|
|
4380
|
+
}
|
|
4381
|
+
return null;
|
|
4382
|
+
}
|
|
4383
|
+
function honoOps(a, name, chainFrom, files, openapi = false) {
|
|
3720
4384
|
const registrations = [];
|
|
3721
4385
|
const uses = [];
|
|
4386
|
+
const batchGaps = /* @__PURE__ */ new Set();
|
|
4387
|
+
const addOpenapi = (config, handler, handlerRange, at, batch = false) => {
|
|
4388
|
+
const entries = openapiEntries(config.file ? analyse(config.file) : a, config, files);
|
|
4389
|
+
const path = entries.get("path")?.value;
|
|
4390
|
+
const resolved = path ? literalOf(path) : null;
|
|
4391
|
+
const routingPath = resolved?.replace(/\/\{(.+?)\}/g, "/:$1") ?? null;
|
|
4392
|
+
const middleware = entries.get("middleware")?.value;
|
|
4393
|
+
const owner = middleware?.file ? analyse(middleware.file) : a;
|
|
4394
|
+
registrations.push({
|
|
4395
|
+
path: routingPath,
|
|
4396
|
+
middleware: middleware ? openapiMiddleware(owner, middleware).map((arg) => ({ ...arg, file: owner.file })) : [],
|
|
4397
|
+
handler,
|
|
4398
|
+
handlerRange,
|
|
4399
|
+
at,
|
|
4400
|
+
batch
|
|
4401
|
+
});
|
|
4402
|
+
return [routingPath];
|
|
4403
|
+
};
|
|
3722
4404
|
const visit = (method, open, close, at, previous) => {
|
|
3723
4405
|
const args = argsOf(a, open, close);
|
|
4406
|
+
if (method === "openapiRoutes") {
|
|
4407
|
+
if (!openapi) return previous;
|
|
4408
|
+
let paths2 = previous;
|
|
4409
|
+
let inspected = 0;
|
|
4410
|
+
const gap = () => {
|
|
4411
|
+
batchGaps.add(at);
|
|
4412
|
+
paths2 = [null];
|
|
4413
|
+
};
|
|
4414
|
+
const walk = (origin, list, depth) => {
|
|
4415
|
+
if (!list || depth >= 8) {
|
|
4416
|
+
gap();
|
|
4417
|
+
return;
|
|
4418
|
+
}
|
|
4419
|
+
const value = openapiValue(origin, list, files);
|
|
4420
|
+
if (!value || !value.text.startsWith("[")) {
|
|
4421
|
+
gap();
|
|
4422
|
+
return;
|
|
4423
|
+
}
|
|
4424
|
+
const owner = analyse(value.file);
|
|
4425
|
+
for (const item of argsOf(owner, value.at, owner.pairs.get(value.at))) {
|
|
4426
|
+
if (++inspected > 256) {
|
|
4427
|
+
gap();
|
|
4428
|
+
return;
|
|
4429
|
+
}
|
|
4430
|
+
if (item.text.startsWith("...")) {
|
|
4431
|
+
const lead = 3 + item.text.slice(3).length - item.text.slice(3).trimStart().length;
|
|
4432
|
+
walk(owner, { at: item.at + lead, text: item.text.slice(lead), source: item.source.slice(lead) }, depth + 1);
|
|
4433
|
+
continue;
|
|
4434
|
+
}
|
|
4435
|
+
const entries = openapiEntries(owner, item, files);
|
|
4436
|
+
if (entries.get("addRoute")?.value?.text === "false") continue;
|
|
4437
|
+
const config = entries.get("route")?.value;
|
|
4438
|
+
const handler = entries.get("handler");
|
|
4439
|
+
if (!config || !handler || !handler.value && !handler.range) {
|
|
4440
|
+
gap();
|
|
4441
|
+
continue;
|
|
4442
|
+
}
|
|
4443
|
+
paths2 = addOpenapi(config, handler.value, handler.range, at, true);
|
|
4444
|
+
}
|
|
4445
|
+
};
|
|
4446
|
+
walk(a, args[0], 0);
|
|
4447
|
+
return paths2;
|
|
4448
|
+
}
|
|
4449
|
+
if (method === "openapi") {
|
|
4450
|
+
if (!openapi || !args[0] || !args[1]) return previous;
|
|
4451
|
+
return addOpenapi(args[0], args[1], null, at);
|
|
4452
|
+
}
|
|
3724
4453
|
if (method === "use") {
|
|
3725
4454
|
const prefixes = args[0] ? usePrefixes(a, args[0]) : null;
|
|
3726
4455
|
const middleware = flatten(a, prefixes === null ? args : args.slice(1));
|
|
@@ -3762,7 +4491,7 @@ function honoOps(a, name, chainFrom) {
|
|
|
3762
4491
|
}
|
|
3763
4492
|
registrations.sort((x, y) => x.at - y.at);
|
|
3764
4493
|
uses.sort((x, y) => x.at - y.at);
|
|
3765
|
-
return { registrations, uses };
|
|
4494
|
+
return { registrations, uses, batchGaps: [...batchGaps] };
|
|
3766
4495
|
}
|
|
3767
4496
|
function fastifyOps(a, name) {
|
|
3768
4497
|
const registrations = [];
|
|
@@ -4044,7 +4773,7 @@ function exportedPlugin(t, files, depth) {
|
|
|
4044
4773
|
const text = t.code.slice(from, end).trimEnd();
|
|
4045
4774
|
return pluginOf(t, { text, source: t.source.slice(from, from + text.length), at: from }, files, depth);
|
|
4046
4775
|
}
|
|
4047
|
-
var toRefs = (a, args) => args.map((arg) => ({ text: arg.source, at: arg.at, file: a.file }));
|
|
4776
|
+
var toRefs = (a, args) => args.map((arg) => ({ text: arg.source, at: arg.at, file: arg.file ?? a.file }));
|
|
4048
4777
|
function mountedFile(a, arg, files) {
|
|
4049
4778
|
const inline = /^require\s*\(\s*['"]([^'"]+)['"]\s*\)$/.exec(arg.source);
|
|
4050
4779
|
if (inline) return moduleFor(inline[1], a.file, files);
|
|
@@ -4096,31 +4825,42 @@ function paramNamesOf(a, range) {
|
|
|
4096
4825
|
}
|
|
4097
4826
|
var within = (site, at) => site.range === null || site.range.start <= at && at <= site.range.end;
|
|
4098
4827
|
var opsCache = /* @__PURE__ */ new WeakMap();
|
|
4099
|
-
|
|
4828
|
+
var contextCache = /* @__PURE__ */ new WeakMap();
|
|
4829
|
+
function contextOf(a) {
|
|
4830
|
+
let context = contextCache.get(a.file);
|
|
4831
|
+
if (!context) {
|
|
4832
|
+
context = registrationContext(a.code, a.pairs, a.bodies);
|
|
4833
|
+
contextCache.set(a.file, context);
|
|
4834
|
+
}
|
|
4835
|
+
return context;
|
|
4836
|
+
}
|
|
4837
|
+
function opsOf(site, files) {
|
|
4100
4838
|
const cached = opsCache.get(site);
|
|
4101
4839
|
if (cached) return cached;
|
|
4102
4840
|
const { a, name } = site;
|
|
4103
4841
|
let ops;
|
|
4104
4842
|
if (site.framework === "express") ops = { registrations: expressRegistrations(a, name), uses: expressUses(a, name), registers: [] };
|
|
4105
|
-
else if (site.framework === "hono") ops = { ...honoOps(a, name, site.chainFrom), registers: [] };
|
|
4843
|
+
else if (site.framework === "hono") ops = { ...honoOps(a, name, site.chainFrom, files, site.openapi), registers: [] };
|
|
4106
4844
|
else ops = fastifyOps(a, name);
|
|
4845
|
+
const context = contextOf(a);
|
|
4107
4846
|
const result = {
|
|
4108
|
-
registrations: ops.registrations.filter((reg) => within(site, reg.at)),
|
|
4109
|
-
uses: ops.uses.filter((use) => within(site, use.at)),
|
|
4110
|
-
registers: ops.registers.filter((reg) => within(site, reg.at))
|
|
4847
|
+
registrations: ops.registrations.filter((reg) => within(site, reg.at) && context.reachable(reg.at)),
|
|
4848
|
+
uses: ops.uses.filter((use) => within(site, use.at) && context.reachable(use.at)),
|
|
4849
|
+
registers: ops.registers.filter((reg) => within(site, reg.at) && context.reachable(reg.at)),
|
|
4850
|
+
batchGaps: (ops.batchGaps ?? []).filter((at) => within(site, at))
|
|
4111
4851
|
};
|
|
4112
4852
|
opsCache.set(site, result);
|
|
4113
4853
|
return result;
|
|
4114
4854
|
}
|
|
4115
4855
|
function generalMiddleware(site, before, files) {
|
|
4116
|
-
const args = opsOf(site).uses.filter((use) => !use.mount && use.at < before && (use.prefix === null || site.framework !== "hono" && use.prefix === "/" || /^\/?\*$/.test(use.prefix))).flatMap((use) => use.args).filter((arg) => site.framework !== "express" || mountedFile(site.a, arg, files) === null);
|
|
4856
|
+
const args = opsOf(site, files).uses.filter((use) => !use.mount && use.at < before && contextOf(site.a).covers(use.at, Number.isFinite(before) ? before : (site.range?.end ?? site.a.code.length) - 1) && (use.prefix === null || site.framework !== "hono" && use.prefix === "/" || /^\/?\*$/.test(use.prefix))).flatMap((use) => use.args).filter((arg) => site.framework !== "express" || mountedFile(site.a, arg, files) === null);
|
|
4117
4857
|
return toRefs(site.a, args);
|
|
4118
4858
|
}
|
|
4119
4859
|
function passedSites(site, files) {
|
|
4120
4860
|
const { a } = site;
|
|
4121
4861
|
const found = [];
|
|
4122
4862
|
const receive = (range, index, at) => {
|
|
4123
|
-
if (!range) return;
|
|
4863
|
+
if (!range || !contextOf(a).reachable(at)) return;
|
|
4124
4864
|
const target = analyse(range.file);
|
|
4125
4865
|
const name = paramNamesOf(target, range)[index];
|
|
4126
4866
|
if (!name) return;
|
|
@@ -4128,6 +4868,7 @@ function passedSites(site, files) {
|
|
|
4128
4868
|
a: target,
|
|
4129
4869
|
name,
|
|
4130
4870
|
framework: site.framework,
|
|
4871
|
+
openapi: Boolean(site.openapi),
|
|
4131
4872
|
range: { start: range.start, end: range.end },
|
|
4132
4873
|
prefix: site.prefix,
|
|
4133
4874
|
root: site.root ?? site,
|
|
@@ -4177,7 +4918,7 @@ function normalizePath(path) {
|
|
|
4177
4918
|
function registeredSites(site, files) {
|
|
4178
4919
|
const { a } = site;
|
|
4179
4920
|
const found = [];
|
|
4180
|
-
for (const reg of opsOf(site).registers) {
|
|
4921
|
+
for (const reg of opsOf(site, files).registers) {
|
|
4181
4922
|
const options = entriesOf(a, reg.options);
|
|
4182
4923
|
const prefixArg = options.get("prefix")?.value;
|
|
4183
4924
|
const prefix = joinPath(site.prefix, (prefixArg && pathOf(a, prefixArg)) ?? "");
|
|
@@ -4236,24 +4977,50 @@ function serverIndex(files) {
|
|
|
4236
4977
|
const cached = indexCache.get(files);
|
|
4237
4978
|
if (cached) return cached;
|
|
4238
4979
|
const sites = [];
|
|
4980
|
+
const registrationGaps = /* @__PURE__ */ new Set();
|
|
4981
|
+
const factoryGaps = /* @__PURE__ */ new Map();
|
|
4982
|
+
const factories = projectFactoryResolver(files, factoryGaps);
|
|
4239
4983
|
const seen = /* @__PURE__ */ new Set();
|
|
4240
4984
|
const add = (site) => {
|
|
4241
|
-
|
|
4985
|
+
if (site.inherited.length > 256) {
|
|
4986
|
+
registrationGaps.add(site.a.file);
|
|
4987
|
+
site.inherited = [];
|
|
4988
|
+
}
|
|
4989
|
+
const root = site.root ?? site;
|
|
4990
|
+
const identity2 = createHash2("sha256").update(JSON.stringify([
|
|
4991
|
+
site.a.file.path,
|
|
4992
|
+
site.name,
|
|
4993
|
+
site.framework,
|
|
4994
|
+
site.range?.start ?? -1,
|
|
4995
|
+
site.prefix,
|
|
4996
|
+
Boolean(site.openapi),
|
|
4997
|
+
root.a.file.path,
|
|
4998
|
+
root.name,
|
|
4999
|
+
root.range?.start ?? -1,
|
|
5000
|
+
root.prefix
|
|
5001
|
+
]));
|
|
5002
|
+
for (const ref of site.inherited) identity2.update(JSON.stringify([ref.file.path, ref.at]));
|
|
5003
|
+
const key = identity2.digest("hex");
|
|
4242
5004
|
if (seen.has(key)) return false;
|
|
5005
|
+
if (sites.length >= 4096) {
|
|
5006
|
+
registrationGaps.add(site.a.file);
|
|
5007
|
+
return false;
|
|
5008
|
+
}
|
|
4243
5009
|
seen.add(key);
|
|
4244
5010
|
sites.push(site);
|
|
4245
5011
|
return true;
|
|
4246
5012
|
};
|
|
4247
5013
|
for (const file of files) {
|
|
4248
|
-
if (!/\.[mc]?[jt]sx?$/.test(file.path)
|
|
5014
|
+
if (!/\.[mc]?[jt]sx?$/.test(file.path)) continue;
|
|
4249
5015
|
const a = analyse(file);
|
|
4250
5016
|
const names = frameworkNamesOf(a);
|
|
4251
|
-
|
|
4252
|
-
for (const instance of
|
|
5017
|
+
const direct = names ? instancesOf(a, names) : [];
|
|
5018
|
+
for (const instance of [...direct, ...factories(a, new Set(direct.map((item) => item.name)))]) {
|
|
4253
5019
|
add({
|
|
4254
5020
|
a,
|
|
4255
5021
|
name: instance.name,
|
|
4256
5022
|
framework: instance.framework,
|
|
5023
|
+
openapi: Boolean(instance.openapi),
|
|
4257
5024
|
range: null,
|
|
4258
5025
|
prefix: instance.prefix,
|
|
4259
5026
|
inherited: [],
|
|
@@ -4274,15 +5041,22 @@ function serverIndex(files) {
|
|
|
4274
5041
|
const shadowed = (site, at) => Boolean(site.weak) && sites.some((other) => other !== site && !other.weak && other.a === site.a && other.name === site.name && other.range !== null && within(other, at));
|
|
4275
5042
|
const byRouterFile = /* @__PURE__ */ new Map();
|
|
4276
5043
|
const byRoot = /* @__PURE__ */ new Map();
|
|
5044
|
+
const batchGaps = /* @__PURE__ */ new Map();
|
|
4277
5045
|
for (const site of sites) {
|
|
4278
5046
|
const { a } = site;
|
|
4279
|
-
const { registrations, uses } = opsOf(site);
|
|
5047
|
+
const { registrations, uses, batchGaps: gaps } = opsOf(site, files);
|
|
5048
|
+
for (const at of gaps ?? []) if (!shadowed(site, at) && !batchGaps.has(a.file)) batchGaps.set(a.file, at);
|
|
4280
5049
|
const routes = byRouterFile.get(a.file) ?? [];
|
|
4281
5050
|
for (const reg of registrations) {
|
|
4282
5051
|
if (shadowed(site, reg.at)) continue;
|
|
4283
|
-
const handler = reg.handlerRange ?? (reg.handler ? handlerOf(a, reg.handler, files) : null);
|
|
4284
|
-
|
|
4285
|
-
const
|
|
5052
|
+
const handler = reg.handlerRange ?? (reg.handler ? handlerOf(reg.handler.file ? analyse(reg.handler.file) : a, reg.handler, files) : null);
|
|
5053
|
+
const handlerSource = handler ? analyse(handler.file) : null;
|
|
5054
|
+
const knownBatchHandler = !reg.batch || reg.handlerRange !== null || handler && handlerSource && (handler.arrow || FUNCTION_EXPRESSION.test(handlerSource.code.slice(handler.start, handler.end)) || handlerSource.bodies.some((body) => handler.start <= body.start && body.end <= handler.end));
|
|
5055
|
+
if (!handler || !knownBatchHandler) {
|
|
5056
|
+
if (reg.batch && !batchGaps.has(a.file)) batchGaps.set(a.file, reg.at);
|
|
5057
|
+
continue;
|
|
5058
|
+
}
|
|
5059
|
+
const before = uses.filter((use) => !use.mount && use.at < reg.at && contextOf(a).covers(use.at, reg.at) && pathMatches(use.prefix, reg.path, site.framework)).flatMap((use) => use.args.filter((arg) => site.framework !== "express" || mountedFile(a, arg, files) === null));
|
|
4286
5060
|
const url = reg.path === null ? "(dynamic path)" : joinPath(site.prefix, reg.path);
|
|
4287
5061
|
const callbacks = flatten(a, reg.middleware);
|
|
4288
5062
|
const root = site.root ?? site;
|
|
@@ -4301,7 +5075,7 @@ function serverIndex(files) {
|
|
|
4301
5075
|
};
|
|
4302
5076
|
add2(handler, callbacks);
|
|
4303
5077
|
callbacks.forEach((callback, index2) => {
|
|
4304
|
-
const range = handlerOf(a, callback, files);
|
|
5078
|
+
const range = handlerOf(callback.file ? analyse(callback.file) : a, callback, files);
|
|
4305
5079
|
if (range) add2(range, callbacks.slice(0, index2));
|
|
4306
5080
|
});
|
|
4307
5081
|
}
|
|
@@ -4311,19 +5085,23 @@ function serverIndex(files) {
|
|
|
4311
5085
|
const root = sites.find((other) => other.a === a && other.range === null && other.name === arg.text && !other.root);
|
|
4312
5086
|
return root ? byRoot.get(root) : void 0;
|
|
4313
5087
|
};
|
|
5088
|
+
const fileRoutes = (file) => [.../* @__PURE__ */ new Set([
|
|
5089
|
+
...byRouterFile.get(file) ?? [],
|
|
5090
|
+
...sites.filter((other) => other.a.file === file && other.range === null && !other.root).flatMap((other) => byRoot.get(other) ?? [])
|
|
5091
|
+
])];
|
|
4314
5092
|
for (const site of sites) {
|
|
4315
5093
|
if (site.framework === "fastify") continue;
|
|
4316
5094
|
const { a } = site;
|
|
4317
|
-
const uses = opsOf(site).uses;
|
|
5095
|
+
const uses = opsOf(site, files).uses;
|
|
4318
5096
|
for (const use of uses) {
|
|
4319
5097
|
if (site.framework === "hono" && !use.mount) continue;
|
|
4320
5098
|
use.args.forEach((arg, index2) => {
|
|
4321
5099
|
const target = mountedFile(a, arg, files);
|
|
4322
|
-
const routes = target && target !== a.file ?
|
|
5100
|
+
const routes = target && target !== a.file ? fileRoutes(target) : localRoutes(a, arg);
|
|
4323
5101
|
if (!routes || routes.length === 0) return;
|
|
4324
5102
|
const isMiddleware = (x) => mountedFile(a, x, files) === null && localRoutes(a, x) === void 0;
|
|
4325
5103
|
const covers2 = (other) => site.framework === "hono" ? other.prefix === null || /\*$/.test(other.prefix) && pathMatches(other.prefix, use.prefix, "hono") : pathMatches(other.prefix, use.prefix, site.framework);
|
|
4326
|
-
const earlier = uses.filter((other) => !other.mount && other.at < use.at && covers2(other)).flatMap((other) => site.framework === "hono" ? other.args : other.args.filter(isMiddleware));
|
|
5104
|
+
const earlier = uses.filter((other) => !other.mount && other.at < use.at && contextOf(a).covers(other.at, use.at) && covers2(other)).flatMap((other) => site.framework === "hono" ? other.args : other.args.filter(isMiddleware));
|
|
4327
5105
|
const middleware = [...site.inherited, ...toRefs(a, earlier), ...toRefs(a, use.args.slice(0, index2).filter(isMiddleware))];
|
|
4328
5106
|
const prefix = joinPath(site.prefix, use.prefix ?? "/");
|
|
4329
5107
|
for (const route of routes) {
|
|
@@ -4341,13 +5119,27 @@ function serverIndex(files) {
|
|
|
4341
5119
|
byHandlerFile.set(route.file, list);
|
|
4342
5120
|
}
|
|
4343
5121
|
}
|
|
4344
|
-
const
|
|
5122
|
+
for (const site of sites) if (contextOf(site.a).limited) registrationGaps.add(site.a.file);
|
|
5123
|
+
const index = { byHandlerFile, batchGaps, factoryGaps, registrationGaps };
|
|
4345
5124
|
indexCache.set(files, index);
|
|
4346
5125
|
return index;
|
|
4347
5126
|
}
|
|
4348
5127
|
function nodeRoutesFor(file, files) {
|
|
4349
5128
|
return serverIndex(files).byHandlerFile.get(file) ?? [];
|
|
4350
5129
|
}
|
|
5130
|
+
function reportOpenapiBatchCoverage(ctx) {
|
|
5131
|
+
for (const file of serverIndex(ctx.files).registrationGaps) {
|
|
5132
|
+
ctx.reportIncomplete("engine/route-registration", `${file.path}: Route registration context reached a documented scope or graph limit. Conditional or truncated inherited middleware was not accepted as protection.`);
|
|
5133
|
+
}
|
|
5134
|
+
for (const [file, at] of serverIndex(ctx.files).factoryGaps) {
|
|
5135
|
+
const line = file.content.slice(0, at).split("\n").length;
|
|
5136
|
+
ctx.reportIncomplete("engine/router-factories", `${file.path}:${line}: Project router-factory analysis reached a resolution, expression, candidate or module-configuration limit. See the documented bounds; simplify the wrapper or scan its direct route entry separately.`);
|
|
5137
|
+
}
|
|
5138
|
+
for (const [file, at] of serverIndex(ctx.files).batchGaps) {
|
|
5139
|
+
const line = file.content.slice(0, at).split("\n").length;
|
|
5140
|
+
ctx.reportIncomplete("engine/openapi-routes", `${file.path}:${line}: OpenAPI batch routes were not fully resolved. Use static entries and resolvable handlers; limits are 8 spread levels and 256 entries per call.`);
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
4351
5143
|
function serverRoutesOf(file, files) {
|
|
4352
5144
|
const route = routeOf(file);
|
|
4353
5145
|
if (route) return [route];
|
|
@@ -4425,7 +5217,8 @@ function middlewareDefinition(ref, files) {
|
|
|
4425
5217
|
const local = member === null ? findFunction(a, root) : null;
|
|
4426
5218
|
if (local) return { name: callee, range: local, spec: null, inline };
|
|
4427
5219
|
const target = importOf(a, root, files);
|
|
4428
|
-
const
|
|
5220
|
+
const exported = member ?? (target && target.name !== "default" ? target.name : null);
|
|
5221
|
+
const range = target ? importedFunction(target, member) ?? (member ? propertyValue(analyse(target.file), member) : null) ?? (exported ? exportedMember(target.file, exported, files, 0) : null) : null;
|
|
4429
5222
|
return { name: callee, range, spec: null, inline };
|
|
4430
5223
|
}
|
|
4431
5224
|
function propertyValue(a, name) {
|
|
@@ -5381,7 +6174,7 @@ var IMPORT_SPEC = /(?:from|import|require)\s*\(?\s*['"]([^'"]+)['"]/g;
|
|
|
5381
6174
|
function normalizeSpec(spec, fromPath) {
|
|
5382
6175
|
if (spec.startsWith(".")) {
|
|
5383
6176
|
return {
|
|
5384
|
-
key: moduleKey(
|
|
6177
|
+
key: moduleKey(posix2.normalize(posix2.join(posix2.dirname(fromPath), spec))),
|
|
5385
6178
|
alias: false
|
|
5386
6179
|
};
|
|
5387
6180
|
}
|
|
@@ -6008,13 +6801,13 @@ function quotedValue(text) {
|
|
|
6008
6801
|
function matcherEntry(text) {
|
|
6009
6802
|
const trimmed = text.trimStart();
|
|
6010
6803
|
if (trimmed.startsWith("{")) {
|
|
6011
|
-
const
|
|
6012
|
-
if (
|
|
6013
|
-
if (topLevelPropertyStart(
|
|
6804
|
+
const object2 = sliceDelimited(trimmed, "{", "}");
|
|
6805
|
+
if (object2 === null) return null;
|
|
6806
|
+
if (topLevelPropertyStart(object2, "has") !== null || topLevelPropertyStart(object2, "missing") !== null) {
|
|
6014
6807
|
return { conditional: true };
|
|
6015
6808
|
}
|
|
6016
|
-
const source = topLevelPropertyStart(
|
|
6017
|
-
const pattern2 = source === null ? null : quotedValue(
|
|
6809
|
+
const source = topLevelPropertyStart(object2, "source");
|
|
6810
|
+
const pattern2 = source === null ? null : quotedValue(object2.slice(source));
|
|
6018
6811
|
return pattern2 === null ? null : { pattern: pattern2 };
|
|
6019
6812
|
}
|
|
6020
6813
|
const pattern = quotedValue(trimmed);
|
|
@@ -8552,7 +9345,106 @@ function shouldRunRule(id, only, skip) {
|
|
|
8552
9345
|
}
|
|
8553
9346
|
|
|
8554
9347
|
// src/engine.ts
|
|
8555
|
-
import { createHash as
|
|
9348
|
+
import { createHash as createHash3 } from "crypto";
|
|
9349
|
+
|
|
9350
|
+
// src/diagnostics.ts
|
|
9351
|
+
var ScanInputError = class extends TypeError {
|
|
9352
|
+
code = "INVALID_ARGUMENT";
|
|
9353
|
+
};
|
|
9354
|
+
function diagnosticCodeOf(error) {
|
|
9355
|
+
if (error.kind === "crashed") return "RULE_EXECUTION_FAILED";
|
|
9356
|
+
if (["engine/openapi-routes", "engine/router-factories", "engine/route-registration"].includes(error.ruleId)) return "ROUTE_UNRESOLVED";
|
|
9357
|
+
if (error.ruleId === "engine/findings-limit") return "FINDINGS_LIMIT";
|
|
9358
|
+
if (error.ruleId.startsWith("gitleak/")) return "GIT_CHECK_INCOMPLETE";
|
|
9359
|
+
return "ANALYSIS_INCOMPLETE";
|
|
9360
|
+
}
|
|
9361
|
+
|
|
9362
|
+
// src/scan-control.ts
|
|
9363
|
+
var ScanCancelledError = class extends Error {
|
|
9364
|
+
code = "SCAN_CANCELLED";
|
|
9365
|
+
constructor() {
|
|
9366
|
+
super("Scan cancelled.");
|
|
9367
|
+
this.name = "AbortError";
|
|
9368
|
+
}
|
|
9369
|
+
};
|
|
9370
|
+
var ScanProgressError = class extends Error {
|
|
9371
|
+
code = "PROGRESS_CALLBACK_FAILED";
|
|
9372
|
+
constructor(cause) {
|
|
9373
|
+
super("Scan progress callback failed.", { cause });
|
|
9374
|
+
}
|
|
9375
|
+
};
|
|
9376
|
+
function checkScanCancelled(signal) {
|
|
9377
|
+
if (signal?.aborted) throw new ScanCancelledError();
|
|
9378
|
+
}
|
|
9379
|
+
async function waitForProgress(value, signal) {
|
|
9380
|
+
if (!signal) {
|
|
9381
|
+
await value;
|
|
9382
|
+
return;
|
|
9383
|
+
}
|
|
9384
|
+
let onAbort = () => {
|
|
9385
|
+
};
|
|
9386
|
+
const aborted = new Promise((_, reject) => {
|
|
9387
|
+
onAbort = () => reject(new ScanCancelledError());
|
|
9388
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
9389
|
+
if (signal.aborted) onAbort();
|
|
9390
|
+
});
|
|
9391
|
+
try {
|
|
9392
|
+
await Promise.race([Promise.resolve(value), aborted]);
|
|
9393
|
+
} finally {
|
|
9394
|
+
signal.removeEventListener("abort", onAbort);
|
|
9395
|
+
}
|
|
9396
|
+
}
|
|
9397
|
+
function createScanControl(options) {
|
|
9398
|
+
const { signal, onProgress } = options;
|
|
9399
|
+
return {
|
|
9400
|
+
active: signal !== void 0 || onProgress !== void 0,
|
|
9401
|
+
check: () => checkScanCancelled(signal),
|
|
9402
|
+
async checkpoint(progress) {
|
|
9403
|
+
checkScanCancelled(signal);
|
|
9404
|
+
await new Promise((resolve3) => setImmediate(resolve3));
|
|
9405
|
+
checkScanCancelled(signal);
|
|
9406
|
+
if (onProgress) {
|
|
9407
|
+
try {
|
|
9408
|
+
await waitForProgress(onProgress(Object.freeze({ ...progress })), signal);
|
|
9409
|
+
} catch (error) {
|
|
9410
|
+
if (signal?.aborted || error instanceof ScanCancelledError) throw new ScanCancelledError();
|
|
9411
|
+
throw new ScanProgressError(error);
|
|
9412
|
+
}
|
|
9413
|
+
}
|
|
9414
|
+
checkScanCancelled(signal);
|
|
9415
|
+
}
|
|
9416
|
+
};
|
|
9417
|
+
}
|
|
9418
|
+
|
|
9419
|
+
// src/exclusions.ts
|
|
9420
|
+
var MAX_EXCLUSIONS = 64;
|
|
9421
|
+
var MAX_EXCLUSION_LENGTH = 512;
|
|
9422
|
+
var SEGMENT = String.raw`[^\s/\\](?:[^/\\]*[^\s/\\])?`;
|
|
9423
|
+
var EXCLUSION_PATH_PATTERN = String.raw`^(?![!])(?!.*[:*?\[\]{}\u0000-\u001f\u007f-\u009f\u2028\u2029])(?!(?:.*[/\\])?\.{1,2}(?:[/\\]|$))` + SEGMENT + String.raw`(?:[/\\]` + SEGMENT + String.raw`)*[/\\]?$`;
|
|
9424
|
+
var PATH = new RegExp(EXCLUSION_PATH_PATTERN);
|
|
9425
|
+
function isExclusionPath(value) {
|
|
9426
|
+
return typeof value === "string" && value.length > 0 && value.length <= MAX_EXCLUSION_LENGTH && PATH.test(value);
|
|
9427
|
+
}
|
|
9428
|
+
function createExclusions(values = []) {
|
|
9429
|
+
if (values.length > MAX_EXCLUSIONS || !values.every(isExclusionPath)) throw new TypeError("Exclusions must be at most 64 literal relative paths without traversal, wildcards or control characters.");
|
|
9430
|
+
const requested = [...new Set(values.map((value) => value.replace(/\\/g, "/").replace(/\/$/, "")))];
|
|
9431
|
+
const matched = /* @__PURE__ */ new Set();
|
|
9432
|
+
return {
|
|
9433
|
+
requested,
|
|
9434
|
+
matches(path) {
|
|
9435
|
+
const normalized = path.replace(/\\/g, "/").replace(/\/$/, "");
|
|
9436
|
+
let excluded = false;
|
|
9437
|
+
for (const prefix of requested) if (normalized === prefix || normalized.startsWith(prefix + "/")) {
|
|
9438
|
+
matched.add(prefix);
|
|
9439
|
+
excluded = true;
|
|
9440
|
+
}
|
|
9441
|
+
return excluded;
|
|
9442
|
+
},
|
|
9443
|
+
matched: () => [...matched].sort()
|
|
9444
|
+
};
|
|
9445
|
+
}
|
|
9446
|
+
|
|
9447
|
+
// src/engine.ts
|
|
8556
9448
|
var SEVERITY_ORDER = { P0: 0, P1: 1, P2: 2 };
|
|
8557
9449
|
var CONFIDENCE_ORDER = { certain: 0, likely: 1 };
|
|
8558
9450
|
function dedupe(findings) {
|
|
@@ -8581,7 +9473,7 @@ function sanitize(findings, files) {
|
|
|
8581
9473
|
const file = f.file === null ? void 0 : byPath.get(f.file);
|
|
8582
9474
|
const source = f.line === null ? file?.content : file?.lines[f.line - 1];
|
|
8583
9475
|
return source === void 0 ? {} : {
|
|
8584
|
-
sourceFingerprint:
|
|
9476
|
+
sourceFingerprint: createHash3("sha256").update(source.trim(), "utf8").digest("hex")
|
|
8585
9477
|
};
|
|
8586
9478
|
};
|
|
8587
9479
|
return findings.map((f) => ({
|
|
@@ -8616,9 +9508,9 @@ function sanitizeSkippedForOutput(items) {
|
|
|
8616
9508
|
}));
|
|
8617
9509
|
}
|
|
8618
9510
|
function downgradeExampleContext(findings, files) {
|
|
8619
|
-
const
|
|
9511
|
+
const examples2 = new Set(files.filter((f) => f.isExampleContext).map((f) => f.path));
|
|
8620
9512
|
return findings.map(
|
|
8621
|
-
(f) => f.file !== null &&
|
|
9513
|
+
(f) => f.file !== null && examples2.has(f.file) ? { ...f, confidence: "likely" } : f
|
|
8622
9514
|
);
|
|
8623
9515
|
}
|
|
8624
9516
|
function suppressIgnoredLines(findings, files) {
|
|
@@ -8671,20 +9563,36 @@ function sortFindings(findings) {
|
|
|
8671
9563
|
}
|
|
8672
9564
|
async function scan(root, options = {}) {
|
|
8673
9565
|
const started = Date.now();
|
|
9566
|
+
const exclusions = createExclusions(options.exclude);
|
|
9567
|
+
const control = createScanControl(options);
|
|
9568
|
+
control.check();
|
|
9569
|
+
const progress = {
|
|
9570
|
+
phase: "discovery",
|
|
9571
|
+
filesCompleted: 0,
|
|
9572
|
+
filesTotal: null,
|
|
9573
|
+
projectRulesCompleted: 0,
|
|
9574
|
+
projectRulesTotal: null
|
|
9575
|
+
};
|
|
9576
|
+
if (control.active) await control.checkpoint(progress);
|
|
8674
9577
|
const gitExecutable = resolveGitExecutable(root);
|
|
8675
9578
|
const git2 = detectGitRepo(root, gitExecutable);
|
|
8676
9579
|
const honorIgnoreMarkers = options.honorIgnoreMarkers !== false;
|
|
8677
|
-
const { files, skipped, ignored, vendored } = collectFiles(root, git2 === "repo", gitExecutable, {}, honorIgnoreMarkers);
|
|
9580
|
+
const { files, skipped, ignored, vendored } = collectFiles(root, git2 === "repo", gitExecutable, {}, honorIgnoreMarkers, exclusions.matches);
|
|
8678
9581
|
const findings = [];
|
|
8679
9582
|
const errors = [];
|
|
8680
9583
|
const fileRules = FILE_RULES.filter((rule2) => shouldRunRule(rule2.id, options.only ?? [], options.skip ?? []));
|
|
8681
9584
|
const projectRules = PROJECT_RULES.filter((rule2) => shouldRunRule(rule2.id, options.only ?? [], options.skip ?? []));
|
|
9585
|
+
progress.filesTotal = files.length;
|
|
9586
|
+
progress.projectRulesTotal = projectRules.length;
|
|
9587
|
+
progress.phase = "files";
|
|
9588
|
+
if (control.active) await control.checkpoint(progress);
|
|
8682
9589
|
const incompleteSeen = /* @__PURE__ */ new Set();
|
|
8683
9590
|
const ctx = {
|
|
8684
9591
|
root,
|
|
8685
9592
|
files,
|
|
8686
9593
|
git: git2,
|
|
8687
9594
|
gitExecutable,
|
|
9595
|
+
excludePath: exclusions.matches,
|
|
8688
9596
|
// 统一记录不完整状态,避免重复提示。
|
|
8689
9597
|
reportIncomplete: (ruleId, message) => {
|
|
8690
9598
|
if (incompleteSeen.has(`${ruleId} ${message}`)) return;
|
|
@@ -8693,6 +9601,7 @@ async function scan(root, options = {}) {
|
|
|
8693
9601
|
}
|
|
8694
9602
|
};
|
|
8695
9603
|
for (const file of files) {
|
|
9604
|
+
control.check();
|
|
8696
9605
|
for (const rule2 of fileRules) {
|
|
8697
9606
|
try {
|
|
8698
9607
|
if (!rule2.appliesTo(file)) continue;
|
|
@@ -8701,13 +9610,29 @@ async function scan(root, options = {}) {
|
|
|
8701
9610
|
errors.push({ ruleId: rule2.id, file: file.path, message: messageOf(err), kind: "crashed" });
|
|
8702
9611
|
}
|
|
8703
9612
|
}
|
|
9613
|
+
progress.filesCompleted++;
|
|
9614
|
+
if (control.active && progress.filesCompleted % 32 === 0) await control.checkpoint(progress);
|
|
8704
9615
|
}
|
|
9616
|
+
if (control.active) await control.checkpoint(progress);
|
|
8705
9617
|
for (const rule2 of projectRules) {
|
|
9618
|
+
progress.phase = rule2.id.startsWith("gitleak/") ? "history" : "project";
|
|
9619
|
+
if (control.active) await control.checkpoint(progress);
|
|
8706
9620
|
try {
|
|
8707
9621
|
findings.push(...await rule2.check(ctx));
|
|
8708
9622
|
} catch (err) {
|
|
8709
9623
|
errors.push({ ruleId: rule2.id, file: null, message: messageOf(err), kind: "crashed" });
|
|
8710
9624
|
}
|
|
9625
|
+
control.check();
|
|
9626
|
+
progress.projectRulesCompleted++;
|
|
9627
|
+
}
|
|
9628
|
+
progress.phase = "finalize";
|
|
9629
|
+
if (control.active) await control.checkpoint(progress);
|
|
9630
|
+
if ([...fileRules, ...projectRules].some((rule2) => /^(?:api|auth|injection|ssrf|redirect|webhook)\//.test(rule2.id))) {
|
|
9631
|
+
try {
|
|
9632
|
+
reportOpenapiBatchCoverage(ctx);
|
|
9633
|
+
} catch (err) {
|
|
9634
|
+
errors.push({ ruleId: "engine/openapi-routes", file: null, message: messageOf(err), kind: "crashed" });
|
|
9635
|
+
}
|
|
8711
9636
|
}
|
|
8712
9637
|
const deduped = dedupe(downgradeExampleContext(findings, files));
|
|
8713
9638
|
const { kept, ignored: ignoredFindings } = honorIgnoreMarkers ? suppressIgnoredLines(deduped, files) : { kept: deduped, ignored: [] };
|
|
@@ -8723,12 +9648,14 @@ async function scan(root, options = {}) {
|
|
|
8723
9648
|
);
|
|
8724
9649
|
return false;
|
|
8725
9650
|
});
|
|
8726
|
-
|
|
9651
|
+
const result = {
|
|
9652
|
+
...exclusions.requested.length ? { exclusions: { requested: exclusions.requested.map(clean), matched: exclusions.matched().map(clean) } } : {},
|
|
8727
9653
|
findings: sanitize(bounded, files),
|
|
8728
9654
|
filesScanned: files.length,
|
|
8729
9655
|
durationMs: Date.now() - started,
|
|
8730
9656
|
errors: errors.map((e) => ({
|
|
8731
9657
|
...e,
|
|
9658
|
+
code: e.code ?? diagnosticCodeOf(e),
|
|
8732
9659
|
file: e.file === null ? null : clean(e.file),
|
|
8733
9660
|
message: clean(e.message)
|
|
8734
9661
|
})),
|
|
@@ -8746,14 +9673,113 @@ async function scan(root, options = {}) {
|
|
|
8746
9673
|
// 主动忽略不影响完整性;错误、跳过或零文件扫描均标记为未完成。
|
|
8747
9674
|
partial: errors.length > 0 || skipped.length > 0 || files.length === 0
|
|
8748
9675
|
};
|
|
9676
|
+
progress.phase = "complete";
|
|
9677
|
+
if (control.active) await control.checkpoint(progress);
|
|
9678
|
+
return result;
|
|
8749
9679
|
}
|
|
8750
9680
|
function messageOf(err) {
|
|
8751
9681
|
if (err instanceof Error) return err.message;
|
|
8752
9682
|
return String(err);
|
|
8753
9683
|
}
|
|
8754
9684
|
|
|
9685
|
+
// src/rules/examples.ts
|
|
9686
|
+
var publicEnv = {
|
|
9687
|
+
context: "Server-only environment value",
|
|
9688
|
+
before: "NEXT_PUBLIC_SERVER_TOKEN=<private value>",
|
|
9689
|
+
after: "SERVER_TOKEN=<server-only value>",
|
|
9690
|
+
limitation: "Use the value only in server code. Rotate any exposed credential; renaming does not revoke it."
|
|
9691
|
+
};
|
|
9692
|
+
var auth = {
|
|
9693
|
+
context: "Supabase route with a request-scoped session client and a separate admin client",
|
|
9694
|
+
before: "await admin.from('items').delete().eq('id', id);",
|
|
9695
|
+
after: "const { data: { user }, error } = await sessionClient.auth.getUser();\nif (error || !user) return new Response(null, { status: 401 });\nif (!await canDelete(user.id, id)) return new Response(null, { status: 403 });\nawait admin.from('items').delete().eq('id', id);",
|
|
9696
|
+
limitation: "Bind sessionClient to the caller. Implement canDelete with actual resource ownership or permissions; authentication alone is insufficient."
|
|
9697
|
+
};
|
|
9698
|
+
var firestore = {
|
|
9699
|
+
context: "Firestore document ownership",
|
|
9700
|
+
before: "match /users/{uid} { allow read, write: if true; }",
|
|
9701
|
+
after: "match /users/{uid} {\n allow read, write: if request.auth != null && request.auth.uid == uid;\n}",
|
|
9702
|
+
limitation: "Firestore syntax only. Review field validation and overlapping rules; adapt ownership to the data model."
|
|
9703
|
+
};
|
|
9704
|
+
var cors = {
|
|
9705
|
+
context: "Express cors middleware with credentialed access from one trusted site",
|
|
9706
|
+
before: "app.use(cors({ origin: true, credentials: true }));",
|
|
9707
|
+
after: "app.use(cors({ origin: 'https://app.example.com', credentials: true }));",
|
|
9708
|
+
limitation: "Replace the example origin with an exact trusted origin. CORS does not replace authentication, authorization or CSRF controls."
|
|
9709
|
+
};
|
|
9710
|
+
var examples = {
|
|
9711
|
+
"api/admin-db-access-without-auth": auth,
|
|
9712
|
+
"api/db-write-without-auth": auth,
|
|
9713
|
+
"exposure/private-name-in-public-env": publicEnv,
|
|
9714
|
+
"exposure/secret-in-public-env": publicEnv,
|
|
9715
|
+
"exposure/supabase-service-role-in-client": {
|
|
9716
|
+
...publicEnv,
|
|
9717
|
+
context: "Supabase admin credential",
|
|
9718
|
+
limitation: "Never ship an admin key to the client. Use a publishable/anon key with appropriate RLS for client access; rotate exposed admin credentials."
|
|
9719
|
+
},
|
|
9720
|
+
"cors/reflected-origin-with-credentials": cors,
|
|
9721
|
+
"cors/wildcard-with-credentials": { ...cors, before: "app.use(cors({ origin: '*', credentials: true }));" },
|
|
9722
|
+
"firebase/open-rules": firestore,
|
|
9723
|
+
"firebase/test-mode-rules": { ...firestore, before: "allow read, write: if request.time < timestamp.date(2030, 1, 1);" },
|
|
9724
|
+
"supabase/rls-not-enabled": {
|
|
9725
|
+
context: "Supabase table with a UUID owner_id column",
|
|
9726
|
+
before: "CREATE TABLE public.notes (id uuid PRIMARY KEY, owner_id uuid NOT NULL, body text);",
|
|
9727
|
+
after: "ALTER TABLE public.notes ENABLE ROW LEVEL SECURITY;\nCREATE POLICY owner_access ON public.notes TO authenticated\nUSING ((SELECT auth.uid()) = owner_id)\nWITH CHECK ((SELECT auth.uid()) = owner_id);",
|
|
9728
|
+
limitation: "Apply ownership checks appropriate to the application. Review grants and other policies; service-role access bypasses RLS."
|
|
9729
|
+
},
|
|
9730
|
+
"auth/unverified-session": {
|
|
9731
|
+
context: "Server-side Supabase identity verification",
|
|
9732
|
+
before: "const { data: { session } } = await supabase.auth.getSession();\nif (!session) return new Response(null, { status: 401 });",
|
|
9733
|
+
after: "const { data: { user }, error } = await supabase.auth.getUser();\nif (error || !user) return new Response(null, { status: 401 });",
|
|
9734
|
+
limitation: "Use a client bound to the current request. Verified identity still requires resource-specific authorization."
|
|
9735
|
+
},
|
|
9736
|
+
"injection/sql": {
|
|
9737
|
+
context: "PostgreSQL driver with bound parameters",
|
|
9738
|
+
before: 'await db.query("SELECT * FROM users WHERE id = " + id);',
|
|
9739
|
+
after: "await db.query('SELECT * FROM users WHERE id = $1', [id]);",
|
|
9740
|
+
limitation: "Use the parameter syntax of your actual driver. Parameters do not substitute table names or authorize access."
|
|
9741
|
+
},
|
|
9742
|
+
"injection/command": {
|
|
9743
|
+
context: "A fixed executable with separately validated arguments",
|
|
9744
|
+
before: "exec('tool --input ' + requestValue);",
|
|
9745
|
+
after: "execFile(TRUSTED_EXECUTABLE, ['--input', validatedValue], { shell: false });",
|
|
9746
|
+
limitation: "Set TRUSTED_EXECUTABLE to a trusted absolute path. Validate argument meaning and filesystem access; never let the caller choose the executable."
|
|
9747
|
+
},
|
|
9748
|
+
"redirect/open": {
|
|
9749
|
+
context: "Named destinations on a fixed application origin",
|
|
9750
|
+
before: "return Response.redirect(requestedUrl);",
|
|
9751
|
+
after: "const path = destination === 'settings' ? '/settings' : '/dashboard';\nreturn Response.redirect(new URL(path, APP_ORIGIN));",
|
|
9752
|
+
limitation: "APP_ORIGIN must be trusted configuration, not a request header. Do not replace the fixed paths with caller-controlled URLs."
|
|
9753
|
+
},
|
|
9754
|
+
"ssrf/request-url": {
|
|
9755
|
+
context: "Named server resources instead of caller-provided hosts",
|
|
9756
|
+
before: "return fetch(requestedUrl);",
|
|
9757
|
+
after: "if (resource !== 'status') return new Response(null, { status: 400 });\nreturn fetch('https://api.example.com/status', { redirect: 'error' });",
|
|
9758
|
+
limitation: "Use a destination you control. Review DNS and egress restrictions; a URL allowlist alone is not a complete SSRF defense."
|
|
9759
|
+
},
|
|
9760
|
+
"webhook/unverified-signature": {
|
|
9761
|
+
context: "Stripe webhook using the original request-body bytes",
|
|
9762
|
+
before: "const event = JSON.parse(rawBody);\nawait handle(event);",
|
|
9763
|
+
after: "let event;\ntry { event = stripe.webhooks.constructEvent(rawBody, signature, SERVER_SIGNING_SECRET); }\ncatch { return new Response(null, { status: 400 }); }\nawait handle(event);",
|
|
9764
|
+
limitation: "Preserve raw bytes and use the endpoint signing secret. Verification must finish before side effects; implement replay/idempotency handling separately."
|
|
9765
|
+
}
|
|
9766
|
+
};
|
|
9767
|
+
function fixExampleFor(ruleId) {
|
|
9768
|
+
if (SECRET_PATTERNS.some((pattern) => !pattern.publicByDesign && ruleId === `secrets/hardcoded/${pattern.id}`)) return {
|
|
9769
|
+
context: "A credential used only by a server runtime",
|
|
9770
|
+
before: "const apiKey = '<private value>';",
|
|
9771
|
+
after: "const apiKey = process.env.SERVICE_API_KEY;\nif (!apiKey) throw new Error('Server credential is missing');",
|
|
9772
|
+
limitation: "Use trusted server-side configuration, never a public/client environment prefix. Rotate the old credential and review history and deployed bundles."
|
|
9773
|
+
};
|
|
9774
|
+
if (!Object.hasOwn(examples, ruleId)) return null;
|
|
9775
|
+
return { ...examples[ruleId] };
|
|
9776
|
+
}
|
|
9777
|
+
|
|
8755
9778
|
// src/rules/catalog.ts
|
|
8756
|
-
var rule = (id, name, severity, confidence, scope, limitation) =>
|
|
9779
|
+
var rule = (id, name, severity, confidence, scope, limitation) => {
|
|
9780
|
+
const example = fixExampleFor(id);
|
|
9781
|
+
return { id, name, severity, confidence, scope, limitation, reportsFindings: true, ...example ? { example } : {} };
|
|
9782
|
+
};
|
|
8757
9783
|
var RULE_CATALOG = [
|
|
8758
9784
|
rule(
|
|
8759
9785
|
"api/admin-db-access-without-auth",
|
|
@@ -8768,8 +9794,8 @@ var RULE_CATALOG = [
|
|
|
8768
9794
|
"Database write without a recognized auth guard",
|
|
8769
9795
|
"P1",
|
|
8770
9796
|
"likely",
|
|
8771
|
-
"Server routes and Next.js Server Functions in Next.js, SvelteKit (endpoints and form actions), Nuxt, Remix/React Router, Astro, Express, Hono, and Fastify.",
|
|
8772
|
-
"Session-scoped clients and indirect guards may need manual review. In Express, Hono, and Fastify routes, writes in called project functions are followed two levels deep; file-based routes report writes in the route file only.
|
|
9797
|
+
"Server routes and Next.js Server Functions in Next.js, SvelteKit (endpoints and form actions), Nuxt, Remix/React Router, Astro, Express, Hono (including OpenAPIHono.openapi and openapiRoutes), and Fastify.",
|
|
9798
|
+
"Session-scoped clients and indirect guards may need manual review. In Express, Hono, and Fastify routes, writes in called project functions are followed two levels deep; file-based routes report writes in the route file only. OpenAPI configuration follows constants and static ESM imports/re-exports within eight resolution steps. Unresolved configuration does not suppress findings; unresolved batch entries or handlers mark coverage incomplete. OpenAPI security declarations are not authentication, and arbitrary module side effects are not modelled."
|
|
8773
9799
|
),
|
|
8774
9800
|
rule(
|
|
8775
9801
|
"auth/unverified-session",
|
|
@@ -8945,10 +9971,7 @@ function verdictOf(findings) {
|
|
|
8945
9971
|
return { blocking, minor, unsure };
|
|
8946
9972
|
}
|
|
8947
9973
|
|
|
8948
|
-
// src/
|
|
8949
|
-
function listRules() {
|
|
8950
|
-
return RULE_CATALOG.map((rule2) => ({ ...rule2 }));
|
|
8951
|
-
}
|
|
9974
|
+
// src/summary.ts
|
|
8952
9975
|
function summarize(result) {
|
|
8953
9976
|
const verdict = verdictOf(result.findings);
|
|
8954
9977
|
const findings = result.changeView?.totalFindings ?? result.findings.length;
|
|
@@ -8962,37 +9985,69 @@ function summarize(result) {
|
|
|
8962
9985
|
exitCode: blocking > 0 ? 1 : findings > 0 ? 2 : partial ? 3 : 0
|
|
8963
9986
|
};
|
|
8964
9987
|
}
|
|
9988
|
+
|
|
9989
|
+
// src/build-info.ts
|
|
9990
|
+
var VERSION = true ? "0.8.0" : "0.0.0-dev";
|
|
9991
|
+
var identity = typeof define_CANSHIP_BUILD_INFO_default === "object" ? define_CANSHIP_BUILD_INFO_default : { version: VERSION, channel: "development", revision: null, dirty: null };
|
|
9992
|
+
function getBuildInfo() {
|
|
9993
|
+
return { ...identity };
|
|
9994
|
+
}
|
|
9995
|
+
function getCapabilities() {
|
|
9996
|
+
return {
|
|
9997
|
+
staticScan: { network: false, projectCodeExecution: false, projectWrites: false },
|
|
9998
|
+
fileOutputs: "explicit-only",
|
|
9999
|
+
onlineValidation: true,
|
|
10000
|
+
onlineValidationPolicy: { cliOnly: true, defaultEnabled: false, confirmation: "plan-digest", credentials: false, responseBodiesRetained: false }
|
|
10001
|
+
};
|
|
10002
|
+
}
|
|
10003
|
+
|
|
10004
|
+
// src/index.ts
|
|
10005
|
+
function listRules() {
|
|
10006
|
+
return RULE_CATALOG.map((rule2) => ({ ...rule2, ...rule2.example ? { example: { ...rule2.example } } : {} }));
|
|
10007
|
+
}
|
|
8965
10008
|
async function scan2(root, options = {}) {
|
|
8966
|
-
if (typeof root !== "string" || root.length === 0) throw new
|
|
10009
|
+
if (typeof root !== "string" || root.length === 0) throw new ScanInputError("Scan root must be a non-empty path.");
|
|
8967
10010
|
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
8968
|
-
throw new
|
|
10011
|
+
throw new ScanInputError("Scan options must be an object.");
|
|
8969
10012
|
}
|
|
8970
|
-
const allowed = /* @__PURE__ */ new Set(["only", "skip", "honorIgnoreMarkers", "noExcerpts"]);
|
|
8971
|
-
if (Object.keys(options).some((key) => !allowed.has(key))) throw new
|
|
10013
|
+
const allowed = /* @__PURE__ */ new Set(["only", "skip", "honorIgnoreMarkers", "noExcerpts", "signal", "onProgress", "exclude"]);
|
|
10014
|
+
if (Object.keys(options).some((key) => !allowed.has(key))) throw new ScanInputError("Unknown scan option.");
|
|
10015
|
+
if (options.signal !== void 0 && (options.signal === null || typeof options.signal !== "object" || typeof options.signal.aborted !== "boolean" || typeof options.signal.addEventListener !== "function" || typeof options.signal.removeEventListener !== "function")) throw new ScanInputError("Expected an AbortSignal.");
|
|
10016
|
+
if (options.onProgress !== void 0 && typeof options.onProgress !== "function") throw new ScanInputError("Expected a progress callback.");
|
|
10017
|
+
if (options.exclude !== void 0 && (!Array.isArray(options.exclude) || options.exclude.length > MAX_EXCLUSIONS || !options.exclude.every(isExclusionPath))) throw new ScanInputError("Expected at most 64 literal relative exclusion paths.");
|
|
8972
10018
|
for (const name of ["honorIgnoreMarkers", "noExcerpts"]) {
|
|
8973
|
-
if (options[name] !== void 0 && typeof options[name] !== "boolean") throw new
|
|
10019
|
+
if (options[name] !== void 0 && typeof options[name] !== "boolean") throw new ScanInputError("Expected a boolean scan option.");
|
|
8974
10020
|
}
|
|
8975
10021
|
for (const name of ["only", "skip"]) {
|
|
8976
10022
|
const values = options[name];
|
|
8977
10023
|
if (values !== void 0 && (!Array.isArray(values) || [...values].some((value) => typeof value !== "string" || !isKnownSelector(value)))) {
|
|
8978
|
-
throw new
|
|
10024
|
+
throw new ScanInputError("Rule selectors must be an array of known IDs or namespaces.");
|
|
8979
10025
|
}
|
|
8980
10026
|
}
|
|
8981
|
-
if (options.only?.length && options.skip?.length) throw new
|
|
10027
|
+
if (options.only?.length && options.skip?.length) throw new ScanInputError("only and skip are mutually exclusive.");
|
|
10028
|
+
checkScanCancelled(options.signal);
|
|
8982
10029
|
const directory = resolve2(root);
|
|
8983
10030
|
try {
|
|
8984
10031
|
if (!statSync3(directory).isDirectory()) throw new Error("not a directory");
|
|
8985
10032
|
} catch {
|
|
8986
|
-
throw new Error("Scan root must be an accessible directory.");
|
|
10033
|
+
throw Object.assign(new Error("Scan root must be an accessible directory."), { code: "SCAN_ROOT_UNAVAILABLE" });
|
|
8987
10034
|
}
|
|
10035
|
+
const omitExcerpts = options.noExcerpts === true;
|
|
8988
10036
|
const result = await scan(directory, {
|
|
8989
10037
|
only: [...options.only ?? []],
|
|
8990
10038
|
skip: [...options.skip ?? []],
|
|
8991
|
-
honorIgnoreMarkers: options.honorIgnoreMarkers ?? true
|
|
10039
|
+
honorIgnoreMarkers: options.honorIgnoreMarkers ?? true,
|
|
10040
|
+
exclude: [...options.exclude ?? []],
|
|
10041
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
10042
|
+
...options.onProgress === void 0 ? {} : { onProgress: options.onProgress }
|
|
8992
10043
|
});
|
|
8993
|
-
return
|
|
10044
|
+
return omitExcerpts ? { ...result, findings: result.findings.map((finding) => ({ ...finding, excerpt: null })) } : result;
|
|
8994
10045
|
}
|
|
8995
10046
|
export {
|
|
10047
|
+
ScanCancelledError,
|
|
10048
|
+
ScanProgressError,
|
|
10049
|
+
getBuildInfo,
|
|
10050
|
+
getCapabilities,
|
|
8996
10051
|
listRules,
|
|
8997
10052
|
scan2 as scan,
|
|
8998
10053
|
summarize
|