hullproof 0.0.0-stage → 0.5.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.
Files changed (363) hide show
  1. package/LICENSE +222 -0
  2. package/NOTICE +32 -0
  3. package/README.md +313 -2
  4. package/checks/db.firebase-rules-not-open.json +24 -0
  5. package/checks/db.rls-every-table.json +25 -0
  6. package/checks/log.no-credential-in-logs.json +30 -0
  7. package/checks/secrets.no-env-file-committed.json +31 -0
  8. package/checks/secrets.no-secret-in-public-env.json +37 -0
  9. package/checks/storage.firebase-rules-not-open.json +22 -0
  10. package/checks/supply.no-untrusted-event-in-run.json +23 -0
  11. package/checks/web.cors-no-reflected-origin.json +31 -0
  12. package/detectors/ai.json +25 -0
  13. package/detectors/auth.json +27 -0
  14. package/detectors/ci.json +19 -0
  15. package/detectors/containers.json +8 -0
  16. package/detectors/data_stores.json +38 -0
  17. package/detectors/frameworks.json +94 -0
  18. package/detectors/hosting.json +44 -0
  19. package/detectors/iac.json +43 -0
  20. package/detectors/languages.json +48 -0
  21. package/detectors/mobile.json +22 -0
  22. package/detectors/package_managers.json +39 -0
  23. package/detectors/payments.json +19 -0
  24. package/detectors/runtimes.json +17 -0
  25. package/dist/checks/code/admin-role.js +46 -0
  26. package/dist/checks/code/owner-filter.js +91 -0
  27. package/dist/checks/code/shared.js +84 -0
  28. package/dist/checks/code/webhook-verify.js +77 -0
  29. package/dist/checks/context.js +64 -0
  30. package/dist/checks/map/config-checks.js +74 -0
  31. package/dist/checks/map/entry-checks.js +97 -0
  32. package/dist/checks/map/shared.js +44 -0
  33. package/dist/checks/ops.js +307 -0
  34. package/dist/checks/pro-map.js +1 -0
  35. package/dist/checks/run.js +92 -0
  36. package/dist/checks/types.js +1 -0
  37. package/dist/ci/comment.js +44 -0
  38. package/dist/ci/sarif-check.js +157 -0
  39. package/dist/ci/sarif.js +61 -0
  40. package/dist/cli.js +358 -0
  41. package/dist/commands/ci.js +115 -0
  42. package/dist/commands/git.js +58 -0
  43. package/dist/commands/hooks.js +30 -0
  44. package/dist/commands/hunt-pro.js +1 -0
  45. package/dist/commands/hunt.js +86 -0
  46. package/dist/commands/map.js +33 -0
  47. package/dist/commands/mcp.js +17 -0
  48. package/dist/commands/pack.js +48 -0
  49. package/dist/commands/prove-pro.js +1 -0
  50. package/dist/commands/prove.js +173 -0
  51. package/dist/commands/report-gate.js +92 -0
  52. package/dist/commands/scan.js +158 -0
  53. package/dist/confirm/agreement.js +18 -0
  54. package/dist/confirm/chain.js +84 -0
  55. package/dist/confirm/confirm.js +112 -0
  56. package/dist/confirm/honesty.js +75 -0
  57. package/dist/confirm/lead.js +92 -0
  58. package/dist/confirm/merge.js +88 -0
  59. package/dist/confirm/query.js +145 -0
  60. package/dist/confirm/rule.js +86 -0
  61. package/dist/confirm/store.js +42 -0
  62. package/dist/confirm/summary.js +6 -0
  63. package/dist/confirm/synthesis.js +28 -0
  64. package/dist/discover/detectors.js +50 -0
  65. package/dist/discover/discover.js +270 -0
  66. package/dist/discover/extract.js +282 -0
  67. package/dist/discover/git.js +40 -0
  68. package/dist/discover/kit-files.js +47 -0
  69. package/dist/discover/walk.js +110 -0
  70. package/dist/env.js +57 -0
  71. package/dist/gate/gate.js +173 -0
  72. package/dist/hooks/install.js +153 -0
  73. package/dist/hooks/run.js +112 -0
  74. package/dist/map/analyze.js +201 -0
  75. package/dist/map/build.js +253 -0
  76. package/dist/map/candidates.js +49 -0
  77. package/dist/map/cblocks.js +77 -0
  78. package/dist/map/config.js +291 -0
  79. package/dist/map/dotnet.js +242 -0
  80. package/dist/map/entry.js +78 -0
  81. package/dist/map/expo.js +190 -0
  82. package/dist/map/go.js +292 -0
  83. package/dist/map/js-express.js +143 -0
  84. package/dist/map/js-misc.js +295 -0
  85. package/dist/map/js-next.js +292 -0
  86. package/dist/map/laravel.js +389 -0
  87. package/dist/map/lex-more.js +305 -0
  88. package/dist/map/lex.js +425 -0
  89. package/dist/map/light-index.js +191 -0
  90. package/dist/map/light-profiles.js +324 -0
  91. package/dist/map/light.js +574 -0
  92. package/dist/map/local-vocab.js +27 -0
  93. package/dist/map/mobile.js +326 -0
  94. package/dist/map/py-django.js +281 -0
  95. package/dist/map/py.js +391 -0
  96. package/dist/map/rails.js +391 -0
  97. package/dist/map/repo.js +178 -0
  98. package/dist/map/rust.js +312 -0
  99. package/dist/map/spring.js +207 -0
  100. package/dist/map/taint.js +96 -0
  101. package/dist/map/touches.js +355 -0
  102. package/dist/map/vocab.js +88 -0
  103. package/dist/mapping/controls.js +34 -0
  104. package/dist/mapping/dependency-policy.js +44 -0
  105. package/dist/mapping/map.js +44 -0
  106. package/dist/mapping/maps/gitleaks.json +12 -0
  107. package/dist/mapping/maps/osv-scanner.json +7 -0
  108. package/dist/mapping/maps/trivy.json +13 -0
  109. package/dist/mcp/confine.js +59 -0
  110. package/dist/mcp/protocol.js +140 -0
  111. package/dist/mcp/schema.js +43 -0
  112. package/dist/mcp/server.js +37 -0
  113. package/dist/mcp/tools.js +238 -0
  114. package/dist/packs/coverage.js +79 -0
  115. package/dist/packs/manifest.js +36 -0
  116. package/dist/packs/validate.js +271 -0
  117. package/dist/prove/container.js +276 -0
  118. package/dist/prove/exec-main.js +20 -0
  119. package/dist/prove/executor.js +149 -0
  120. package/dist/prove/local.js +131 -0
  121. package/dist/prove/probes.js +65 -0
  122. package/dist/prove/recipe.js +278 -0
  123. package/dist/prove/run.js +136 -0
  124. package/dist/prove/runner.js +1 -0
  125. package/dist/prove/safety.js +126 -0
  126. package/dist/prove/spec.js +357 -0
  127. package/dist/prove/store.js +65 -0
  128. package/dist/report/build.js +297 -0
  129. package/dist/report/entry-points.js +75 -0
  130. package/dist/report/hunt-section.js +71 -0
  131. package/dist/report/kit.js +41 -0
  132. package/dist/report/load.js +35 -0
  133. package/dist/report/messages.js +273 -0
  134. package/dist/report/prove-section.js +65 -0
  135. package/dist/report/report.js +156 -0
  136. package/dist/report/surface.js +21 -0
  137. package/dist/report/test-code.js +51 -0
  138. package/dist/report/tiers.js +44 -0
  139. package/dist/scanners/common.js +162 -0
  140. package/dist/scanners/gitleaks.js +90 -0
  141. package/dist/scanners/index.js +113 -0
  142. package/dist/scanners/osv-scanner.js +77 -0
  143. package/dist/scanners/run-targets.js +34 -0
  144. package/dist/scanners/secret-noise.js +89 -0
  145. package/dist/scanners/semgrep.js +142 -0
  146. package/dist/scanners/trivy.js +65 -0
  147. package/dist/scanners/types.js +1 -0
  148. package/dist/types/check.js +1 -0
  149. package/dist/types/codemap.js +1 -0
  150. package/dist/types/findings.js +5 -0
  151. package/dist/types/hunt.js +1 -0
  152. package/dist/types/index.js +1 -0
  153. package/dist/types/prove.js +1 -0
  154. package/dist/types/results.js +2 -0
  155. package/dist/types/scan.js +1 -0
  156. package/dist/types/stack.js +5 -0
  157. package/dist/util.js +71 -0
  158. package/dist/version.js +2 -0
  159. package/package.json +46 -4
  160. package/packs/android/fixture-result.json +43 -0
  161. package/packs/android/pack.json +56 -0
  162. package/packs/dotnet/fixture-result.json +62 -0
  163. package/packs/dotnet/pack.json +88 -0
  164. package/packs/expo/answer-key.json +51 -0
  165. package/packs/expo/fixture-result.json +62 -0
  166. package/packs/expo/pack.json +79 -0
  167. package/packs/firebase/answer-key.json +27 -0
  168. package/packs/firebase/fixture-result.json +43 -0
  169. package/packs/firebase/pack.json +58 -0
  170. package/packs/go/answer-key.json +101 -0
  171. package/packs/go/fixture-result.json +64 -0
  172. package/packs/go/pack.json +99 -0
  173. package/packs/ios/fixture-result.json +43 -0
  174. package/packs/ios/pack.json +55 -0
  175. package/packs/laravel/answer-key.json +84 -0
  176. package/packs/laravel/fixture-result.json +60 -0
  177. package/packs/laravel/pack.json +91 -0
  178. package/packs/node-ts/answer-key.json +220 -0
  179. package/packs/node-ts/fixture-result.json +114 -0
  180. package/packs/node-ts/pack.json +134 -0
  181. package/packs/python/answer-key.json +195 -0
  182. package/packs/python/fixture-result.json +119 -0
  183. package/packs/python/pack.json +121 -0
  184. package/packs/rails/answer-key.json +84 -0
  185. package/packs/rails/fixture-result.json +60 -0
  186. package/packs/rails/pack.json +78 -0
  187. package/packs/rust/fixture-result.json +43 -0
  188. package/packs/rust/pack.json +69 -0
  189. package/packs/spring/fixture-result.json +63 -0
  190. package/packs/spring/pack.json +103 -0
  191. package/packs/supabase/answer-key.json +52 -0
  192. package/packs/supabase/fixture-result.json +58 -0
  193. package/packs/supabase/pack.json +60 -0
  194. package/rules/gitleaks.toml +256 -0
  195. package/rules/semgrep/api-route-no-auth.yaml +31 -0
  196. package/rules/semgrep/cors-reflect-origin.yaml +48 -0
  197. package/rules/semgrep/eval-or-new-function.yaml +185 -0
  198. package/rules/semgrep/log-session-token.yaml +48 -0
  199. package/rules/semgrep/math-random-tokens.yaml +24 -0
  200. package/rules/semgrep/next-public-secret-name.yaml +32 -0
  201. package/rules/semgrep/postgrest-filter-injection.yaml +78 -0
  202. package/rules/semgrep/raw-sql-concat.yaml +42 -0
  203. package/rules/semgrep/read-by-id-no-owner.yaml +49 -0
  204. package/rules/semgrep/server-action-no-auth.yaml +26 -0
  205. package/rules/semgrep/sql-variants.yaml +99 -0
  206. package/rules/semgrep/ssrf-unguarded-url.yaml +137 -0
  207. package/rules/semgrep/subprocess-injection.yaml +163 -0
  208. package/rules/semgrep/taint-go.yaml +333 -0
  209. package/rules/semgrep/taint-java.yaml +317 -0
  210. package/rules/semgrep/taint-javascript.yaml +412 -0
  211. package/rules/semgrep/taint-php.yaml +361 -0
  212. package/rules/semgrep/taint-python.yaml +414 -0
  213. package/rules/semgrep/tool-endpoint-admin-client.yaml +86 -0
  214. package/rules/semgrep/user-metadata-authz.yaml +22 -0
  215. package/rules/semgrep/verify-result-unused.yaml +62 -0
  216. package/rules/semgrep/webhook-parse-before-verify.yaml +29 -0
  217. package/rules/test-artifact-ids.json +4 -0
  218. package/rules/tiers.json +427 -0
  219. package/schemas/check-results.schema.json +55 -0
  220. package/schemas/check.schema.json +41 -0
  221. package/schemas/codemap.schema.json +216 -0
  222. package/schemas/findings.schema.json +1048 -0
  223. package/schemas/hunt-output.schema.json +46 -0
  224. package/schemas/hunt-result.schema.json +58 -0
  225. package/schemas/results.schema.json +17 -0
  226. package/schemas/scan-hits.schema.json +32 -0
  227. package/schemas/scan-meta.schema.json +25 -0
  228. package/schemas/stack.schema.json +93 -0
  229. package/src/checks/README.md +32 -0
  230. package/src/checks/code/admin-role.ts +49 -0
  231. package/src/checks/code/owner-filter.ts +95 -0
  232. package/src/checks/code/shared.ts +66 -0
  233. package/src/checks/code/webhook-verify.ts +75 -0
  234. package/src/checks/context.ts +72 -0
  235. package/src/checks/map/config-checks.ts +79 -0
  236. package/src/checks/map/entry-checks.ts +116 -0
  237. package/src/checks/map/shared.ts +63 -0
  238. package/src/checks/ops.ts +280 -0
  239. package/src/checks/pro-map.ts +4 -0
  240. package/src/checks/run.ts +80 -0
  241. package/src/checks/types.ts +12 -0
  242. package/src/ci/comment.ts +52 -0
  243. package/src/ci/sarif-check.ts +100 -0
  244. package/src/ci/sarif.ts +72 -0
  245. package/src/cli.ts +273 -0
  246. package/src/commands/ci.ts +104 -0
  247. package/src/commands/git.ts +59 -0
  248. package/src/commands/hooks.ts +27 -0
  249. package/src/commands/hunt-pro.ts +4 -0
  250. package/src/commands/hunt.ts +68 -0
  251. package/src/commands/map.ts +27 -0
  252. package/src/commands/mcp.ts +18 -0
  253. package/src/commands/pack.ts +34 -0
  254. package/src/commands/prove-pro.ts +4 -0
  255. package/src/commands/prove.ts +126 -0
  256. package/src/commands/report-gate.ts +75 -0
  257. package/src/commands/scan.ts +156 -0
  258. package/src/confirm/agreement.ts +23 -0
  259. package/src/confirm/chain.ts +67 -0
  260. package/src/confirm/confirm.ts +107 -0
  261. package/src/confirm/honesty.ts +65 -0
  262. package/src/confirm/lead.ts +76 -0
  263. package/src/confirm/merge.ts +89 -0
  264. package/src/confirm/query.ts +126 -0
  265. package/src/confirm/rule.ts +76 -0
  266. package/src/confirm/store.ts +42 -0
  267. package/src/confirm/summary.ts +9 -0
  268. package/src/confirm/synthesis.ts +33 -0
  269. package/src/discover/detectors.ts +71 -0
  270. package/src/discover/discover.ts +253 -0
  271. package/src/discover/extract.ts +235 -0
  272. package/src/discover/git.ts +31 -0
  273. package/src/discover/kit-files.ts +37 -0
  274. package/src/discover/walk.ts +88 -0
  275. package/src/env.ts +51 -0
  276. package/src/gate/gate.ts +169 -0
  277. package/src/hooks/install.ts +142 -0
  278. package/src/hooks/run.ts +121 -0
  279. package/src/map/analyze.ts +219 -0
  280. package/src/map/build.ts +206 -0
  281. package/src/map/candidates.ts +47 -0
  282. package/src/map/cblocks.ts +55 -0
  283. package/src/map/config.ts +207 -0
  284. package/src/map/dotnet.ts +205 -0
  285. package/src/map/entry.ts +80 -0
  286. package/src/map/expo.ts +171 -0
  287. package/src/map/go.ts +271 -0
  288. package/src/map/js-express.ts +135 -0
  289. package/src/map/js-misc.ts +221 -0
  290. package/src/map/js-next.ts +282 -0
  291. package/src/map/laravel.ts +327 -0
  292. package/src/map/lex-more.ts +152 -0
  293. package/src/map/lex.ts +274 -0
  294. package/src/map/light-index.ts +134 -0
  295. package/src/map/light-profiles.ts +363 -0
  296. package/src/map/light.ts +484 -0
  297. package/src/map/local-vocab.ts +30 -0
  298. package/src/map/mobile.ts +263 -0
  299. package/src/map/py-django.ts +239 -0
  300. package/src/map/py.ts +329 -0
  301. package/src/map/rails.ts +291 -0
  302. package/src/map/repo.ts +150 -0
  303. package/src/map/rust.ts +256 -0
  304. package/src/map/spring.ts +185 -0
  305. package/src/map/taint.ts +86 -0
  306. package/src/map/touches.ts +296 -0
  307. package/src/map/vocab.ts +106 -0
  308. package/src/mapping/controls.ts +47 -0
  309. package/src/mapping/dependency-policy.ts +55 -0
  310. package/src/mapping/map.ts +59 -0
  311. package/src/mapping/maps/gitleaks.json +12 -0
  312. package/src/mapping/maps/osv-scanner.json +7 -0
  313. package/src/mapping/maps/trivy.json +13 -0
  314. package/src/mcp/confine.ts +44 -0
  315. package/src/mcp/protocol.ts +144 -0
  316. package/src/mcp/schema.ts +35 -0
  317. package/src/mcp/server.ts +32 -0
  318. package/src/mcp/tools.ts +235 -0
  319. package/src/packs/coverage.ts +98 -0
  320. package/src/packs/manifest.ts +97 -0
  321. package/src/packs/validate.ts +204 -0
  322. package/src/prove/container.ts +237 -0
  323. package/src/prove/exec-main.ts +16 -0
  324. package/src/prove/executor.ts +130 -0
  325. package/src/prove/local.ts +100 -0
  326. package/src/prove/probes.ts +68 -0
  327. package/src/prove/recipe.ts +215 -0
  328. package/src/prove/run.ts +124 -0
  329. package/src/prove/runner.ts +28 -0
  330. package/src/prove/safety.ts +98 -0
  331. package/src/prove/spec.ts +256 -0
  332. package/src/prove/store.ts +48 -0
  333. package/src/report/build.ts +304 -0
  334. package/src/report/entry-points.ts +78 -0
  335. package/src/report/hunt-section.ts +67 -0
  336. package/src/report/kit.ts +59 -0
  337. package/src/report/load.ts +49 -0
  338. package/src/report/messages.ts +270 -0
  339. package/src/report/prove-section.ts +61 -0
  340. package/src/report/report.ts +160 -0
  341. package/src/report/surface.ts +32 -0
  342. package/src/report/test-code.ts +56 -0
  343. package/src/report/tiers.ts +50 -0
  344. package/src/scanners/common.ts +149 -0
  345. package/src/scanners/gitleaks.ts +89 -0
  346. package/src/scanners/index.ts +103 -0
  347. package/src/scanners/osv-scanner.ts +90 -0
  348. package/src/scanners/run-targets.ts +35 -0
  349. package/src/scanners/secret-noise.ts +80 -0
  350. package/src/scanners/semgrep.ts +139 -0
  351. package/src/scanners/trivy.ts +66 -0
  352. package/src/scanners/types.ts +98 -0
  353. package/src/types/check.ts +52 -0
  354. package/src/types/codemap.ts +142 -0
  355. package/src/types/findings.ts +107 -0
  356. package/src/types/hunt.ts +97 -0
  357. package/src/types/index.ts +10 -0
  358. package/src/types/prove.ts +83 -0
  359. package/src/types/results.ts +15 -0
  360. package/src/types/scan.ts +63 -0
  361. package/src/types/stack.ts +61 -0
  362. package/src/util.ts +59 -0
  363. package/src/version.ts +2 -0
