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,152 @@
1
+ // Masking for the languages added after JavaScript and Python: Go, Ruby and PHP (second wave), Java and C# (which have light profiles),
2
+ // and Kotlin, Swift and Rust (masking only). Same contract as mask() in lex.ts: a copy of the text with the same length in which
3
+ // comments and the insides of string literals are spaces, quote characters and newlines kept, so a position in the copy is the same
4
+ // position in the original.
5
+ export type MoreLang = 'go' | 'rb' | 'php' | 'java' | 'cs'; // languages with a body profile (light-profiles.ts)
6
+ export type MaskLang = MoreLang | 'kt' | 'swift' | 'rs';
7
+
8
+ // Java, C#, Kotlin, Swift and Rust: line and block comments, "strings" with escapes, text blocks and raw strings, character literals.
9
+ function maskC(text: string, lang: 'java' | 'cs' | 'kt' | 'swift' | 'rs'): string {
10
+ const n = text.length;
11
+ const out: string[] = [];
12
+ let i = 0;
13
+ const put = (from: number, to: number): void => { out.push(blank(text.slice(from, to))); };
14
+ const keep = (from: number, to: number): void => { out.push(text.slice(from, to)); };
15
+ const closed = (open: number, close: string, escapes: boolean, doubled = false): number => {
16
+ // index of the closing delimiter at or after open, or -1
17
+ let j = open;
18
+ while (j < n) {
19
+ if (escapes && text[j] === '\\') { j += 2; continue; }
20
+ if (text.startsWith(close, j)) { if (doubled && text.startsWith(close + close, j)) { j += close.length * 2; continue; } return j; }
21
+ j++;
22
+ }
23
+ return -1;
24
+ };
25
+ while (i < n) {
26
+ const c = text[i];
27
+ const c2 = text[i + 1];
28
+ if (c === '/' && c2 === '/') { const e = text.indexOf('\n', i); const end = e < 0 ? n : e; put(i, end); i = end; continue; }
29
+ if (c === '/' && c2 === '*') { const e = text.indexOf('*/', i + 2); const end = e < 0 ? n : e + 2; put(i, end); i = end; continue; }
30
+ // C#: $"...", @"...", $@"..." ; raw strings in every language: """..."""
31
+ let pre = 0;
32
+ if (lang === 'cs') { while (text[i + pre] === '$' || text[i + pre] === '@') pre++; }
33
+ if (lang === 'rs' && (c === 'r' || (c === 'b' && c2 === 'r')) && /^b?r#*"/.test(text.slice(i, i + 12))) {
34
+ const m = /^b?r(#*)"/.exec(text.slice(i, i + 12))!;
35
+ const start = i + m[0].length;
36
+ const e = text.indexOf('"' + m[1], start);
37
+ const end = e < 0 ? n : e;
38
+ keep(i, start); put(start, end); if (e >= 0) keep(e, e + 1 + m[1].length); i = e < 0 ? n : e + 1 + m[1].length; continue;
39
+ }
40
+ const q = i + pre;
41
+ if (text[q] === '"') {
42
+ if (text.startsWith('"""', q)) {
43
+ const e = text.indexOf('"""', q + 3);
44
+ const end = e < 0 ? n : e;
45
+ keep(i, q + 3); put(q + 3, end); if (e >= 0) keep(e, e + 3); i = e < 0 ? n : e + 3; continue;
46
+ }
47
+ const verbatim = lang === 'cs' && text.slice(i, q).includes('@');
48
+ const e = closed(q + 1, '"', !verbatim, verbatim);
49
+ const end = e < 0 ? n : e;
50
+ keep(i, q + 1); put(q + 1, end); if (e >= 0) keep(e, e + 1); i = e < 0 ? n : e + 1; continue;
51
+ }
52
+ if (c === '\'' && lang !== 'rs' || (c === '\'' && lang === 'rs' && /^'(?:\\.[^']*|[^\\'])'/.test(text.slice(i, i + 12)))) {
53
+ const e = closed(i + 1, '\'', true);
54
+ if (e >= 0 && e - i <= 12) { keep(i, i + 1); put(i + 1, e); keep(e, e + 1); i = e + 1; continue; }
55
+ }
56
+ keep(i, i + 1);
57
+ i++;
58
+ }
59
+ return out.join('');
60
+ }
61
+
62
+ const blank = (s: string): string => s.replace(/[^\n]/g, ' ');
63
+
64
+ export function maskMore(text: string, lang: MaskLang): string {
65
+ if (lang === 'java' || lang === 'cs' || lang === 'kt' || lang === 'swift' || lang === 'rs') return maskC(text, lang);
66
+ const n = text.length;
67
+ const out: string[] = [];
68
+ const put = (from: number, to: number): void => { out.push(blank(text.slice(from, to))); };
69
+ const keep = (from: number, to: number): void => { out.push(text.slice(from, to)); };
70
+ let i = 0;
71
+ let prevSig = '';
72
+ let prevWord = '';
73
+ // Heredoc terminators waiting for the end of the current line (Ruby and PHP).
74
+ let pending: { id: string; squiggly: boolean }[] = [];
75
+
76
+ const quoted = (q: string, escapes: boolean): void => {
77
+ let j = i + 1;
78
+ while (j < n && text[j] !== q && (text[j] !== '\n' || lang !== 'go')) { if (escapes && text[j] === '\\') j++; j++; }
79
+ const close = j < n && text[j] === q ? j : -1;
80
+ keep(i, i + 1);
81
+ put(i + 1, close < 0 ? Math.min(j, n) : close);
82
+ if (close >= 0) keep(close, close + 1);
83
+ i = close < 0 ? Math.min(j, n) : close + 1;
84
+ prevSig = q;
85
+ prevWord = '';
86
+ };
87
+
88
+ const flushHeredocs = (): void => {
89
+ // i is just after a newline. Blank lines until each pending terminator line.
90
+ for (const h of pending) {
91
+ for (;;) {
92
+ if (i >= n) break;
93
+ const e = text.indexOf('\n', i);
94
+ const end = e < 0 ? n : e;
95
+ const row = text.slice(i, end);
96
+ const isEnd = (lang === 'rb' ? row.trim() : row.replace(/^\s+/, '').replace(/[;,)\s]+$/, '')) === h.id;
97
+ if (isEnd) { keep(i, end); i = end; break; }
98
+ put(i, end);
99
+ i = end;
100
+ if (i < n) { out.push('\n'); i++; }
101
+ }
102
+ }
103
+ pending = [];
104
+ };
105
+
106
+ while (i < n) {
107
+ const c = text[i];
108
+ const c2 = text[i + 1];
109
+ if (c === '\n') {
110
+ keep(i, i + 1); i++;
111
+ if (pending.length) flushHeredocs();
112
+ // Ruby block comments
113
+ if (lang === 'rb' && text.startsWith('=begin', i)) { const e = text.indexOf('\n=end', i); const end = e < 0 ? n : text.indexOf('\n', e + 1) < 0 ? n : text.indexOf('\n', e + 1); put(i, end); i = end; }
114
+ continue;
115
+ }
116
+ if (lang === 'go') {
117
+ if (c === '/' && c2 === '/') { const e = text.indexOf('\n', i); const end = e < 0 ? n : e; put(i, end); i = end; continue; }
118
+ if (c === '/' && c2 === '*') { const e = text.indexOf('*/', i + 2); const end = e < 0 ? n : e + 2; put(i, end); i = end; continue; }
119
+ if (c === '"') { quoted('"', true); continue; }
120
+ if (c === '\'') { quoted('\'', true); continue; }
121
+ if (c === '`') { const e = text.indexOf('`', i + 1); const end = e < 0 ? n : e; keep(i, i + 1); put(i + 1, end); if (e >= 0) keep(e, e + 1); i = e < 0 ? n : e + 1; prevSig = '`'; continue; }
122
+ } else if (lang === 'rb') {
123
+ if (i === 0 && text.startsWith('=begin')) { const e = text.indexOf('\n=end'); const end = e < 0 ? n : e + 5; put(0, end); i = end; continue; }
124
+ if (c === '#') { const e = text.indexOf('\n', i); const end = e < 0 ? n : e; put(i, end); i = end; continue; }
125
+ const hd = c === '<' && c2 === '<' ? /^<<([~-]?)(['"`]?)([A-Za-z_]\w*)\2/.exec(text.slice(i, i + 60)) : null;
126
+ if (hd && !/[\w)\]]\s*$/.test(text.slice(Math.max(0, i - 2), i).replace(/ $/, '')) || (hd && /[=(,]\s*$/.test(text.slice(Math.max(0, i - 12), i)))) {
127
+ pending.push({ id: hd![3], squiggly: hd![1] === '~' });
128
+ keep(i, i + hd![0].length); i += hd![0].length; continue;
129
+ }
130
+ if (c === '"' || c === '\'' || c === '`') { quoted(c, true); continue; }
131
+ if (c === '/' && /^[(,=~{|&!;]$/.test(prevSig) || (c === '/' && /^(?:when|if|unless|and|or|not|return|split|match|scan|gsub|sub)$/.test(prevWord))) {
132
+ let j = i + 1; let cls = false;
133
+ while (j < n && text[j] !== '\n') { if (text[j] === '\\') { j += 2; continue; } if (text[j] === '[') cls = true; else if (text[j] === ']') cls = false; else if (text[j] === '/' && !cls) break; j++; }
134
+ if (j < n && text[j] === '/') { keep(i, i + 1); put(i + 1, j); keep(j, j + 1); i = j + 1; prevSig = '/'; prevWord = ''; continue; }
135
+ }
136
+ } else {
137
+ if ((c === '/' && c2 === '/') || (c === '#' && c2 !== '[')) { const e = text.indexOf('\n', i); const end = e < 0 ? n : e; put(i, end); i = end; continue; }
138
+ if (c === '/' && c2 === '*') { const e = text.indexOf('*/', i + 2); const end = e < 0 ? n : e + 2; put(i, end); i = end; continue; }
139
+ const hd = c === '<' && c2 === '<' && text[i + 2] === '<' ? /^<<<\s*(['"]?)([A-Za-z_]\w*)\1/.exec(text.slice(i, i + 60)) : null;
140
+ if (hd) { pending.push({ id: hd[2], squiggly: true }); keep(i, i + hd[0].length); i += hd[0].length; continue; }
141
+ if (c === '"' || c === '\'') { quoted(c, true); continue; }
142
+ }
143
+ keep(i, i + 1);
144
+ if (!/\s/.test(c)) {
145
+ prevSig = c;
146
+ if (/[A-Za-z_]/.test(c)) { const m = /^[A-Za-z_]\w*/.exec(text.slice(i, i + 20)); if (m) { prevWord = m[0]; out.pop(); keep(i, i + m[0].length); i += m[0].length; prevSig = 'w'; continue; } }
147
+ else prevWord = '';
148
+ }
149
+ i++;
150
+ }
151
+ return out.join('');
152
+ }
package/src/map/lex.ts ADDED
@@ -0,0 +1,274 @@
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
+
4
+ import { maskMore } from './lex-more.ts';
5
+
6
+ export type Lang = 'js' | 'py' | 'go' | 'rb' | 'php' | 'java' | 'cs' | 'kt' | 'swift' | 'rs';
7
+
8
+ const blank = (s: string): string => s.replace(/[^\n]/g, ' ');
9
+
10
+
11
+ // Whether a "/" at `i` starts a regular expression literal (and is not a division or a JSX closing tag), judged from the
12
+ // text before it.
13
+ function regexAllowed(text: string, i: number): boolean {
14
+ let k = i - 1;
15
+ while (k >= 0 && /\s/.test(text[k])) k--;
16
+ if (k < 0) return true;
17
+ if ('(,=:[!&|?{;+-*%~^'.includes(text[k])) return true;
18
+ const m = /([A-Za-z_$][\w$]*)$/.exec(text.slice(Math.max(0, k - 12), k + 1));
19
+ return !!m && /^(return|typeof|case|in|of|delete|void|throw|new|else|do|yield|await)$/.test(m[1]);
20
+ }
21
+
22
+ // A copy of `text` with the same length in which comments and the insides of string literals are spaces. Quote characters
23
+ // and newlines stay, so a regular expression run on the copy never matches inside a comment or a string, and a position in
24
+ // the copy is the same position in the original.
25
+ export function mask(text: string, lang: Lang): string {
26
+ if (lang !== 'js' && lang !== 'py') return maskMore(text, lang);
27
+ const n = text.length;
28
+ const out: string[] = [];
29
+ let i = 0;
30
+ let prevSig = '';
31
+ const put = (from: number, to: number): void => { out.push(blank(text.slice(from, to))); };
32
+ const keep = (from: number, to: number): void => { out.push(text.slice(from, to)); };
33
+
34
+ const skipTemplate = (start: number): number => { // start is the index of the opening backtick; returns the index after the closing one
35
+ let j = start + 1;
36
+ while (j < n) {
37
+ const c = text[j];
38
+ if (c === '\\') { j += 2; continue; }
39
+ if (c === '`') 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 === '`') { j = skipTemplate(j); continue; }
46
+ if (d === '"' || d === "'") { j++; while (j < n && text[j] !== d && text[j] !== '\n') { if (text[j] === '\\') j++; j++; } j++; continue; }
47
+ if (d === '{') depth++;
48
+ else if (d === '}') depth--;
49
+ j++;
50
+ }
51
+ continue;
52
+ }
53
+ j++;
54
+ }
55
+ return n;
56
+ };
57
+
58
+ while (i < n) {
59
+ const c = text[i];
60
+ const c2 = text[i + 1];
61
+ if (lang === 'js') {
62
+ if (c === '/' && c2 === '/') { const e = text.indexOf('\n', i); const end = e < 0 ? n : e; put(i, end); i = end; continue; }
63
+ if (c === '/' && c2 === '*') { const e = text.indexOf('*/', i + 2); const end = e < 0 ? n : e + 2; put(i, end); i = end; continue; }
64
+ if (c === '`') { const end = skipTemplate(i); keep(i, i + 1); put(i + 1, Math.max(i + 1, end - 1)); if (end - 1 > i) keep(end - 1, end); i = end; prevSig = '`'; continue; }
65
+ if (c === '"' || c === "'") {
66
+ let j = i + 1;
67
+ while (j < n && text[j] !== c && text[j] !== '\n') { if (text[j] === '\\') j++; j++; }
68
+ const close = j < n && text[j] === c ? j : -1;
69
+ keep(i, i + 1);
70
+ put(i + 1, close < 0 ? Math.min(j, n) : close);
71
+ if (close >= 0) keep(close, close + 1);
72
+ i = close < 0 ? Math.min(j, n) : close + 1;
73
+ prevSig = c;
74
+ continue;
75
+ }
76
+ if (c === '/' && regexAllowed(text, i)) {
77
+ // regular expression literal
78
+ let j = i + 1;
79
+ let inClass = false;
80
+ while (j < n && text[j] !== '\n') {
81
+ if (text[j] === '\\') { j += 2; continue; }
82
+ if (text[j] === '[') inClass = true;
83
+ else if (text[j] === ']') inClass = false;
84
+ else if (text[j] === '/' && !inClass) break;
85
+ j++;
86
+ }
87
+ if (j < n && text[j] === '/') { keep(i, i + 1); put(i + 1, j); keep(j, j + 1); i = j + 1; prevSig = '/'; continue; }
88
+ }
89
+ } else {
90
+ if (c === '#') { const e = text.indexOf('\n', i); const end = e < 0 ? n : e; put(i, end); i = end; continue; }
91
+ if (c === '"' || c === "'") {
92
+ const triple = text[i + 1] === c && text[i + 2] === c;
93
+ if (triple) {
94
+ const e = text.indexOf(c.repeat(3), i + 3);
95
+ const end = e < 0 ? n : e + 3;
96
+ keep(i, i + 3); put(i + 3, Math.max(i + 3, end - 3)); if (e >= 0) keep(end - 3, end);
97
+ i = end; prevSig = c; continue;
98
+ }
99
+ let j = i + 1;
100
+ while (j < n && text[j] !== c && text[j] !== '\n') { if (text[j] === '\\') j++; j++; }
101
+ const close = j < n && text[j] === c ? j : -1;
102
+ keep(i, i + 1);
103
+ put(i + 1, close < 0 ? Math.min(j, n) : close);
104
+ if (close >= 0) keep(close, close + 1);
105
+ i = close < 0 ? Math.min(j, n) : close + 1;
106
+ prevSig = c;
107
+ continue;
108
+ }
109
+ }
110
+ keep(i, i + 1);
111
+ if (!/\s/.test(c)) prevSig = c;
112
+ i++;
113
+ }
114
+ return out.join('');
115
+ }
116
+
117
+ // Line numbers from character positions, by binary search over the line starts. Built once per file.
118
+ export class Lines {
119
+ private starts: number[] = [0];
120
+ constructor(text: string) { for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) this.starts.push(i + 1); }
121
+ at(index: number): number {
122
+ let lo = 0;
123
+ let hi = this.starts.length - 1;
124
+ while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (this.starts[mid] <= index) lo = mid; else hi = mid - 1; }
125
+ return lo + 1;
126
+ }
127
+ start(line: number): number { return this.starts[Math.max(0, Math.min(this.starts.length - 1, line - 1))]; }
128
+ get count(): number { return this.starts.length; }
129
+ }
130
+
131
+ const PAIR: Record<string, string> = { '(': ')', '{': '}', '[': ']' };
132
+
133
+ // The index just past the bracket that closes the one at `open`, in masked text, or -1.
134
+ export function closeOf(masked: string, open: number): number {
135
+ const stack: string[] = [];
136
+ for (let i = open; i < masked.length; i++) {
137
+ const c = masked[i];
138
+ if (PAIR[c]) stack.push(PAIR[c]);
139
+ else if (c === ')' || c === '}' || c === ']') {
140
+ if (c !== stack[stack.length - 1]) return -1;
141
+ stack.pop();
142
+ if (stack.length === 0) return i + 1;
143
+ }
144
+ }
145
+ return -1;
146
+ }
147
+
148
+ // Top level argument ranges of the call whose "(" is at `open`: [{start, end}] with end exclusive, trimmed.
149
+ export function splitArgs(masked: string, open: number): { start: number; end: number }[] {
150
+ const close = closeOf(masked, open);
151
+ if (close < 0) return [];
152
+ const args: { start: number; end: number }[] = [];
153
+ let depth = 0;
154
+ let from = open + 1;
155
+ const push = (end: number): void => {
156
+ let s = from;
157
+ let e = end;
158
+ while (s < e && /\s/.test(masked[s])) s++;
159
+ while (e > s && /\s/.test(masked[e - 1])) e--;
160
+ if (e > s) args.push({ start: s, end: e });
161
+ };
162
+ for (let i = open + 1; i < close - 1; i++) {
163
+ const c = masked[i];
164
+ if (c === '(' || c === '[' || c === '{') depth++;
165
+ else if (c === ')' || c === ']' || c === '}') depth--;
166
+ else if (c === ',' && depth === 0) { push(i); from = i + 1; }
167
+ else if (c === '=' && masked[i + 1] === '>' ) { /* arrow: the body follows; handled by depth of braces */ }
168
+ }
169
+ push(close - 1);
170
+ return args;
171
+ }
172
+
173
+ // The end (exclusive) of the statement that starts at `from`: the first ";" at depth 0, or a newline at depth 0 after which
174
+ // the next code character does not continue the expression (a ".", "?", ":", operator or closing bracket).
175
+ export function statementEnd(masked: string, from: number): number {
176
+ let depth = 0;
177
+ for (let i = from; i < masked.length; i++) {
178
+ const c = masked[i];
179
+ if (c === '(' || c === '[' || c === '{') depth++;
180
+ else if (c === ')' || c === ']' || c === '}') { if (depth === 0) return i; depth--; }
181
+ else if (c === ';' && depth === 0) return i;
182
+ else if (c === '\n' && depth === 0) {
183
+ const rest = masked.slice(i + 1, i + 200);
184
+ const next = /\S/.exec(rest);
185
+ const prev = masked.slice(from, i).trimEnd().slice(-1);
186
+ if (!next) return i;
187
+ const nc = rest[next.index];
188
+ if (nc === '.' || nc === '?' || nc === ':' || nc === '+' || nc === '|' || nc === '&' || nc === ',' || nc === ')' || nc === ']') continue;
189
+ if (prev === ',' || prev === '+' || prev === '=' || prev === '?' || prev === ':' || prev === '|' || prev === '&' || prev === '(') continue;
190
+ return i;
191
+ }
192
+ }
193
+ return masked.length;
194
+ }
195
+
196
+ export interface Extent { start: number; end: number; bodyStart: number }
197
+
198
+ // The extent of the JavaScript or TypeScript function that begins at `from` (the index of "async", "function", "(" or a
199
+ // single parameter name before "=>"). bodyStart is the first index of the body text (the "{" or the first token of an
200
+ // expression body). Returns null when the text there is not a function.
201
+ // `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.
202
+ export function jsFunction(masked: string, from: number, method = false): Extent | null {
203
+ let i = from;
204
+ const ws = (): void => { while (i < masked.length && /\s/.test(masked[i])) i++; };
205
+ ws();
206
+ if (masked.startsWith('async', i) && /\W/.test(masked[i + 5] ?? ' ')) { i += 5; ws(); }
207
+ let isFunctionKeyword = false;
208
+ if (masked.startsWith('function', i) && /\W/.test(masked[i + 8] ?? ' ')) { isFunctionKeyword = true; i += 8; ws(); if (masked[i] === '*') { i++; ws(); } while (/[\w$]/.test(masked[i] ?? '')) i++; ws(); if (masked[i] === '<') { let d = 0; for (; i < masked.length; i++) { if (masked[i] === '<') d++; else if (masked[i] === '>') { d--; if (d === 0) { i++; break; } } } ws(); } }
209
+ if (masked[i] === '(') {
210
+ const pc = closeOf(masked, i);
211
+ if (pc < 0) return null;
212
+ i = pc;
213
+ } else if (!isFunctionKeyword && /[\w$]/.test(masked[i] ?? '')) {
214
+ while (/[\w$]/.test(masked[i] ?? '')) i++;
215
+ ws();
216
+ if (!(masked[i] === '=' && masked[i + 1] === '>')) return null;
217
+ } else return null;
218
+ ws();
219
+ // Optional return type annotation: scan to the "{" (function) or "=>" (arrow) at bracket depth 0.
220
+ if (masked[i] === ':') {
221
+ let depth = 0;
222
+ for (i = i + 1; i < masked.length; i++) {
223
+ const c = masked[i];
224
+ if (c === '(' || c === '[') depth++;
225
+ else if (c === ')' || c === ']') depth--;
226
+ else if (c === '<') depth++;
227
+ else if (c === '>' && masked[i - 1] !== '=') depth--;
228
+ else if (c === '{' && depth <= 0 && !isFunctionKeyword && !method) { depth++; }
229
+ else if (c === '}' && depth > 0 && !isFunctionKeyword && !method) { depth--; }
230
+ else if (c === '=' && masked[i + 1] === '>' && depth <= 0) break;
231
+ else if (c === '{' && depth <= 0 && (isFunctionKeyword || method)) break;
232
+ }
233
+ }
234
+ ws();
235
+ if (masked[i] === '=' && masked[i + 1] === '>') {
236
+ i += 2;
237
+ ws();
238
+ if (masked[i] === '{') { const e = closeOf(masked, i); return e < 0 ? null : { start: from, end: e, bodyStart: i }; }
239
+ const e = statementEnd(masked, i);
240
+ return { start: from, end: e, bodyStart: i };
241
+ }
242
+ if (masked[i] === '{') { const e = closeOf(masked, i); return e < 0 ? null : { start: from, end: e, bodyStart: i }; }
243
+ return null;
244
+ }
245
+
246
+ // The Python block that belongs to the "def" or "class" line starting at `lineStart` (an index at the start of the line):
247
+ // the header (which may span several lines until the colon that ends it) and every following line indented deeper.
248
+ export function pyBlock(masked: string, lines: Lines, lineStart: number): Extent {
249
+ const indentOf = (ls: number): number => { let k = ls; let w = 0; while (masked[k] === ' ' || masked[k] === '\t') { w += masked[k] === '\t' ? 8 : 1; k++; } return w; };
250
+ const base = indentOf(lineStart);
251
+ // header ends at the first ":" at bracket depth 0 followed by end of line
252
+ let depth = 0;
253
+ let i = lineStart;
254
+ for (; i < masked.length; i++) {
255
+ const c = masked[i];
256
+ if (c === '(' || c === '[' || c === '{') depth++;
257
+ else if (c === ')' || c === ']' || c === '}') depth--;
258
+ else if (c === ':' && depth === 0 && /^[ \t]*(\n|$)/.test(masked.slice(i + 1, i + 40))) break;
259
+ 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 */ }
260
+ }
261
+ const bodyStart = Math.min(masked.length, i + 1);
262
+ let end = bodyStart;
263
+ const total = lines.count;
264
+ let ln = lines.at(bodyStart) + 1;
265
+ for (; ln <= total; ln++) {
266
+ const ls = lines.start(ln);
267
+ const le = masked.indexOf('\n', ls);
268
+ const row = masked.slice(ls, le < 0 ? masked.length : le);
269
+ if (row.trim() === '') { continue; }
270
+ if (indentOf(ls) <= base) break;
271
+ end = le < 0 ? masked.length : le;
272
+ }
273
+ return { start: lineStart, end, bodyStart };
274
+ }
@@ -0,0 +1,134 @@
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.ts';
4
+ import type { HelperRef } from './analyze.ts';
5
+ import { closeOf, statementEnd } from './lex.ts';
6
+ import type { MoreLang } from './lex-more.ts';
7
+ import type { Repo } from './repo.ts';
8
+
9
+ // The extent of a Go function whose "func" keyword is at `at`: [start, end) with the body inside braces.
10
+ export function goFunc(masked: string, at: number): { start: number; end: number; bodyStart: number; params: string } | null {
11
+ let i = at + 4;
12
+ while (/\s/.test(masked[i] ?? '')) i++;
13
+ if (masked[i] === '(' && /^\(\s*\w*\s*\*?\s*[\w.[\]]+\s*\)\s*\w/.test(masked.slice(i, i + 120))) { const c = closeOf(masked, i); if (c < 0) return null; i = c; while (/\s/.test(masked[i] ?? '')) i++; }
14
+ while (/[\w]/.test(masked[i] ?? '')) i++;
15
+ while (/\s/.test(masked[i] ?? '')) i++;
16
+ if (masked[i] === '[') { const c = closeOf(masked, i); if (c < 0) return null; i = c; }
17
+ if (masked[i] !== '(') return null;
18
+ const pc = closeOf(masked, i);
19
+ if (pc < 0) return null;
20
+ const params = masked.slice(i + 1, pc - 1);
21
+ for (i = pc; i < masked.length; i++) {
22
+ const c = masked[i];
23
+ if (c === '{') {
24
+ const before = masked.slice(Math.max(0, i - 12), i).trimEnd();
25
+ if (/(?:interface|struct)$/.test(before)) { const e = closeOf(masked, i); if (e < 0) return null; i = e - 1; continue; }
26
+ const e = closeOf(masked, i);
27
+ return e < 0 ? null : { start: at, end: e, bodyStart: i, params };
28
+ }
29
+ if (c === ';' ) return null;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ // A Ruby method: from "def" to the "end" at the same indentation.
35
+ export function rubyDef(masked: string, defAt: number): { start: number; end: number } {
36
+ const ls = masked.lastIndexOf('\n', defAt) + 1;
37
+ const indent = /^[ \t]*/.exec(masked.slice(ls))![0].length;
38
+ const oneLine = /^[^\n]*\bdef\b[^\n]*=\s*\S/.exec(masked.slice(defAt, defAt + 200));
39
+ const firstEnd = masked.indexOf('\n', defAt);
40
+ if (oneLine && !/\bend\b/.test(masked.slice(defAt, firstEnd < 0 ? masked.length : firstEnd))) return { start: defAt, end: firstEnd < 0 ? masked.length : firstEnd };
41
+ let pos = firstEnd < 0 ? masked.length : firstEnd + 1;
42
+ while (pos < masked.length) {
43
+ const e = masked.indexOf('\n', pos);
44
+ const row = masked.slice(pos, e < 0 ? masked.length : e);
45
+ const ind = /^[ \t]*/.exec(row)![0].length;
46
+ if (row.trim() !== '' && ind <= indent && /^\s*end\b/.test(row)) return { start: defAt, end: pos + row.length };
47
+ if (row.trim() !== '' && ind < indent) return { start: defAt, end: pos };
48
+ if (e < 0) break;
49
+ pos = e + 1;
50
+ }
51
+ return { start: defAt, end: masked.length };
52
+ }
53
+
54
+ // A PHP function or method: from "function" to the closing brace of its body.
55
+ export function phpFunc(masked: string, at: number): { start: number; end: number; bodyStart: number; params: string } | null {
56
+ const open = masked.indexOf('(', at);
57
+ if (open < 0) return null;
58
+ const pc = closeOf(masked, open);
59
+ if (pc < 0) return null;
60
+ const params = masked.slice(open + 1, pc - 1);
61
+ const m = /^[^{;]*\{/.exec(masked.slice(pc, pc + 300));
62
+ if (!m) return null;
63
+ const bodyStart = pc + m[0].length - 1;
64
+ const e = closeOf(masked, bodyStart);
65
+ return e < 0 ? null : { start: at, end: e, bodyStart, params };
66
+ }
67
+
68
+ // 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
69
+ // interface method (ending in ";") has no body and gives null.
70
+ export function cMethod(masked: string, at: number): { start: number; end: number; bodyStart: number; params: string } | null {
71
+ let i = at;
72
+ while (/[\w$]/.test(masked[i] ?? '')) i++;
73
+ while (/\s/.test(masked[i] ?? '')) i++;
74
+ if (masked[i] === '<') { let d = 0; for (; i < masked.length; i++) { if (masked[i] === '<') d++; else if (masked[i] === '>' && --d === 0) { i++; break; } else if (masked[i] === '\n' && d > 4) return null; } while (/\s/.test(masked[i] ?? '')) i++; }
75
+ if (masked[i] !== '(') return null;
76
+ const pc = closeOf(masked, i);
77
+ if (pc < 0) return null;
78
+ const params = masked.slice(i + 1, pc - 1);
79
+ for (let j = pc; j < masked.length; j++) {
80
+ const c = masked[j];
81
+ if (c === '{') { const e = closeOf(masked, j); return e < 0 ? null : { start: at, end: e, bodyStart: j, params }; }
82
+ if (c === ';') return null;
83
+ if (c === '=' && masked[j + 1] === '>') return { start: at, end: statementEnd(masked, j + 2), bodyStart: j + 2, params };
84
+ }
85
+ return null;
86
+ }
87
+
88
+ // Method declarations: at least one modifier, a type, a name and "(".
89
+ 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;
90
+
91
+ export class FnIndex {
92
+ private idx = new Map<string, HelperRef[]>();
93
+ readonly repo: Repo;
94
+ readonly lang: MoreLang;
95
+ constructor(repo: Repo, lang: MoreLang) {
96
+ this.repo = repo;
97
+ this.lang = lang;
98
+ const put = (name: string, ref: HelperRef): void => { const l = this.idx.get(name); if (l) l.push(ref); else this.idx.set(name, [ref]); };
99
+ for (const file of repo.filesOf(lang)) {
100
+ const v = repo.view(file);
101
+ if (!v) continue;
102
+ const m = v.masked;
103
+ if (lang === 'go') {
104
+ for (const x of m.matchAll(/^func\s+(?:\(\s*\w*\s*\*?\s*[\w.[\]]+\s*\)\s*)?(\w+)\s*[([]/gm)) {
105
+ const f = goFunc(m, x.index!);
106
+ if (f) put(x[1], { view: v, start: f.start, end: f.end });
107
+ }
108
+ } else if (lang === 'rb') {
109
+ for (const x of m.matchAll(/^[ \t]*def\s+(?:self\.)?(\w+[?!=]?)/gm)) {
110
+ const d = rubyDef(m, x.index! + x[0].indexOf('def'));
111
+ put(x[1], { view: v, start: d.start, end: d.end });
112
+ }
113
+ } else if (lang === 'java' || lang === 'cs') {
114
+ for (const x of m.matchAll(C_METHOD)) {
115
+ const f = cMethod(m, x.index! + x[0].lastIndexOf(x[1]));
116
+ if (f) put(x[1], { view: v, start: f.start, end: f.end });
117
+ }
118
+ } else {
119
+ for (const x of m.matchAll(/\bfunction\s+&?\s*(\w+)\s*\(/g)) {
120
+ const f = phpFunc(m, x.index!);
121
+ if (f) put(x[1], { view: v, start: f.start, end: f.end });
122
+ }
123
+ }
124
+ }
125
+ }
126
+ defs(name: string): HelperRef[] { return (this.idx.get(name) ?? []).slice().sort((a, b) => cmp(a.view.file, b.view.file) || a.start - b.start); }
127
+ find = (name: string, fromFile: string): HelperRef | null => {
128
+ const refs = this.idx.get(name);
129
+ if (!refs) return null;
130
+ if (refs.length === 1) return refs[0];
131
+ const same = refs.filter((r) => r.view.file === fromFile);
132
+ return same.length === 1 ? same[0] : null;
133
+ };
134
+ }