@@ -0,0 +1,425 @@
1
+ // Text helpers for the extractors (approach (b) in docs/PHASE-2-DESIGN.md): blank out comments and string contents while
2
+ // keeping every position, match brackets, and cut function bodies. No parser, no dependency.
3
+ import { maskMore } from './lex-more.js';
4
+ const blank = (s) => s.replace(/[^\n]/g, ' ');
5
+ // Whether a "/" at `i` starts a regular expression literal (and is not a division or a JSX closing tag), judged from the
6
+ // text before it.
7
+ function regexAllowed(text, i) {
8
+ let k = i - 1;
9
+ while (k >= 0 && /\s/.test(text[k]))
10
+ k--;
11
+ if (k < 0)
12
+ return true;
13
+ if ('(,=:[!&|?{;+-*%~^'.includes(text[k]))
14
+ return true;
15
+ const m = /([A-Za-z_$][\w$]*)$/.exec(text.slice(Math.max(0, k - 12), k + 1));
16
+ return !!m && /^(return|typeof|case|in|of|delete|void|throw|new|else|do|yield|await)$/.test(m[1]);
17
+ }
18
+ // A copy of `text` with the same length in which comments and the insides of string literals are spaces. Quote characters
19
+ // and newlines stay, so a regular expression run on the copy never matches inside a comment or a string, and a position in
20
+ // the copy is the same position in the original.
21
+ export function mask(text, lang) {
22
+ if (lang !== 'js' && lang !== 'py')
23
+ return maskMore(text, lang);
24
+ const n = text.length;
25
+ const out = [];
26
+ let i = 0;
27
+ let prevSig = '';
28
+ const put = (from, to) => { out.push(blank(text.slice(from, to))); };
29
+ const keep = (from, to) => { out.push(text.slice(from, to)); };
30
+ const skipTemplate = (start) => {
31
+ let j = start + 1;
32
+ while (j < n) {
33
+ const c = text[j];
34
+ if (c === '\\') {
35
+ j += 2;
36
+ continue;
37
+ }
38
+ if (c === '`')
39
+ return j + 1;
40
+ if (c === '$' && text[j + 1] === '{') {
41
+ let depth = 1;
42
+ j += 2;
43
+ while (j < n && depth > 0) {
44
+ const d = text[j];
45
+ if (d === '`') {
46
+ j = skipTemplate(j);
47
+ continue;
48
+ }
49
+ if (d === '"' || d === "'") {
50
+ j++;
51
+ while (j < n && text[j] !== d && text[j] !== '\n') {
52
+ if (text[j] === '\\')
53
+ j++;
54
+ j++;
55
+ }
56
+ j++;
57
+ continue;
58
+ }
59
+ if (d === '{')
60
+ depth++;
61
+ else if (d === '}')
62
+ depth--;
63
+ j++;
64
+ }
65
+ continue;
66
+ }
67
+ j++;
68
+ }
69
+ return n;
70
+ };
71
+ while (i < n) {
72
+ const c = text[i];
73
+ const c2 = text[i + 1];
74
+ if (lang === 'js') {
75
+ if (c === '/' && c2 === '/') {
76
+ const e = text.indexOf('\n', i);
77
+ const end = e < 0 ? n : e;
78
+ put(i, end);
79
+ i = end;
80
+ continue;
81
+ }
82
+ if (c === '/' && c2 === '*') {
83
+ const e = text.indexOf('*/', i + 2);
84
+ const end = e < 0 ? n : e + 2;
85
+ put(i, end);
86
+ i = end;
87
+ continue;
88
+ }
89
+ if (c === '`') {
90
+ const end = skipTemplate(i);
91
+ keep(i, i + 1);
92
+ put(i + 1, Math.max(i + 1, end - 1));
93
+ if (end - 1 > i)
94
+ keep(end - 1, end);
95
+ i = end;
96
+ prevSig = '`';
97
+ continue;
98
+ }
99
+ if (c === '"' || c === "'") {
100
+ let j = i + 1;
101
+ while (j < n && text[j] !== c && text[j] !== '\n') {
102
+ if (text[j] === '\\')
103
+ j++;
104
+ j++;
105
+ }
106
+ const close = j < n && text[j] === c ? j : -1;
107
+ keep(i, i + 1);
108
+ put(i + 1, close < 0 ? Math.min(j, n) : close);
109
+ if (close >= 0)
110
+ keep(close, close + 1);
111
+ i = close < 0 ? Math.min(j, n) : close + 1;
112
+ prevSig = c;
113
+ continue;
114
+ }
115
+ if (c === '/' && regexAllowed(text, i)) {
116
+ // regular expression literal
117
+ let j = i + 1;
118
+ let inClass = false;
119
+ while (j < n && text[j] !== '\n') {
120
+ if (text[j] === '\\') {
121
+ j += 2;
122
+ continue;
123
+ }
124
+ if (text[j] === '[')
125
+ inClass = true;
126
+ else if (text[j] === ']')
127
+ inClass = false;
128
+ else if (text[j] === '/' && !inClass)
129
+ break;
130
+ j++;
131
+ }
132
+ if (j < n && text[j] === '/') {
133
+ keep(i, i + 1);
134
+ put(i + 1, j);
135
+ keep(j, j + 1);
136
+ i = j + 1;
137
+ prevSig = '/';
138
+ continue;
139
+ }
140
+ }
141
+ }
142
+ else {
143
+ if (c === '#') {
144
+ const e = text.indexOf('\n', i);
145
+ const end = e < 0 ? n : e;
146
+ put(i, end);
147
+ i = end;
148
+ continue;
149
+ }
150
+ if (c === '"' || c === "'") {
151
+ const triple = text[i + 1] === c && text[i + 2] === c;
152
+ if (triple) {
153
+ const e = text.indexOf(c.repeat(3), i + 3);
154
+ const end = e < 0 ? n : e + 3;
155
+ keep(i, i + 3);
156
+ put(i + 3, Math.max(i + 3, end - 3));
157
+ if (e >= 0)
158
+ keep(end - 3, end);
159
+ i = end;
160
+ prevSig = c;
161
+ continue;
162
+ }
163
+ let j = i + 1;
164
+ while (j < n && text[j] !== c && text[j] !== '\n') {
165
+ if (text[j] === '\\')
166
+ j++;
167
+ j++;
168
+ }
169
+ const close = j < n && text[j] === c ? j : -1;
170
+ keep(i, i + 1);
171
+ put(i + 1, close < 0 ? Math.min(j, n) : close);
172
+ if (close >= 0)
173
+ keep(close, close + 1);
174
+ i = close < 0 ? Math.min(j, n) : close + 1;
175
+ prevSig = c;
176
+ continue;
177
+ }
178
+ }
179
+ keep(i, i + 1);
180
+ if (!/\s/.test(c))
181
+ prevSig = c;
182
+ i++;
183
+ }
184
+ return out.join('');
185
+ }
186
+ // Line numbers from character positions, by binary search over the line starts. Built once per file.
187
+ export class Lines {
188
+ starts = [0];
189
+ constructor(text) { for (let i = 0; i < text.length; i++)
190
+ if (text.charCodeAt(i) === 10)
191
+ this.starts.push(i + 1); }
192
+ at(index) {
193
+ let lo = 0;
194
+ let hi = this.starts.length - 1;
195
+ while (lo < hi) {
196
+ const mid = (lo + hi + 1) >> 1;
197
+ if (this.starts[mid] <= index)
198
+ lo = mid;
199
+ else
200
+ hi = mid - 1;
201
+ }
202
+ return lo + 1;
203
+ }
204
+ start(line) { return this.starts[Math.max(0, Math.min(this.starts.length - 1, line - 1))]; }
205
+ get count() { return this.starts.length; }
206
+ }
207
+ const PAIR = { '(': ')', '{': '}', '[': ']' };
208
+ // The index just past the bracket that closes the one at `open`, in masked text, or -1.
209
+ export function closeOf(masked, open) {
210
+ const stack = [];
211
+ for (let i = open; i < masked.length; i++) {
212
+ const c = masked[i];
213
+ if (PAIR[c])
214
+ stack.push(PAIR[c]);
215
+ else if (c === ')' || c === '}' || c === ']') {
216
+ if (c !== stack[stack.length - 1])
217
+ return -1;
218
+ stack.pop();
219
+ if (stack.length === 0)
220
+ return i + 1;
221
+ }
222
+ }
223
+ return -1;
224
+ }
225
+ // Top level argument ranges of the call whose "(" is at `open`: [{start, end}] with end exclusive, trimmed.
226
+ export function splitArgs(masked, open) {
227
+ const close = closeOf(masked, open);
228
+ if (close < 0)
229
+ return [];
230
+ const args = [];
231
+ let depth = 0;
232
+ let from = open + 1;
233
+ const push = (end) => {
234
+ let s = from;
235
+ let e = end;
236
+ while (s < e && /\s/.test(masked[s]))
237
+ s++;
238
+ while (e > s && /\s/.test(masked[e - 1]))
239
+ e--;
240
+ if (e > s)
241
+ args.push({ start: s, end: e });
242
+ };
243
+ for (let i = open + 1; i < close - 1; i++) {
244
+ const c = masked[i];
245
+ if (c === '(' || c === '[' || c === '{')
246
+ depth++;
247
+ else if (c === ')' || c === ']' || c === '}')
248
+ depth--;
249
+ else if (c === ',' && depth === 0) {
250
+ push(i);
251
+ from = i + 1;
252
+ }
253
+ else if (c === '=' && masked[i + 1] === '>') { /* arrow: the body follows; handled by depth of braces */ }
254
+ }
255
+ push(close - 1);
256
+ return args;
257
+ }
258
+ // The end (exclusive) of the statement that starts at `from`: the first ";" at depth 0, or a newline at depth 0 after which
259
+ // the next code character does not continue the expression (a ".", "?", ":", operator or closing bracket).
260
+ export function statementEnd(masked, from) {
261
+ let depth = 0;
262
+ for (let i = from; i < masked.length; i++) {
263
+ const c = masked[i];
264
+ if (c === '(' || c === '[' || c === '{')
265
+ depth++;
266
+ else if (c === ')' || c === ']' || c === '}') {
267
+ if (depth === 0)
268
+ return i;
269
+ depth--;
270
+ }
271
+ else if (c === ';' && depth === 0)
272
+ return i;
273
+ else if (c === '\n' && depth === 0) {
274
+ const rest = masked.slice(i + 1, i + 200);
275
+ const next = /\S/.exec(rest);
276
+ const prev = masked.slice(from, i).trimEnd().slice(-1);
277
+ if (!next)
278
+ return i;
279
+ const nc = rest[next.index];
280
+ if (nc === '.' || nc === '?' || nc === ':' || nc === '+' || nc === '|' || nc === '&' || nc === ',' || nc === ')' || nc === ']')
281
+ continue;
282
+ if (prev === ',' || prev === '+' || prev === '=' || prev === '?' || prev === ':' || prev === '|' || prev === '&' || prev === '(')
283
+ continue;
284
+ return i;
285
+ }
286
+ }
287
+ return masked.length;
288
+ }
289
+ // The extent of the JavaScript or TypeScript function that begins at `from` (the index of "async", "function", "(" or a
290
+ // single parameter name before "=>"). bodyStart is the first index of the body text (the "{" or the first token of an
291
+ // expression body). Returns null when the text there is not a function.
292
+ // `method`: the text is a class method, so a "{" after the return type at bracket depth 0 is the body and never an object type.
293
+ export function jsFunction(masked, from, method = false) {
294
+ let i = from;
295
+ const ws = () => { while (i < masked.length && /\s/.test(masked[i]))
296
+ i++; };
297
+ ws();
298
+ if (masked.startsWith('async', i) && /\W/.test(masked[i + 5] ?? ' ')) {
299
+ i += 5;
300
+ ws();
301
+ }
302
+ let isFunctionKeyword = false;
303
+ if (masked.startsWith('function', i) && /\W/.test(masked[i + 8] ?? ' ')) {
304
+ isFunctionKeyword = true;
305
+ i += 8;
306
+ ws();
307
+ if (masked[i] === '*') {
308
+ i++;
309
+ ws();
310
+ }
311
+ while (/[\w$]/.test(masked[i] ?? ''))
312
+ i++;
313
+ ws();
314
+ if (masked[i] === '<') {
315
+ let d = 0;
316
+ for (; i < masked.length; i++) {
317
+ if (masked[i] === '<')
318
+ d++;
319
+ else if (masked[i] === '>') {
320
+ d--;
321
+ if (d === 0) {
322
+ i++;
323
+ break;
324
+ }
325
+ }
326
+ }
327
+ ws();
328
+ }
329
+ }
330
+ if (masked[i] === '(') {
331
+ const pc = closeOf(masked, i);
332
+ if (pc < 0)
333
+ return null;
334
+ i = pc;
335
+ }
336
+ else if (!isFunctionKeyword && /[\w$]/.test(masked[i] ?? '')) {
337
+ while (/[\w$]/.test(masked[i] ?? ''))
338
+ i++;
339
+ ws();
340
+ if (!(masked[i] === '=' && masked[i + 1] === '>'))
341
+ return null;
342
+ }
343
+ else
344
+ return null;
345
+ ws();
346
+ // Optional return type annotation: scan to the "{" (function) or "=>" (arrow) at bracket depth 0.
347
+ if (masked[i] === ':') {
348
+ let depth = 0;
349
+ for (i = i + 1; i < masked.length; i++) {
350
+ const c = masked[i];
351
+ if (c === '(' || c === '[')
352
+ depth++;
353
+ else if (c === ')' || c === ']')
354
+ depth--;
355
+ else if (c === '<')
356
+ depth++;
357
+ else if (c === '>' && masked[i - 1] !== '=')
358
+ depth--;
359
+ else if (c === '{' && depth <= 0 && !isFunctionKeyword && !method) {
360
+ depth++;
361
+ }
362
+ else if (c === '}' && depth > 0 && !isFunctionKeyword && !method) {
363
+ depth--;
364
+ }
365
+ else if (c === '=' && masked[i + 1] === '>' && depth <= 0)
366
+ break;
367
+ else if (c === '{' && depth <= 0 && (isFunctionKeyword || method))
368
+ break;
369
+ }
370
+ }
371
+ ws();
372
+ if (masked[i] === '=' && masked[i + 1] === '>') {
373
+ i += 2;
374
+ ws();
375
+ if (masked[i] === '{') {
376
+ const e = closeOf(masked, i);
377
+ return e < 0 ? null : { start: from, end: e, bodyStart: i };
378
+ }
379
+ const e = statementEnd(masked, i);
380
+ return { start: from, end: e, bodyStart: i };
381
+ }
382
+ if (masked[i] === '{') {
383
+ const e = closeOf(masked, i);
384
+ return e < 0 ? null : { start: from, end: e, bodyStart: i };
385
+ }
386
+ return null;
387
+ }
388
+ // The Python block that belongs to the "def" or "class" line starting at `lineStart` (an index at the start of the line):
389
+ // the header (which may span several lines until the colon that ends it) and every following line indented deeper.
390
+ export function pyBlock(masked, lines, lineStart) {
391
+ const indentOf = (ls) => { let k = ls; let w = 0; while (masked[k] === ' ' || masked[k] === '\t') {
392
+ w += masked[k] === '\t' ? 8 : 1;
393
+ k++;
394
+ } return w; };
395
+ const base = indentOf(lineStart);
396
+ // header ends at the first ":" at bracket depth 0 followed by end of line
397
+ let depth = 0;
398
+ let i = lineStart;
399
+ for (; i < masked.length; i++) {
400
+ const c = masked[i];
401
+ if (c === '(' || c === '[' || c === '{')
402
+ depth++;
403
+ else if (c === ')' || c === ']' || c === '}')
404
+ depth--;
405
+ else if (c === ':' && depth === 0 && /^[ \t]*(\n|$)/.test(masked.slice(i + 1, i + 40)))
406
+ break;
407
+ else if (c === ':' && depth === 0 && /^[ \t]*\S/.test(masked.slice(i + 1, i + 40)) && /^\s*(def|class|async\s+def)\b/.test(masked.slice(lineStart, lineStart + 12))) { /* annotation colon, keep scanning */ }
408
+ }
409
+ const bodyStart = Math.min(masked.length, i + 1);
410
+ let end = bodyStart;
411
+ const total = lines.count;
412
+ let ln = lines.at(bodyStart) + 1;
413
+ for (; ln <= total; ln++) {
414
+ const ls = lines.start(ln);
415
+ const le = masked.indexOf('\n', ls);
416
+ const row = masked.slice(ls, le < 0 ? masked.length : le);
417
+ if (row.trim() === '') {
418
+ continue;
419
+ }
420
+ if (indentOf(ls) <= base)
421
+ break;
422
+ end = le < 0 ? masked.length : le;
423
+ }
424
+ return { start: lineStart, end, bodyStart };
425
+ }
@@ -0,0 +1,191 @@
1
+ // An index of the repository's own functions by name for Go, Ruby and PHP: where a function starts and ends, so a handler
2
+ // can be followed to its definition and one hop into the helpers it calls. Built once per language and repository.
3
+ import { cmp } from '../util.js';
4
+ import { closeOf, statementEnd } from './lex.js';
5
+ // The extent of a Go function whose "func" keyword is at `at`: [start, end) with the body inside braces.
6
+ export function goFunc(masked, at) {
7
+ let i = at + 4;
8
+ while (/\s/.test(masked[i] ?? ''))
9
+ i++;
10
+ if (masked[i] === '(' && /^\(\s*\w*\s*\*?\s*[\w.[\]]+\s*\)\s*\w/.test(masked.slice(i, i + 120))) {
11
+ const c = closeOf(masked, i);
12
+ if (c < 0)
13
+ return null;
14
+ i = c;
15
+ while (/\s/.test(masked[i] ?? ''))
16
+ i++;
17
+ }
18
+ while (/[\w]/.test(masked[i] ?? ''))
19
+ i++;
20
+ while (/\s/.test(masked[i] ?? ''))
21
+ i++;
22
+ if (masked[i] === '[') {
23
+ const c = closeOf(masked, i);
24
+ if (c < 0)
25
+ return null;
26
+ i = c;
27
+ }
28
+ if (masked[i] !== '(')
29
+ return null;
30
+ const pc = closeOf(masked, i);
31
+ if (pc < 0)
32
+ return null;
33
+ const params = masked.slice(i + 1, pc - 1);
34
+ for (i = pc; i < masked.length; i++) {
35
+ const c = masked[i];
36
+ if (c === '{') {
37
+ const before = masked.slice(Math.max(0, i - 12), i).trimEnd();
38
+ if (/(?:interface|struct)$/.test(before)) {
39
+ const e = closeOf(masked, i);
40
+ if (e < 0)
41
+ return null;
42
+ i = e - 1;
43
+ continue;
44
+ }
45
+ const e = closeOf(masked, i);
46
+ return e < 0 ? null : { start: at, end: e, bodyStart: i, params };
47
+ }
48
+ if (c === ';')
49
+ return null;
50
+ }
51
+ return null;
52
+ }
53
+ // A Ruby method: from "def" to the "end" at the same indentation.
54
+ export function rubyDef(masked, defAt) {
55
+ const ls = masked.lastIndexOf('\n', defAt) + 1;
56
+ const indent = /^[ \t]*/.exec(masked.slice(ls))[0].length;
57
+ const oneLine = /^[^\n]*\bdef\b[^\n]*=\s*\S/.exec(masked.slice(defAt, defAt + 200));
58
+ const firstEnd = masked.indexOf('\n', defAt);
59
+ if (oneLine && !/\bend\b/.test(masked.slice(defAt, firstEnd < 0 ? masked.length : firstEnd)))
60
+ return { start: defAt, end: firstEnd < 0 ? masked.length : firstEnd };
61
+ let pos = firstEnd < 0 ? masked.length : firstEnd + 1;
62
+ while (pos < masked.length) {
63
+ const e = masked.indexOf('\n', pos);
64
+ const row = masked.slice(pos, e < 0 ? masked.length : e);
65
+ const ind = /^[ \t]*/.exec(row)[0].length;
66
+ if (row.trim() !== '' && ind <= indent && /^\s*end\b/.test(row))
67
+ return { start: defAt, end: pos + row.length };
68
+ if (row.trim() !== '' && ind < indent)
69
+ return { start: defAt, end: pos };
70
+ if (e < 0)
71
+ break;
72
+ pos = e + 1;
73
+ }
74
+ return { start: defAt, end: masked.length };
75
+ }
76
+ // A PHP function or method: from "function" to the closing brace of its body.
77
+ export function phpFunc(masked, at) {
78
+ const open = masked.indexOf('(', at);
79
+ if (open < 0)
80
+ return null;
81
+ const pc = closeOf(masked, open);
82
+ if (pc < 0)
83
+ return null;
84
+ const params = masked.slice(open + 1, pc - 1);
85
+ const m = /^[^{;]*\{/.exec(masked.slice(pc, pc + 300));
86
+ if (!m)
87
+ return null;
88
+ const bodyStart = pc + m[0].length - 1;
89
+ const e = closeOf(masked, bodyStart);
90
+ return e < 0 ? null : { start: at, end: e, bodyStart, params };
91
+ }
92
+ // A Java or C# method whose name starts at `at`: the parameters, then a block body, or in C# an expression body after "=>". An abstract or
93
+ // interface method (ending in ";") has no body and gives null.
94
+ export function cMethod(masked, at) {
95
+ let i = at;
96
+ while (/[\w$]/.test(masked[i] ?? ''))
97
+ i++;
98
+ while (/\s/.test(masked[i] ?? ''))
99
+ i++;
100
+ if (masked[i] === '<') {
101
+ let d = 0;
102
+ for (; i < masked.length; i++) {
103
+ if (masked[i] === '<')
104
+ d++;
105
+ else if (masked[i] === '>' && --d === 0) {
106
+ i++;
107
+ break;
108
+ }
109
+ else if (masked[i] === '\n' && d > 4)
110
+ return null;
111
+ }
112
+ while (/\s/.test(masked[i] ?? ''))
113
+ i++;
114
+ }
115
+ if (masked[i] !== '(')
116
+ return null;
117
+ const pc = closeOf(masked, i);
118
+ if (pc < 0)
119
+ return null;
120
+ const params = masked.slice(i + 1, pc - 1);
121
+ for (let j = pc; j < masked.length; j++) {
122
+ const c = masked[j];
123
+ if (c === '{') {
124
+ const e = closeOf(masked, j);
125
+ return e < 0 ? null : { start: at, end: e, bodyStart: j, params };
126
+ }
127
+ if (c === ';')
128
+ return null;
129
+ if (c === '=' && masked[j + 1] === '>')
130
+ return { start: at, end: statementEnd(masked, j + 2), bodyStart: j + 2, params };
131
+ }
132
+ return null;
133
+ }
134
+ // Method declarations: at least one modifier, a type, a name and "(".
135
+ export const C_METHOD = /^[ \t]*(?:@\w+(?:\([^)\n]*\))?[ \t]*)*(?:(?:public|protected|private|internal|static|final|abstract|synchronized|async|override|virtual|sealed|partial|new|extern|unsafe|default)\s+)+[^;{}()=\n]*?\s(\w+)\s*(?:<[^>\n]*>)?\s*\(/gm;
136
+ export class FnIndex {
137
+ idx = new Map();
138
+ repo;
139
+ lang;
140
+ constructor(repo, lang) {
141
+ this.repo = repo;
142
+ this.lang = lang;
143
+ const put = (name, ref) => { const l = this.idx.get(name); if (l)
144
+ l.push(ref);
145
+ else
146
+ this.idx.set(name, [ref]); };
147
+ for (const file of repo.filesOf(lang)) {
148
+ const v = repo.view(file);
149
+ if (!v)
150
+ continue;
151
+ const m = v.masked;
152
+ if (lang === 'go') {
153
+ for (const x of m.matchAll(/^func\s+(?:\(\s*\w*\s*\*?\s*[\w.[\]]+\s*\)\s*)?(\w+)\s*[([]/gm)) {
154
+ const f = goFunc(m, x.index);
155
+ if (f)
156
+ put(x[1], { view: v, start: f.start, end: f.end });
157
+ }
158
+ }
159
+ else if (lang === 'rb') {
160
+ for (const x of m.matchAll(/^[ \t]*def\s+(?:self\.)?(\w+[?!=]?)/gm)) {
161
+ const d = rubyDef(m, x.index + x[0].indexOf('def'));
162
+ put(x[1], { view: v, start: d.start, end: d.end });
163
+ }
164
+ }
165
+ else if (lang === 'java' || lang === 'cs') {
166
+ for (const x of m.matchAll(C_METHOD)) {
167
+ const f = cMethod(m, x.index + x[0].lastIndexOf(x[1]));
168
+ if (f)
169
+ put(x[1], { view: v, start: f.start, end: f.end });
170
+ }
171
+ }
172
+ else {
173
+ for (const x of m.matchAll(/\bfunction\s+&?\s*(\w+)\s*\(/g)) {
174
+ const f = phpFunc(m, x.index);
175
+ if (f)
176
+ put(x[1], { view: v, start: f.start, end: f.end });
177
+ }
178
+ }
179
+ }
180
+ }
181
+ defs(name) { return (this.idx.get(name) ?? []).slice().sort((a, b) => cmp(a.view.file, b.view.file) || a.start - b.start); }
182
+ find = (name, fromFile) => {
183
+ const refs = this.idx.get(name);
184
+ if (!refs)
185
+ return null;
186
+ if (refs.length === 1)
187
+ return refs[0];
188
+ const same = refs.filter((r) => r.view.file === fromFile);
189
+ return same.length === 1 ? same[0] : null;
190
+ };
191
+ }