@shortlink-org/portolan 0.1.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 (826) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +387 -0
  3. package/catalog/enum_test.go +46 -0
  4. package/catalog/model.go +932 -0
  5. package/catalog/roundtrip_test.go +185 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/portolan.mjs +368 -0
  8. package/go.mod +14 -0
  9. package/go.sum +14 -0
  10. package/index.html +13 -0
  11. package/internal/commands/cargo.go +154 -0
  12. package/internal/commands/commands.go +143 -0
  13. package/internal/commands/commands_test.go +323 -0
  14. package/internal/commands/gradle.go +77 -0
  15. package/internal/commands/justfile.go +82 -0
  16. package/internal/commands/makefile.go +106 -0
  17. package/internal/commands/maven.go +93 -0
  18. package/internal/commands/packagejson.go +159 -0
  19. package/internal/commands/pyproject.go +169 -0
  20. package/internal/commands/taskfile.go +101 -0
  21. package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
  22. package/internal/commands/testdata/estate/Makefile +33 -0
  23. package/internal/commands/testdata/estate/Taskfile.yml +21 -0
  24. package/internal/commands/testdata/estate/build.gradle.kts +21 -0
  25. package/internal/commands/testdata/estate/justfile +20 -0
  26. package/internal/commands/testdata/estate/package.json +13 -0
  27. package/internal/commands/testdata/estate/pom.xml +29 -0
  28. package/internal/commands/testdata/estate/pyproject.toml +27 -0
  29. package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
  30. package/internal/commands/testdata/golden/commands.json +305 -0
  31. package/internal/gohttp/analyze.go +2500 -0
  32. package/internal/gohttp/endpoints.go +1067 -0
  33. package/internal/gohttp/roots.go +320 -0
  34. package/internal/gohttp/typed.go +143 -0
  35. package/internal/goscan/constants.go +85 -0
  36. package/internal/goscan/goscan_test.go +227 -0
  37. package/internal/goscan/names.go +52 -0
  38. package/internal/goscan/parse_test.go +11 -0
  39. package/internal/goscan/source.go +36 -0
  40. package/internal/goscan/tree.go +155 -0
  41. package/internal/goscan/types.go +99 -0
  42. package/internal/wsdl/ids.go +127 -0
  43. package/internal/wsdl/ids_test.go +21 -0
  44. package/internal/wsdl/model.go +62 -0
  45. package/internal/wsdl/parse.go +920 -0
  46. package/internal/wsdl/parse_test.go +133 -0
  47. package/package.json +117 -0
  48. package/plugin/describe.go +107 -0
  49. package/plugin/describe_test.go +114 -0
  50. package/plugin/protocol.go +117 -0
  51. package/plugin/schematest/schematest.go +126 -0
  52. package/plugins/README.md +1160 -0
  53. package/plugins/extract-adr/describe.go +19 -0
  54. package/plugins/extract-adr/describe_test.go +11 -0
  55. package/plugins/extract-adr/extract.go +153 -0
  56. package/plugins/extract-adr/extract_test.go +350 -0
  57. package/plugins/extract-adr/history.go +99 -0
  58. package/plugins/extract-adr/main.go +65 -0
  59. package/plugins/extract-adr/options.schema.json +26 -0
  60. package/plugins/extract-adr/parse.go +642 -0
  61. package/plugins/extract-adr/parse_test.go +405 -0
  62. package/plugins/extract-asyncapi/describe.go +19 -0
  63. package/plugins/extract-asyncapi/describe_test.go +11 -0
  64. package/plugins/extract-asyncapi/extract.go +315 -0
  65. package/plugins/extract-asyncapi/extract_test.go +197 -0
  66. package/plugins/extract-asyncapi/main.go +48 -0
  67. package/plugins/extract-asyncapi/options.schema.json +26 -0
  68. package/plugins/extract-asyncapi/spec.go +151 -0
  69. package/plugins/extract-celery/README.md +169 -0
  70. package/plugins/extract-celery/extract.py +346 -0
  71. package/plugins/extract-celery/extract_test.py +192 -0
  72. package/plugins/extract-celery/main.py +66 -0
  73. package/plugins/extract-celery/options.py +31 -0
  74. package/plugins/extract-celery/options.schema.json +31 -0
  75. package/plugins/extract-celery/producers.py +156 -0
  76. package/plugins/extract-celery/routes_test.py +106 -0
  77. package/plugins/extract-commands/describe.go +19 -0
  78. package/plugins/extract-commands/describe_test.go +11 -0
  79. package/plugins/extract-commands/extract.go +79 -0
  80. package/plugins/extract-commands/extract_test.go +92 -0
  81. package/plugins/extract-commands/main.go +55 -0
  82. package/plugins/extract-commands/options.schema.json +20 -0
  83. package/plugins/extract-csr/avro.go +258 -0
  84. package/plugins/extract-csr/describe.go +19 -0
  85. package/plugins/extract-csr/describe_test.go +11 -0
  86. package/plugins/extract-csr/extract.go +338 -0
  87. package/plugins/extract-csr/extract_test.go +374 -0
  88. package/plugins/extract-csr/jsonschema.go +343 -0
  89. package/plugins/extract-csr/lock.go +25 -0
  90. package/plugins/extract-csr/main.go +84 -0
  91. package/plugins/extract-csr/options.schema.json +43 -0
  92. package/plugins/extract-csr/subject.go +102 -0
  93. package/plugins/extract-django/README.md +386 -0
  94. package/plugins/extract-django/apps.py +100 -0
  95. package/plugins/extract-django/auth.py +302 -0
  96. package/plugins/extract-django/auth_test.py +188 -0
  97. package/plugins/extract-django/clients.py +121 -0
  98. package/plugins/extract-django/contracts.py +458 -0
  99. package/plugins/extract-django/database.py +119 -0
  100. package/plugins/extract-django/domain.py +318 -0
  101. package/plugins/extract-django/events.py +86 -0
  102. package/plugins/extract-django/extract.py +601 -0
  103. package/plugins/extract-django/extract_test.py +397 -0
  104. package/plugins/extract-django/flows.py +1278 -0
  105. package/plugins/extract-django/flows_test.py +338 -0
  106. package/plugins/extract-django/ids.py +42 -0
  107. package/plugins/extract-django/ids_test.py +30 -0
  108. package/plugins/extract-django/lifecycle.py +222 -0
  109. package/plugins/extract-django/lifecycle_test.py +76 -0
  110. package/plugins/extract-django/main.py +66 -0
  111. package/plugins/extract-django/openapi.py +229 -0
  112. package/plugins/extract-django/openapi_test.py +60 -0
  113. package/plugins/extract-django/operations.py +96 -0
  114. package/plugins/extract-django/options.py +61 -0
  115. package/plugins/extract-django/options.schema.json +126 -0
  116. package/plugins/extract-django/routing.py +214 -0
  117. package/plugins/extract-django/routing_test.py +198 -0
  118. package/plugins/extract-django/serializers.py +558 -0
  119. package/plugins/extract-django/store.py +434 -0
  120. package/plugins/extract-django/transport.py +242 -0
  121. package/plugins/extract-flows/describe.go +19 -0
  122. package/plugins/extract-flows/describe_test.go +11 -0
  123. package/plugins/extract-flows/extract.go +91 -0
  124. package/plugins/extract-flows/main.go +45 -0
  125. package/plugins/extract-flows/options.schema.json +16 -0
  126. package/plugins/extract-flows/parse.go +593 -0
  127. package/plugins/extract-flows/parse_test.go +204 -0
  128. package/plugins/extract-glossary/describe.go +19 -0
  129. package/plugins/extract-glossary/describe_test.go +11 -0
  130. package/plugins/extract-glossary/extract.go +115 -0
  131. package/plugins/extract-glossary/extract_test.go +220 -0
  132. package/plugins/extract-glossary/main.go +59 -0
  133. package/plugins/extract-glossary/options.schema.json +21 -0
  134. package/plugins/extract-glossary/parse.go +214 -0
  135. package/plugins/extract-glossary/parse_test.go +203 -0
  136. package/plugins/extract-go/aggregate.go +214 -0
  137. package/plugins/extract-go/client.go +409 -0
  138. package/plugins/extract-go/client_test.go +266 -0
  139. package/plugins/extract-go/describe.go +19 -0
  140. package/plugins/extract-go/describe_test.go +11 -0
  141. package/plugins/extract-go/enum.go +195 -0
  142. package/plugins/extract-go/enum_test.go +82 -0
  143. package/plugins/extract-go/event.go +99 -0
  144. package/plugins/extract-go/extract.go +191 -0
  145. package/plugins/extract-go/extract_test.go +261 -0
  146. package/plugins/extract-go/flow.go +1441 -0
  147. package/plugins/extract-go/flow_test.go +609 -0
  148. package/plugins/extract-go/httpclient.go +174 -0
  149. package/plugins/extract-go/httpclient_test.go +296 -0
  150. package/plugins/extract-go/ids.go +92 -0
  151. package/plugins/extract-go/layout.go +225 -0
  152. package/plugins/extract-go/layout_test.go +125 -0
  153. package/plugins/extract-go/lifecycle.go +324 -0
  154. package/plugins/extract-go/lifecycle_test.go +108 -0
  155. package/plugins/extract-go/main.go +79 -0
  156. package/plugins/extract-go/operation.go +157 -0
  157. package/plugins/extract-go/options.schema.json +66 -0
  158. package/plugins/extract-go/source.go +316 -0
  159. package/plugins/extract-go/transport.go +321 -0
  160. package/plugins/extract-go/transport_test.go +145 -0
  161. package/plugins/extract-go/wiring.go +434 -0
  162. package/plugins/extract-go-nats/describe.go +19 -0
  163. package/plugins/extract-go-nats/describe_test.go +11 -0
  164. package/plugins/extract-go-nats/extract.go +177 -0
  165. package/plugins/extract-go-nats/extract_test.go +389 -0
  166. package/plugins/extract-go-nats/index.go +379 -0
  167. package/plugins/extract-go-nats/main.go +42 -0
  168. package/plugins/extract-go-nats/options.schema.json +21 -0
  169. package/plugins/extract-go-nats/resolve.go +161 -0
  170. package/plugins/extract-go-nats/sites.go +224 -0
  171. package/plugins/extract-graphql/describe.go +19 -0
  172. package/plugins/extract-graphql/describe_test.go +11 -0
  173. package/plugins/extract-graphql/extract.go +433 -0
  174. package/plugins/extract-graphql/extract_test.go +256 -0
  175. package/plugins/extract-graphql/ids.go +49 -0
  176. package/plugins/extract-graphql/lex.go +237 -0
  177. package/plugins/extract-graphql/main.go +51 -0
  178. package/plugins/extract-graphql/options.schema.json +31 -0
  179. package/plugins/extract-graphql/parse.go +621 -0
  180. package/plugins/extract-graphql/parse_test.go +122 -0
  181. package/plugins/extract-http-clients/describe.go +19 -0
  182. package/plugins/extract-http-clients/describe_test.go +11 -0
  183. package/plugins/extract-http-clients/extract.go +705 -0
  184. package/plugins/extract-http-clients/extract_test.go +1263 -0
  185. package/plugins/extract-http-clients/main.go +42 -0
  186. package/plugins/extract-http-clients/options.schema.json +33 -0
  187. package/plugins/extract-java/README.md +190 -0
  188. package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
  189. package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
  190. package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
  191. package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
  192. package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
  193. package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
  194. package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
  195. package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
  196. package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
  197. package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
  198. package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
  199. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  200. package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
  201. package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
  202. package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
  203. package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
  204. package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
  205. package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
  206. package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
  207. package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
  208. package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
  209. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
  210. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
  211. package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
  212. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  213. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
  214. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
  215. package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
  216. package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
  217. package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
  218. package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
  219. package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
  220. package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
  221. package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
  222. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  223. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  224. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  225. package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
  226. package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
  227. package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
  228. package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
  229. package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
  230. package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
  231. package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
  232. package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
  233. package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
  234. package/plugins/extract-java/options.schema.json +70 -0
  235. package/plugins/extract-openapi/describe.go +19 -0
  236. package/plugins/extract-openapi/describe_test.go +11 -0
  237. package/plugins/extract-openapi/discover.go +243 -0
  238. package/plugins/extract-openapi/extract.go +526 -0
  239. package/plugins/extract-openapi/extract_test.go +545 -0
  240. package/plugins/extract-openapi/main.go +75 -0
  241. package/plugins/extract-openapi/options.schema.json +62 -0
  242. package/plugins/extract-openapi/spec.go +350 -0
  243. package/plugins/extract-project/README.md +22 -0
  244. package/plugins/extract-project/describe.go +19 -0
  245. package/plugins/extract-project/describe_test.go +11 -0
  246. package/plugins/extract-project/extract.go +221 -0
  247. package/plugins/extract-project/extract_test.go +109 -0
  248. package/plugins/extract-project/main.go +41 -0
  249. package/plugins/extract-project/options.schema.json +48 -0
  250. package/plugins/extract-proto/ast.go +125 -0
  251. package/plugins/extract-proto/consumes.go +77 -0
  252. package/plugins/extract-proto/describe.go +19 -0
  253. package/plugins/extract-proto/extract.go +293 -0
  254. package/plugins/extract-proto/extract_test.go +459 -0
  255. package/plugins/extract-proto/ids.go +89 -0
  256. package/plugins/extract-proto/ids_test.go +57 -0
  257. package/plugins/extract-proto/lex.go +285 -0
  258. package/plugins/extract-proto/main.go +100 -0
  259. package/plugins/extract-proto/module.go +120 -0
  260. package/plugins/extract-proto/options.schema.json +51 -0
  261. package/plugins/extract-proto/parse.go +720 -0
  262. package/plugins/extract-proto/parse_test.go +307 -0
  263. package/plugins/extract-proto/provides.go +236 -0
  264. package/plugins/extract-proto/resolve.go +222 -0
  265. package/plugins/extract-proto/resolve_test.go +119 -0
  266. package/plugins/extract-python-kafka/README.md +36 -0
  267. package/plugins/extract-python-kafka/extract.py +211 -0
  268. package/plugins/extract-python-kafka/extract_test.py +68 -0
  269. package/plugins/extract-python-kafka/main.py +59 -0
  270. package/plugins/extract-python-kafka/options.py +29 -0
  271. package/plugins/extract-python-kafka/options.schema.json +31 -0
  272. package/plugins/extract-redis/README.md +11 -0
  273. package/plugins/extract-redis/describe.go +19 -0
  274. package/plugins/extract-redis/describe_test.go +11 -0
  275. package/plugins/extract-redis/extract.go +183 -0
  276. package/plugins/extract-redis/extract_test.go +168 -0
  277. package/plugins/extract-redis/keyspaces.go +469 -0
  278. package/plugins/extract-redis/main.go +44 -0
  279. package/plugins/extract-redis/options.schema.json +30 -0
  280. package/plugins/extract-river/describe.go +19 -0
  281. package/plugins/extract-river/describe_test.go +11 -0
  282. package/plugins/extract-river/extract.go +507 -0
  283. package/plugins/extract-river/extract_test.go +132 -0
  284. package/plugins/extract-river/main.go +42 -0
  285. package/plugins/extract-river/options.schema.json +21 -0
  286. package/plugins/extract-rust/Cargo.lock +133 -0
  287. package/plugins/extract-rust/Cargo.toml +23 -0
  288. package/plugins/extract-rust/README.md +204 -0
  289. package/plugins/extract-rust/options.schema.json +65 -0
  290. package/plugins/extract-rust/rustfmt.toml +1 -0
  291. package/plugins/extract-sql/ddl.go +893 -0
  292. package/plugins/extract-sql/ddl_test.go +401 -0
  293. package/plugins/extract-sql/describe.go +19 -0
  294. package/plugins/extract-sql/describe_test.go +11 -0
  295. package/plugins/extract-sql/layout.go +221 -0
  296. package/plugins/extract-sql/layout_test.go +55 -0
  297. package/plugins/extract-sql/lineage.go +107 -0
  298. package/plugins/extract-sql/main.go +141 -0
  299. package/plugins/extract-sql/maps.go +564 -0
  300. package/plugins/extract-sql/maps_java.go +117 -0
  301. package/plugins/extract-sql/maps_rust.go +333 -0
  302. package/plugins/extract-sql/maps_rust_test.go +70 -0
  303. package/plugins/extract-sql/maps_test.go +204 -0
  304. package/plugins/extract-sql/maps_ts.go +398 -0
  305. package/plugins/extract-sql/maps_ts_test.go +136 -0
  306. package/plugins/extract-sql/options.schema.json +53 -0
  307. package/plugins/extract-sql/projection.go +70 -0
  308. package/plugins/extract-sql/projection_test.go +50 -0
  309. package/plugins/extract-sql/store.go +420 -0
  310. package/plugins/extract-sql/store_test.go +233 -0
  311. package/plugins/extract-sql/view.go +295 -0
  312. package/plugins/extract-ts/README.md +306 -0
  313. package/plugins/extract-ts/ast.test.ts +178 -0
  314. package/plugins/extract-ts/ast.ts +648 -0
  315. package/plugins/extract-ts/clients.ts +185 -0
  316. package/plugins/extract-ts/domain.ts +164 -0
  317. package/plugins/extract-ts/extract.test.ts +208 -0
  318. package/plugins/extract-ts/extract.ts +200 -0
  319. package/plugins/extract-ts/flows.ts +783 -0
  320. package/plugins/extract-ts/graphql.test.ts +73 -0
  321. package/plugins/extract-ts/graphql.ts +181 -0
  322. package/plugins/extract-ts/ids.test.ts +21 -0
  323. package/plugins/extract-ts/ids.ts +52 -0
  324. package/plugins/extract-ts/lifecycle.ts +138 -0
  325. package/plugins/extract-ts/main.ts +48 -0
  326. package/plugins/extract-ts/openapi.test.ts +64 -0
  327. package/plugins/extract-ts/openapi.ts +99 -0
  328. package/plugins/extract-ts/operations.ts +93 -0
  329. package/plugins/extract-ts/options.schema.json +96 -0
  330. package/plugins/extract-ts/source.test.ts +174 -0
  331. package/plugins/extract-ts/source.ts +465 -0
  332. package/plugins/extract-ts/transport.ts +147 -0
  333. package/plugins/extract-ts/tsconfig.json +13 -0
  334. package/plugins/extract-ts/wiring.ts +190 -0
  335. package/plugins/extract-watermill/describe.go +19 -0
  336. package/plugins/extract-watermill/describe_test.go +11 -0
  337. package/plugins/extract-watermill/extract.go +1228 -0
  338. package/plugins/extract-watermill/extract_test.go +234 -0
  339. package/plugins/extract-watermill/main.go +41 -0
  340. package/plugins/extract-watermill/options.schema.json +21 -0
  341. package/plugins/extract-wsdl/describe.go +19 -0
  342. package/plugins/extract-wsdl/describe_test.go +11 -0
  343. package/plugins/extract-wsdl/extract.go +148 -0
  344. package/plugins/extract-wsdl/extract_test.go +45 -0
  345. package/plugins/extract-wsdl/main.go +53 -0
  346. package/plugins/extract-wsdl/options.schema.json +59 -0
  347. package/plugins/fetch-bsr/auth.go +114 -0
  348. package/plugins/fetch-bsr/bsr.go +240 -0
  349. package/plugins/fetch-bsr/cache.go +66 -0
  350. package/plugins/fetch-bsr/describe.go +19 -0
  351. package/plugins/fetch-bsr/fetch.go +192 -0
  352. package/plugins/fetch-bsr/fetch_test.go +465 -0
  353. package/plugins/fetch-bsr/lock.go +71 -0
  354. package/plugins/fetch-bsr/main.go +117 -0
  355. package/plugins/fetch-bsr/options.schema.json +42 -0
  356. package/plugins/fetch-csr/auth.go +95 -0
  357. package/plugins/fetch-csr/cache.go +69 -0
  358. package/plugins/fetch-csr/describe.go +19 -0
  359. package/plugins/fetch-csr/describe_test.go +11 -0
  360. package/plugins/fetch-csr/fetch.go +254 -0
  361. package/plugins/fetch-csr/fetch_test.go +494 -0
  362. package/plugins/fetch-csr/lock.go +95 -0
  363. package/plugins/fetch-csr/main.go +122 -0
  364. package/plugins/fetch-csr/options.schema.json +42 -0
  365. package/plugins/fetch-csr/registry.go +209 -0
  366. package/plugins/fetch-git/cache.go +65 -0
  367. package/plugins/fetch-git/describe.go +19 -0
  368. package/plugins/fetch-git/describe_test.go +11 -0
  369. package/plugins/fetch-git/fetch.go +181 -0
  370. package/plugins/fetch-git/fetch_test.go +332 -0
  371. package/plugins/fetch-git/git.go +169 -0
  372. package/plugins/fetch-git/lock.go +72 -0
  373. package/plugins/fetch-git/main.go +127 -0
  374. package/plugins/fetch-git/offline.go +39 -0
  375. package/plugins/fetch-git/options.schema.json +42 -0
  376. package/plugins/fetch-git/pin.go +90 -0
  377. package/plugins/fetch-git/pin_test.go +74 -0
  378. package/plugins/gen-backstage/describe.go +17 -0
  379. package/plugins/gen-backstage/main.go +22 -0
  380. package/plugins/gen-backstage/options.schema.json +30 -0
  381. package/plugins/gen-backstage/plugin.go +473 -0
  382. package/plugins/gen-backstage/plugin_test.go +145 -0
  383. package/plugins/gen-backstage.wasm +0 -0
  384. package/plugins/gen-markdown/adr.go +162 -0
  385. package/plugins/gen-markdown/aggregate.go +362 -0
  386. package/plugins/gen-markdown/canonical.go +93 -0
  387. package/plugins/gen-markdown/context.go +90 -0
  388. package/plugins/gen-markdown/coverage_test.go +89 -0
  389. package/plugins/gen-markdown/describe.go +19 -0
  390. package/plugins/gen-markdown/describe_test.go +11 -0
  391. package/plugins/gen-markdown/external.go +71 -0
  392. package/plugins/gen-markdown/flow.go +277 -0
  393. package/plugins/gen-markdown/glossary.go +76 -0
  394. package/plugins/gen-markdown/glossary_test.go +148 -0
  395. package/plugins/gen-markdown/llms.go +302 -0
  396. package/plugins/gen-markdown/main.go +22 -0
  397. package/plugins/gen-markdown/markdown.go +254 -0
  398. package/plugins/gen-markdown/markdown_test.go +100 -0
  399. package/plugins/gen-markdown/module.go +107 -0
  400. package/plugins/gen-markdown/options.schema.json +16 -0
  401. package/plugins/gen-markdown/plugin.go +39 -0
  402. package/plugins/gen-markdown/quality_test.go +176 -0
  403. package/plugins/gen-markdown/redis_test.go +38 -0
  404. package/plugins/gen-markdown/render.go +344 -0
  405. package/plugins/gen-markdown/render_test.go +186 -0
  406. package/plugins/gen-markdown/service.go +456 -0
  407. package/plugins/gen-markdown/source.go +177 -0
  408. package/plugins/gen-markdown/store.go +201 -0
  409. package/plugins/gen-markdown.wasm +0 -0
  410. package/plugins/gen-mermaid/describe.go +17 -0
  411. package/plugins/gen-mermaid/main.go +22 -0
  412. package/plugins/gen-mermaid/options.schema.json +10 -0
  413. package/plugins/gen-mermaid/plugin.go +104 -0
  414. package/plugins/gen-mermaid/plugin_test.go +33 -0
  415. package/plugins/gen-mermaid.wasm +0 -0
  416. package/plugins/openapi/ids.go +261 -0
  417. package/plugins/openapi/ids_test.go +98 -0
  418. package/plugins/pyplugin/catalog.py +209 -0
  419. package/plugins/pyplugin/celery_conf.py +436 -0
  420. package/plugins/pyplugin/celery_tasks.py +76 -0
  421. package/plugins/pyplugin/kafka.py +570 -0
  422. package/plugins/pyplugin/names.py +38 -0
  423. package/plugins/pyplugin/protocol.py +79 -0
  424. package/plugins/pyplugin/source.py +285 -0
  425. package/plugins/verify-codeowners/describe.go +19 -0
  426. package/plugins/verify-codeowners/describe_test.go +11 -0
  427. package/plugins/verify-codeowners/main.go +75 -0
  428. package/plugins/verify-codeowners/match.go +85 -0
  429. package/plugins/verify-codeowners/match_test.go +47 -0
  430. package/plugins/verify-codeowners/options.schema.json +15 -0
  431. package/plugins/verify-codeowners/owners.go +164 -0
  432. package/plugins/verify-codeowners/owners_test.go +225 -0
  433. package/plugins/verify-codeowners/parse.go +90 -0
  434. package/plugins/verify-codeowners/parse_test.go +62 -0
  435. package/plugins/verify-otel/describe.go +19 -0
  436. package/plugins/verify-otel/describe_test.go +11 -0
  437. package/plugins/verify-otel/main.go +53 -0
  438. package/plugins/verify-otel/match.go +336 -0
  439. package/plugins/verify-otel/options.schema.json +30 -0
  440. package/plugins/verify-otel/otlp.go +200 -0
  441. package/plugins/verify-otel/verify.go +734 -0
  442. package/plugins/verify-otel/verify_test.go +460 -0
  443. package/portolan.json +1073 -0
  444. package/public/favicon.svg +12 -0
  445. package/schema/portolan.schema.json +2558 -0
  446. package/scripts/README.md +53 -0
  447. package/scripts/build-report.mjs +60 -0
  448. package/scripts/build-report.test.mjs +44 -0
  449. package/scripts/builtin-plugins.mjs +49 -0
  450. package/scripts/catalog-sources.mjs +67 -0
  451. package/scripts/diff.mjs +315 -0
  452. package/scripts/diff.test.mjs +151 -0
  453. package/scripts/forge-comment.mjs +144 -0
  454. package/scripts/forge-comment.test.mjs +151 -0
  455. package/scripts/forge-release.mjs +138 -0
  456. package/scripts/forge-release.test.mjs +106 -0
  457. package/scripts/forge.mjs +61 -0
  458. package/scripts/gen-likec4.mjs +854 -0
  459. package/scripts/gen-likec4.test.mjs +258 -0
  460. package/scripts/gen.mjs +603 -0
  461. package/scripts/generated-docs.test.mjs +66 -0
  462. package/scripts/local-api.mjs +1192 -0
  463. package/scripts/local-api.test.mjs +387 -0
  464. package/scripts/manifest.mjs +214 -0
  465. package/scripts/manifest.test.mjs +148 -0
  466. package/scripts/output-path.mjs +79 -0
  467. package/scripts/output-path.test.mjs +45 -0
  468. package/scripts/package-smoke.mjs +37 -0
  469. package/scripts/plugin-host.mjs +419 -0
  470. package/scripts/plugin-host.test.mjs +109 -0
  471. package/scripts/plugin-wasm-worker.mjs +34 -0
  472. package/scripts/run-builtin.mjs +81 -0
  473. package/scripts/schema.mjs +357 -0
  474. package/scripts/site-docs.mjs +295 -0
  475. package/scripts/site-docs.test.mjs +181 -0
  476. package/scripts/vendor-lock.mjs +58 -0
  477. package/scripts/vendor-lock.test.mjs +69 -0
  478. package/src/README.md +38 -0
  479. package/src/app/App.tsx +448 -0
  480. package/src/app/BranchPicker.test.ts +46 -0
  481. package/src/app/BranchPicker.tsx +153 -0
  482. package/src/app/Breadcrumbs.test.ts +38 -0
  483. package/src/app/Breadcrumbs.tsx +223 -0
  484. package/src/app/BuildStamp.tsx +146 -0
  485. package/src/app/CommandPalette.tsx +292 -0
  486. package/src/app/HashScroll.tsx +45 -0
  487. package/src/app/Sidebar.tsx +1742 -0
  488. package/src/app/TopBar.tsx +393 -0
  489. package/src/app/density.tsx +64 -0
  490. package/src/app/forge-access.test.ts +27 -0
  491. package/src/app/forge-access.tsx +80 -0
  492. package/src/app/panels.tsx +141 -0
  493. package/src/app/pins.tsx +242 -0
  494. package/src/app/query-client.ts +24 -0
  495. package/src/app/responsive.ts +55 -0
  496. package/src/app/shortcuts.test.ts +39 -0
  497. package/src/app/shortcuts.tsx +282 -0
  498. package/src/app/theme.tsx +51 -0
  499. package/src/app/title.test.ts +14 -0
  500. package/src/app/title.ts +30 -0
  501. package/src/app/toast.tsx +59 -0
  502. package/src/app/tree-keys.test.ts +80 -0
  503. package/src/app/tree-keys.ts +87 -0
  504. package/src/app/ui-store.ts +52 -0
  505. package/src/catalog-error.test.ts +105 -0
  506. package/src/catalog-profile.test.ts +59 -0
  507. package/src/catalog-profile.ts +101 -0
  508. package/src/catalog-stores.test.ts +431 -0
  509. package/src/catalog.test.ts +1215 -0
  510. package/src/catalog.ts +3125 -0
  511. package/src/chat/Cards.tsx +408 -0
  512. package/src/chat/ChatPanel.tsx +79 -0
  513. package/src/chat/ChatSettings.tsx +219 -0
  514. package/src/chat/Composer.tsx +99 -0
  515. package/src/chat/Conversation.tsx +281 -0
  516. package/src/chat/Header.tsx +46 -0
  517. package/src/chat/Message.tsx +49 -0
  518. package/src/chat/Notice.tsx +195 -0
  519. package/src/chat/Starter.tsx +67 -0
  520. package/src/chat/Steps.tsx +45 -0
  521. package/src/chat/Waiting.tsx +87 -0
  522. package/src/chat/flags.test.ts +59 -0
  523. package/src/chat/flags.ts +155 -0
  524. package/src/chat/linkify.test.ts +30 -0
  525. package/src/chat/linkify.ts +45 -0
  526. package/src/chat/prefs.ts +56 -0
  527. package/src/chat/prompt.test.ts +43 -0
  528. package/src/chat/prompt.ts +157 -0
  529. package/src/chat/store.ts +35 -0
  530. package/src/chat/tools.ts +58 -0
  531. package/src/chat/transport.ts +70 -0
  532. package/src/components/AdrRow.tsx +29 -0
  533. package/src/components/AnchorLink.tsx +67 -0
  534. package/src/components/ApiReference.tsx +134 -0
  535. package/src/components/AsyncApiReference.tsx +120 -0
  536. package/src/components/CatalogStamp.tsx +155 -0
  537. package/src/components/ChannelRows.tsx +132 -0
  538. package/src/components/CommandRows.tsx +139 -0
  539. package/src/components/DiagramSkeleton.tsx +71 -0
  540. package/src/components/EditorLink.tsx +85 -0
  541. package/src/components/FieldTree.tsx +305 -0
  542. package/src/components/FlowTrigger.tsx +34 -0
  543. package/src/components/Ident.tsx +89 -0
  544. package/src/components/Integrations.tsx +243 -0
  545. package/src/components/LifecycleDiagram.tsx +110 -0
  546. package/src/components/MachineDocs.tsx +42 -0
  547. package/src/components/Markdown.tsx +173 -0
  548. package/src/components/MarkdownTable.tsx +206 -0
  549. package/src/components/Mermaid.tsx +211 -0
  550. package/src/components/MethodRows.tsx +309 -0
  551. package/src/components/Overlay.tsx +125 -0
  552. package/src/components/PageHeader.tsx +189 -0
  553. package/src/components/RowActions.tsx +97 -0
  554. package/src/components/SchemaDocument.tsx +89 -0
  555. package/src/components/Select.tsx +126 -0
  556. package/src/components/ShapeRows.tsx +118 -0
  557. package/src/components/SourceDoc.tsx +82 -0
  558. package/src/components/SourcePreview.tsx +441 -0
  559. package/src/components/TabRow.tsx +182 -0
  560. package/src/components/TechIcon.tsx +34 -0
  561. package/src/components/Toc.tsx +132 -0
  562. package/src/components/WhatLinksHere.tsx +273 -0
  563. package/src/components/WsdlReference.tsx +84 -0
  564. package/src/components/ddd-icons.tsx +107 -0
  565. package/src/components/kind.tsx +157 -0
  566. package/src/components/logo.tsx +61 -0
  567. package/src/components/primitives.tsx +264 -0
  568. package/src/data.test.ts +18 -0
  569. package/src/data.ts +202 -0
  570. package/src/enrich.test.ts +1150 -0
  571. package/src/enrich.ts +672 -0
  572. package/src/er/ErCanvas.tsx +533 -0
  573. package/src/er/RedisSchema.tsx +74 -0
  574. package/src/er/RelationCard.tsx +267 -0
  575. package/src/er/StoreHeader.tsx +155 -0
  576. package/src/er/TableNode.tsx +84 -0
  577. package/src/er/ViewNode.tsx +83 -0
  578. package/src/er/layout.test.ts +115 -0
  579. package/src/er/layout.ts +77 -0
  580. package/src/er/lineage.test.ts +140 -0
  581. package/src/er/lineage.ts +107 -0
  582. package/src/er/markers.tsx +79 -0
  583. package/src/er/spec.test.ts +344 -0
  584. package/src/er/spec.ts +426 -0
  585. package/src/flow/ChainList.tsx +162 -0
  586. package/src/flow/FlowTable.tsx +123 -0
  587. package/src/flow/FlowToolbar.tsx +372 -0
  588. package/src/flow/StepDetail.tsx +380 -0
  589. package/src/flow/StepRail.tsx +428 -0
  590. package/src/flow/answers.test.ts +50 -0
  591. package/src/flow/answers.ts +58 -0
  592. package/src/flow/chain.test.ts +383 -0
  593. package/src/flow/chain.ts +353 -0
  594. package/src/flow/chapters.test.ts +185 -0
  595. package/src/flow/chapters.ts +225 -0
  596. package/src/flow/continues.test.ts +102 -0
  597. package/src/flow/continues.ts +65 -0
  598. package/src/flow/cross-context.test.ts +97 -0
  599. package/src/flow/cross-context.ts +35 -0
  600. package/src/flow/mermaid.test.ts +22 -0
  601. package/src/flow/mermaid.ts +141 -0
  602. package/src/flow/outline.test.ts +212 -0
  603. package/src/flow/outline.ts +170 -0
  604. package/src/flow/paths.test.ts +129 -0
  605. package/src/flow/paths.ts +147 -0
  606. package/src/flow/prefs.test.ts +80 -0
  607. package/src/flow/prefs.ts +104 -0
  608. package/src/graph/DependencyGraph.tsx +283 -0
  609. package/src/graph/DependencyNodes.tsx +162 -0
  610. package/src/graph/EdgeLabel.tsx +61 -0
  611. package/src/graph/FocusedEventGraph.tsx +211 -0
  612. package/src/graph/GraphToolbar.tsx +187 -0
  613. package/src/graph/RoutedEdge.tsx +80 -0
  614. package/src/graph/dependency-layout.test.ts +360 -0
  615. package/src/graph/dependency-layout.ts +444 -0
  616. package/src/graph/elk.test.ts +70 -0
  617. package/src/graph/elk.ts +287 -0
  618. package/src/graph/nodes.tsx +97 -0
  619. package/src/graph/theme.ts +81 -0
  620. package/src/graph/useElkFlow.ts +125 -0
  621. package/src/index.css +1547 -0
  622. package/src/language/InLanguage.tsx +40 -0
  623. package/src/language/TermCard.tsx +132 -0
  624. package/src/language/cards.test.ts +114 -0
  625. package/src/language/cards.ts +111 -0
  626. package/src/lib/admonition.test.ts +49 -0
  627. package/src/lib/admonition.ts +48 -0
  628. package/src/lib/adr.test.ts +131 -0
  629. package/src/lib/adr.ts +68 -0
  630. package/src/lib/api.test.ts +122 -0
  631. package/src/lib/api.ts +135 -0
  632. package/src/lib/backlinks.test.ts +330 -0
  633. package/src/lib/backlinks.ts +567 -0
  634. package/src/lib/branch-compare.ts +16 -0
  635. package/src/lib/build-info.test.ts +76 -0
  636. package/src/lib/build-info.ts +80 -0
  637. package/src/lib/catalog-diff.test.ts +275 -0
  638. package/src/lib/catalog-diff.ts +655 -0
  639. package/src/lib/clipboard.ts +43 -0
  640. package/src/lib/comparison-memory.test.ts +40 -0
  641. package/src/lib/comparison-memory.ts +39 -0
  642. package/src/lib/context-color.ts +27 -0
  643. package/src/lib/context-map.test.ts +148 -0
  644. package/src/lib/context-map.ts +399 -0
  645. package/src/lib/data-model.test.ts +245 -0
  646. package/src/lib/data-model.ts +399 -0
  647. package/src/lib/data-problems.test.ts +178 -0
  648. package/src/lib/data-problems.ts +314 -0
  649. package/src/lib/derive.test.ts +251 -0
  650. package/src/lib/derive.ts +515 -0
  651. package/src/lib/editor-link.test.ts +125 -0
  652. package/src/lib/editor-link.ts +96 -0
  653. package/src/lib/editor-prefs.ts +27 -0
  654. package/src/lib/event-graph.test.ts +237 -0
  655. package/src/lib/event-graph.ts +327 -0
  656. package/src/lib/export-canvas.ts +37 -0
  657. package/src/lib/export-file.test.ts +30 -0
  658. package/src/lib/export-file.ts +20 -0
  659. package/src/lib/flow-tree.test.ts +177 -0
  660. package/src/lib/flow-tree.ts +160 -0
  661. package/src/lib/forge-refs.test.ts +50 -0
  662. package/src/lib/forge-refs.ts +60 -0
  663. package/src/lib/format.test.ts +35 -0
  664. package/src/lib/format.ts +64 -0
  665. package/src/lib/github-catalog.test.ts +301 -0
  666. package/src/lib/github-catalog.ts +610 -0
  667. package/src/lib/integrations.test.ts +119 -0
  668. package/src/lib/integrations.ts +133 -0
  669. package/src/lib/kinds.test.ts +91 -0
  670. package/src/lib/kinds.ts +204 -0
  671. package/src/lib/lifecycle.test.ts +93 -0
  672. package/src/lib/lifecycle.ts +165 -0
  673. package/src/lib/local-api.ts +194 -0
  674. package/src/lib/motion-features.ts +11 -0
  675. package/src/lib/motion.test.ts +120 -0
  676. package/src/lib/motion.tsx +269 -0
  677. package/src/lib/palette-recent.test.ts +80 -0
  678. package/src/lib/palette-recent.ts +90 -0
  679. package/src/lib/palette.test.ts +365 -0
  680. package/src/lib/palette.ts +593 -0
  681. package/src/lib/pins.test.ts +139 -0
  682. package/src/lib/pins.ts +130 -0
  683. package/src/lib/problems.test.ts +112 -0
  684. package/src/lib/project-preview.ts +5 -0
  685. package/src/lib/proto-problems.test.ts +121 -0
  686. package/src/lib/proto-problems.ts +65 -0
  687. package/src/lib/queries.test.ts +113 -0
  688. package/src/lib/queries.ts +131 -0
  689. package/src/lib/registry.test.ts +219 -0
  690. package/src/lib/registry.ts +208 -0
  691. package/src/lib/scenarios.ts +547 -0
  692. package/src/lib/setup-info.test.ts +199 -0
  693. package/src/lib/setup-info.ts +415 -0
  694. package/src/lib/shape.test.ts +289 -0
  695. package/src/lib/shape.ts +454 -0
  696. package/src/lib/sidebar-prefs.test.ts +32 -0
  697. package/src/lib/sidebar-prefs.ts +75 -0
  698. package/src/lib/source-code.test.ts +159 -0
  699. package/src/lib/source-code.ts +286 -0
  700. package/src/lib/source-doc.test.ts +131 -0
  701. package/src/lib/source-doc.ts +103 -0
  702. package/src/lib/source-highlight.test.ts +29 -0
  703. package/src/lib/source-highlight.ts +24 -0
  704. package/src/lib/source-link.test.ts +153 -0
  705. package/src/lib/source-link.ts +201 -0
  706. package/src/lib/spec-files.test.ts +24 -0
  707. package/src/lib/spec-files.ts +23 -0
  708. package/src/lib/table-directive.test.ts +139 -0
  709. package/src/lib/table-directive.ts +106 -0
  710. package/src/lib/tech.test.ts +120 -0
  711. package/src/lib/tech.ts +421 -0
  712. package/src/lib/terms.test.ts +67 -0
  713. package/src/lib/terms.ts +179 -0
  714. package/src/lib/wire-problems.test.ts +263 -0
  715. package/src/lib/wire-problems.ts +281 -0
  716. package/src/likec4/C4View.tsx +55 -0
  717. package/src/likec4/CanvasBridge.tsx +187 -0
  718. package/src/likec4/FlowView.tsx +235 -0
  719. package/src/likec4/InteractiveView.tsx +144 -0
  720. package/src/likec4/canvas-viewport.test.ts +149 -0
  721. package/src/likec4/canvas-viewport.ts +135 -0
  722. package/src/likec4/flow-edges.test.ts +51 -0
  723. package/src/likec4/flow-edges.ts +89 -0
  724. package/src/likec4/frame-theme.test.ts +76 -0
  725. package/src/likec4/frame-theme.ts +101 -0
  726. package/src/likec4/highlight-css.test.ts +49 -0
  727. package/src/likec4/highlight-css.ts +52 -0
  728. package/src/likec4/ids.test.ts +151 -0
  729. package/src/likec4/ids.ts +115 -0
  730. package/src/likec4/levels.tsx +70 -0
  731. package/src/likec4/mapping.test.ts +66 -0
  732. package/src/likec4/mapping.ts +54 -0
  733. package/src/likec4/reserved.json +141 -0
  734. package/src/likec4/view-index.ts +78 -0
  735. package/src/main.tsx +13 -0
  736. package/src/map/ContextMapGraph.tsx +274 -0
  737. package/src/map/ContextNode.tsx +70 -0
  738. package/src/map/FloatingEdge.tsx +86 -0
  739. package/src/merge.test.ts +1038 -0
  740. package/src/merge.ts +1004 -0
  741. package/src/pages/AdrDetail.tsx +242 -0
  742. package/src/pages/AdrIndex.tsx +114 -0
  743. package/src/pages/AggregatePage.tsx +727 -0
  744. package/src/pages/BlockPage.tsx +478 -0
  745. package/src/pages/CatalogFailure.tsx +111 -0
  746. package/src/pages/Changes.tsx +313 -0
  747. package/src/pages/ContextMap.tsx +534 -0
  748. package/src/pages/ContextPage.tsx +346 -0
  749. package/src/pages/EnumPage.tsx +282 -0
  750. package/src/pages/EventPage.tsx +686 -0
  751. package/src/pages/ExternalPage.tsx +204 -0
  752. package/src/pages/FlowDetail.tsx +663 -0
  753. package/src/pages/FlowIndex.tsx +303 -0
  754. package/src/pages/GraphPage.tsx +217 -0
  755. package/src/pages/Language.tsx +257 -0
  756. package/src/pages/ModulePage.tsx +366 -0
  757. package/src/pages/NotFound.tsx +54 -0
  758. package/src/pages/Overview.tsx +344 -0
  759. package/src/pages/Problems.tsx +360 -0
  760. package/src/pages/RegistryIndex.tsx +118 -0
  761. package/src/pages/ServicePage.tsx +746 -0
  762. package/src/pages/Settings.tsx +975 -0
  763. package/src/pages/StorePage.tsx +114 -0
  764. package/src/peek/PeekCard.test.ts +35 -0
  765. package/src/peek/PeekCard.tsx +118 -0
  766. package/src/peek/PeekLayer.tsx +147 -0
  767. package/src/peek/model.test.ts +156 -0
  768. package/src/peek/model.ts +253 -0
  769. package/src/routes.test.ts +222 -0
  770. package/src/routes.ts +449 -0
  771. package/src/selection/DetailPanel.tsx +1407 -0
  772. package/src/selection/hash.test.ts +73 -0
  773. package/src/selection/hash.ts +58 -0
  774. package/src/selection/model.test.ts +199 -0
  775. package/src/selection/model.ts +473 -0
  776. package/src/selection/pages.test.ts +115 -0
  777. package/src/selection/pages.ts +268 -0
  778. package/src/selection/store.test.ts +62 -0
  779. package/src/selection/store.ts +46 -0
  780. package/src/selection/sync.tsx +99 -0
  781. package/src/table/ColumnsMenu.tsx +72 -0
  782. package/src/table/DataTable.tsx +780 -0
  783. package/src/table/Facets.tsx +54 -0
  784. package/src/table/GroupMenu.tsx +69 -0
  785. package/src/table/Toolbar.tsx +190 -0
  786. package/src/table/cells.tsx +117 -0
  787. package/src/table/compare.test.ts +117 -0
  788. package/src/table/compare.ts +196 -0
  789. package/src/table/export.test.ts +88 -0
  790. package/src/table/export.ts +66 -0
  791. package/src/table/facet-groups.test.ts +74 -0
  792. package/src/table/facet-groups.ts +56 -0
  793. package/src/table/grouping.test.ts +84 -0
  794. package/src/table/grouping.ts +82 -0
  795. package/src/table/infer.test.ts +57 -0
  796. package/src/table/infer.ts +50 -0
  797. package/src/table/persist.test.ts +76 -0
  798. package/src/table/persist.ts +112 -0
  799. package/src/table/sort-url.test.ts +75 -0
  800. package/src/table/sort-url.ts +52 -0
  801. package/src/table/types.ts +80 -0
  802. package/src/table/useTableState.ts +220 -0
  803. package/src/test-catalog.ts +7 -0
  804. package/src/testing/estate/data__catalog.json +2847 -0
  805. package/src/testing/estate/data__flows.json +1724 -0
  806. package/src/testing/estate/examples__auth__portolan__api.json +212 -0
  807. package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
  808. package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
  809. package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
  810. package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
  811. package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
  812. package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
  813. package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
  814. package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
  815. package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
  816. package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
  817. package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
  818. package/src/testing/estate.ts +49 -0
  819. package/src/trail/Trail.tsx +81 -0
  820. package/src/trail/model.test.ts +157 -0
  821. package/src/trail/model.ts +189 -0
  822. package/src/trail/record.tsx +32 -0
  823. package/src/trail/store.test.ts +66 -0
  824. package/src/trail/store.ts +102 -0
  825. package/tsconfig.json +35 -0
  826. package/vite.config.ts +171 -0
@@ -0,0 +1,2500 @@
1
+ // Package gohttp reads outbound HTTP-shaped calls from Go source. Its syntax
2
+ // analysis always works without loading dependencies; when a module can be
3
+ // type-checked, an optional SSA/VTA pass adds source-backed dynamic call edges.
4
+ // Every result points back to the source expression that proves it.
5
+ package gohttp
6
+
7
+ import (
8
+ "bytes"
9
+ "fmt"
10
+ "go/ast"
11
+ "go/parser"
12
+ "go/printer"
13
+ "go/token"
14
+ "io/fs"
15
+ "mime"
16
+ "net/url"
17
+ "os"
18
+ "path/filepath"
19
+ "sort"
20
+ "strconv"
21
+ "strings"
22
+ "unicode"
23
+
24
+ "github.com/shortlink-org/portolan/internal/wsdl"
25
+ "github.com/shortlink-org/portolan/plugins/openapi"
26
+ )
27
+
28
+ type Source struct {
29
+ File string
30
+ Line int
31
+ }
32
+
33
+ func (s Source) String() string {
34
+ if s.Line == 0 {
35
+ return s.File
36
+ }
37
+ return fmt.Sprintf("%s:%d", s.File, s.Line)
38
+ }
39
+
40
+ type Contract struct {
41
+ Protocol string
42
+ API string
43
+ External string
44
+ Name string
45
+ Summary string
46
+ URL string
47
+ Source string
48
+ Operations []openapi.Operation
49
+ SOAP []SOAPOperation
50
+ MethodCalls map[string]openapi.Operation
51
+ }
52
+
53
+ type SOAPOperation struct {
54
+ ID string
55
+ Interface string
56
+ Action string
57
+ Request string
58
+ Response string
59
+ Version string
60
+ Style string
61
+ Binding string
62
+ Endpoint string
63
+ Faults []string
64
+ Headers []string
65
+ }
66
+
67
+ type Call struct {
68
+ Function string
69
+ Source Source
70
+ Protocol string
71
+ Method string
72
+ Path string
73
+ Endpoint string
74
+ Action string
75
+ SOAPVersion string
76
+ Request string
77
+ Response string
78
+ API string
79
+ ID string
80
+ External string
81
+ Contract string
82
+ Conditions []string
83
+ Chain []string
84
+ URLTrace []string
85
+ template *callTemplate
86
+ }
87
+
88
+ type FlowGroup struct {
89
+ Function string
90
+ Calls []Call
91
+ Source Source
92
+ Callers []string
93
+ }
94
+
95
+ type RootKind string
96
+
97
+ const (
98
+ RootHTTP RootKind = "http"
99
+ RootCallback RootKind = "callback"
100
+ RootStartup RootKind = "startup"
101
+ RootScheduled RootKind = "scheduled"
102
+ )
103
+
104
+ // RootFlow is a source-backed way execution enters a group of outbound calls
105
+ // without going through provider selection. Provider endpoints retain their
106
+ // richer branch model in EndpointFlow.
107
+ type RootFlow struct {
108
+ Kind RootKind
109
+ Method string
110
+ Path string
111
+ Handler string
112
+ Label string
113
+ Confidence string
114
+ Source Source
115
+ Calls []Call
116
+ Covered []string
117
+ }
118
+
119
+ type EndpointFlow struct {
120
+ Method string
121
+ Path string
122
+ Handler string
123
+ Source Source
124
+ Branches []EndpointBranch
125
+ }
126
+
127
+ type EndpointBranch struct {
128
+ Condition string
129
+ Provider string
130
+ Operation string
131
+ Function string
132
+ Calls []Call
133
+ Source Source
134
+ }
135
+
136
+ type Result struct {
137
+ Calls []Call
138
+ Contracts []Contract
139
+ Flows []FlowGroup
140
+ EndpointFlows []EndpointFlow
141
+ RootFlows []RootFlow
142
+ TypedCallGraph bool
143
+ TypedCallGraphError string
144
+ Warnings []string
145
+ }
146
+
147
+ type parsedFile struct {
148
+ abs string
149
+ rel string
150
+ dir string
151
+ pkg string
152
+ imports map[string]string
153
+ node *ast.File
154
+ generated bool
155
+ }
156
+
157
+ type constValue struct {
158
+ expr ast.Expr
159
+ file *parsedFile
160
+ }
161
+
162
+ type scanner struct {
163
+ root string
164
+ fset *token.FileSet
165
+ files []*parsedFile
166
+ constants map[string]constValue
167
+ contracts []Contract
168
+ warnings []string
169
+ functions map[string]*functionDecl
170
+ methods map[string][]string
171
+ soap map[string][]soapWrapper
172
+ soapFns map[string]bool
173
+ fields map[string]fieldOrigin
174
+ fieldTypes map[string][]endpointType
175
+ typedEdges map[string][]localEdge
176
+ typedCallGraphError string
177
+ }
178
+
179
+ type functionDecl struct {
180
+ key string
181
+ file *parsedFile
182
+ fn *ast.FuncDecl
183
+ }
184
+
185
+ type localEdge struct {
186
+ target string
187
+ line int
188
+ args []ast.Expr
189
+ }
190
+
191
+ type callTemplate struct {
192
+ method ast.Expr
193
+ endpoint ast.Expr
194
+ }
195
+
196
+ type fieldOrigin struct {
197
+ value string
198
+ chain []string
199
+ constructor string
200
+ }
201
+
202
+ type fieldSetter struct {
203
+ owner endpointType
204
+ field string
205
+ param int
206
+ }
207
+
208
+ type valuePart struct {
209
+ literal string
210
+ param int
211
+ isParam bool
212
+ }
213
+
214
+ type soapWrapper struct {
215
+ function string
216
+ name string
217
+ arity int
218
+ variadic bool
219
+ action []valuePart
220
+ request int
221
+ response int
222
+ endpoint []valuePart
223
+ contentTypeAction bool
224
+ version string
225
+ }
226
+
227
+ func Analyze(root string) (Result, error) {
228
+ s := &scanner{root: root, fset: token.NewFileSet(), constants: map[string]constValue{}, functions: map[string]*functionDecl{}, methods: map[string][]string{}, soap: map[string][]soapWrapper{}, soapFns: map[string]bool{}, fields: map[string]fieldOrigin{}, fieldTypes: map[string][]endpointType{}, typedEdges: map[string][]localEdge{}}
229
+ if err := s.read(); err != nil {
230
+ return Result{}, err
231
+ }
232
+ s.indexConstants()
233
+ s.indexFunctions()
234
+ s.indexConcreteFieldTypes()
235
+ s.indexFieldOrigins()
236
+ s.resolveFieldOriginsAtCallSites()
237
+ s.readContracts()
238
+ s.readWSDLContracts()
239
+ s.indexSOAPWrappers()
240
+
241
+ var calls []Call
242
+ for _, file := range s.files {
243
+ if file.generated {
244
+ continue
245
+ }
246
+ for _, decl := range file.node.Decls {
247
+ fn, ok := decl.(*ast.FuncDecl)
248
+ if !ok || fn.Body == nil {
249
+ continue
250
+ }
251
+ locals := s.localStrings(file, fn)
252
+ function := functionKey(file, fn)
253
+ s.walkStatements(file, function, fn.Body.List, nil, locals, &calls)
254
+ }
255
+ }
256
+
257
+ sort.Slice(calls, func(i, j int) bool {
258
+ if calls[i].Source.File != calls[j].Source.File {
259
+ return calls[i].Source.File < calls[j].Source.File
260
+ }
261
+ if calls[i].Source.Line != calls[j].Source.Line {
262
+ return calls[i].Source.Line < calls[j].Source.Line
263
+ }
264
+ return calls[i].ID < calls[j].ID
265
+ })
266
+ calls = uniqueCalls(calls)
267
+ sort.Strings(s.warnings)
268
+ directCalls := calls
269
+ flows := s.flowGroups(directCalls)
270
+ calls = callsSpecializedByFlows(directCalls, flows)
271
+ endpointFlows := s.endpointFlows(flows)
272
+ if s.endpointFlowsNeedTypedCalls(endpointFlows) && s.indexTypedCallEdges() {
273
+ flows = s.flowGroups(directCalls)
274
+ calls = callsSpecializedByFlows(directCalls, flows)
275
+ endpointFlows = s.endpointFlows(flows)
276
+ }
277
+ rootFlows := s.rootFlows(flows, endpointFlows)
278
+ return Result{Calls: calls, Contracts: s.contracts, Flows: flows, EndpointFlows: endpointFlows, RootFlows: rootFlows, TypedCallGraph: len(s.typedEdges) > 0, TypedCallGraphError: s.typedCallGraphError, Warnings: s.warnings}, nil
279
+ }
280
+
281
+ func (s *scanner) endpointFlowsNeedTypedCalls(flows []EndpointFlow) bool {
282
+ for _, flow := range flows {
283
+ for _, branch := range flow.Branches {
284
+ if len(branch.Calls) == 0 {
285
+ return true
286
+ }
287
+ }
288
+ }
289
+ return s.hasRouteAndProviderFactory()
290
+ }
291
+
292
+ func callsSpecializedByFlows(direct []Call, flows []FlowGroup) []Call {
293
+ specializedSources := map[string]bool{}
294
+ var out []Call
295
+ for _, flow := range flows {
296
+ for _, call := range flow.Calls {
297
+ specializedSources[call.Source.String()] = true
298
+ out = append(out, call)
299
+ }
300
+ }
301
+ for _, call := range direct {
302
+ if !specializedSources[call.Source.String()] {
303
+ out = append(out, call)
304
+ }
305
+ }
306
+ sort.Slice(out, func(i, j int) bool {
307
+ if out[i].Source.File != out[j].Source.File {
308
+ return out[i].Source.File < out[j].Source.File
309
+ }
310
+ if out[i].Source.Line != out[j].Source.Line {
311
+ return out[i].Source.Line < out[j].Source.Line
312
+ }
313
+ return out[i].ID < out[j].ID
314
+ })
315
+ return uniqueCalls(out)
316
+ }
317
+
318
+ func functionKey(file *parsedFile, fn *ast.FuncDecl) string {
319
+ name := fn.Name.Name
320
+ if fn.Recv != nil && len(fn.Recv.List) > 0 {
321
+ name = receiverName(fn.Recv.List[0].Type) + "." + name
322
+ }
323
+ if file.dir != "." && file.dir != "" {
324
+ name = file.dir + ":" + name
325
+ }
326
+ return name
327
+ }
328
+
329
+ func (s *scanner) indexFunctions() {
330
+ for _, file := range s.files {
331
+ if file.generated {
332
+ continue
333
+ }
334
+ for _, decl := range file.node.Decls {
335
+ fn, ok := decl.(*ast.FuncDecl)
336
+ if !ok || fn.Body == nil {
337
+ continue
338
+ }
339
+ key := functionKey(file, fn)
340
+ s.functions[key] = &functionDecl{key: key, file: file, fn: fn}
341
+ if fn.Recv != nil {
342
+ s.methods[fn.Name.Name] = append(s.methods[fn.Name.Name], key)
343
+ }
344
+ }
345
+ }
346
+ }
347
+
348
+ // indexConcreteFieldTypes records the concrete values assigned to receiver
349
+ // fields by constructors and assembly code. A field may be declared as an
350
+ // interface while its production implementation is supplied through a
351
+ // composite literal, a direct assignment, or a setter:
352
+ //
353
+ // client, err := transport.New(...)
354
+ // return &Connector{client: client}, nil
355
+ //
356
+ // connector.SetClient(client)
357
+ //
358
+ // The assignment itself is compiler-checked evidence that the concrete type
359
+ // implements the interface. Multiple observed implementations are retained so
360
+ // callers can refuse to guess when the wiring is genuinely ambiguous.
361
+ func (s *scanner) indexConcreteFieldTypes() {
362
+ for _, declaration := range s.functions {
363
+ locals := s.localConcreteTypes(declaration)
364
+ ast.Inspect(declaration.fn.Body, func(node ast.Node) bool {
365
+ switch value := node.(type) {
366
+ case *ast.CompositeLit:
367
+ s.indexCompositeFieldTypes(declaration, value, locals)
368
+ case *ast.AssignStmt:
369
+ s.indexAssignedFieldTypes(declaration, value, locals)
370
+ }
371
+ return true
372
+ })
373
+ }
374
+
375
+ setters := s.fieldSetters()
376
+ for _, declaration := range s.functions {
377
+ locals := s.localConcreteTypes(declaration)
378
+ ast.Inspect(declaration.fn.Body, func(node ast.Node) bool {
379
+ call, ok := node.(*ast.CallExpr)
380
+ if !ok {
381
+ return true
382
+ }
383
+ selector, ok := call.Fun.(*ast.SelectorExpr)
384
+ if !ok {
385
+ return true
386
+ }
387
+ root, ok := selector.X.(*ast.Ident)
388
+ if !ok {
389
+ return true
390
+ }
391
+ owner, ok := locals[root.Name]
392
+ if !ok {
393
+ return true
394
+ }
395
+ for _, setter := range setters[s.methodKey(owner, selector.Sel.Name)] {
396
+ if setter.param >= len(call.Args) {
397
+ continue
398
+ }
399
+ typ, ok := s.concreteExpressionType(declaration, call.Args[setter.param], locals)
400
+ if !ok || !s.typeHasMethods(typ) {
401
+ continue
402
+ }
403
+ key := fieldTypeKey(setter.owner, setter.field)
404
+ s.fieldTypes[key] = appendEndpointType(s.fieldTypes[key], typ)
405
+ }
406
+ return true
407
+ })
408
+ }
409
+ for key := range s.fieldTypes {
410
+ sort.Slice(s.fieldTypes[key], func(i, j int) bool {
411
+ if s.fieldTypes[key][i].dir != s.fieldTypes[key][j].dir {
412
+ return s.fieldTypes[key][i].dir < s.fieldTypes[key][j].dir
413
+ }
414
+ return s.fieldTypes[key][i].name < s.fieldTypes[key][j].name
415
+ })
416
+ }
417
+ }
418
+
419
+ func (s *scanner) indexCompositeFieldTypes(declaration *functionDecl, literal *ast.CompositeLit, locals map[string]endpointType) {
420
+ ownerType, ok := s.typeExpression(declaration.file, literal.Type)
421
+ if !ok {
422
+ return
423
+ }
424
+ for _, element := range literal.Elts {
425
+ field, ok := element.(*ast.KeyValueExpr)
426
+ if !ok {
427
+ continue
428
+ }
429
+ name, ok := field.Key.(*ast.Ident)
430
+ if !ok {
431
+ continue
432
+ }
433
+ typ, ok := s.concreteExpressionType(declaration, field.Value, locals)
434
+ if !ok || !s.typeHasMethods(typ) {
435
+ continue
436
+ }
437
+ key := fieldTypeKey(ownerType, name.Name)
438
+ s.fieldTypes[key] = appendEndpointType(s.fieldTypes[key], typ)
439
+ }
440
+ }
441
+
442
+ func (s *scanner) indexAssignedFieldTypes(declaration *functionDecl, assignment *ast.AssignStmt, locals map[string]endpointType) {
443
+ for index, left := range assignment.Lhs {
444
+ selector, ok := left.(*ast.SelectorExpr)
445
+ if !ok || index >= len(assignment.Rhs) {
446
+ continue
447
+ }
448
+ root, ok := selector.X.(*ast.Ident)
449
+ if !ok {
450
+ continue
451
+ }
452
+ owner, ok := locals[root.Name]
453
+ if !ok {
454
+ continue
455
+ }
456
+ typ, ok := s.concreteExpressionType(declaration, assignment.Rhs[index], locals)
457
+ if !ok || !s.typeHasMethods(typ) {
458
+ continue
459
+ }
460
+ key := fieldTypeKey(owner, selector.Sel.Name)
461
+ s.fieldTypes[key] = appendEndpointType(s.fieldTypes[key], typ)
462
+ }
463
+ }
464
+
465
+ // fieldSetters identifies the narrow setter shape that proves a field receives
466
+ // one of the method's parameters. The concrete implementation is intentionally
467
+ // resolved only at call sites; the interface-typed parameter itself proves no
468
+ // implementation.
469
+ func (s *scanner) fieldSetters() map[string][]fieldSetter {
470
+ out := map[string][]fieldSetter{}
471
+ for key, declaration := range s.functions {
472
+ receiver := receiverVariable(declaration.fn)
473
+ if receiver == "" {
474
+ continue
475
+ }
476
+ owner, ok := s.typeExpression(declaration.file, declaration.fn.Recv.List[0].Type)
477
+ if !ok {
478
+ continue
479
+ }
480
+ params := functionParams(declaration.fn)
481
+ paramAt := map[string]int{}
482
+ for index, name := range params {
483
+ if name != "" {
484
+ paramAt[name] = index
485
+ }
486
+ }
487
+ ast.Inspect(declaration.fn.Body, func(node ast.Node) bool {
488
+ assignment, ok := node.(*ast.AssignStmt)
489
+ if !ok {
490
+ return true
491
+ }
492
+ for index, left := range assignment.Lhs {
493
+ selector, ok := left.(*ast.SelectorExpr)
494
+ if !ok || index >= len(assignment.Rhs) {
495
+ continue
496
+ }
497
+ root, ok := selector.X.(*ast.Ident)
498
+ if !ok || root.Name != receiver {
499
+ continue
500
+ }
501
+ right, ok := assignment.Rhs[index].(*ast.Ident)
502
+ if !ok {
503
+ continue
504
+ }
505
+ param, ok := paramAt[right.Name]
506
+ if !ok {
507
+ continue
508
+ }
509
+ out[key] = append(out[key], fieldSetter{owner: owner, field: selector.Sel.Name, param: param})
510
+ }
511
+ return true
512
+ })
513
+ }
514
+ return out
515
+ }
516
+
517
+ func (s *scanner) localConcreteTypes(declaration *functionDecl) map[string]endpointType {
518
+ locals := map[string]endpointType{}
519
+ if declaration.fn.Recv != nil && len(declaration.fn.Recv.List) > 0 {
520
+ if typ, ok := s.typeExpression(declaration.file, declaration.fn.Recv.List[0].Type); ok {
521
+ for _, name := range declaration.fn.Recv.List[0].Names {
522
+ locals[name.Name] = typ
523
+ }
524
+ }
525
+ }
526
+ if declaration.fn.Type.Params != nil {
527
+ for _, field := range declaration.fn.Type.Params.List {
528
+ typ, ok := s.typeExpression(declaration.file, field.Type)
529
+ if !ok {
530
+ continue
531
+ }
532
+ for _, name := range field.Names {
533
+ locals[name.Name] = typ
534
+ }
535
+ }
536
+ }
537
+ ast.Inspect(declaration.fn.Body, func(node ast.Node) bool {
538
+ switch statement := node.(type) {
539
+ case *ast.AssignStmt:
540
+ if len(statement.Rhs) == 1 && len(statement.Lhs) > 1 {
541
+ resultTypes := s.expressionResultTypes(declaration, statement.Rhs[0], locals)
542
+ for index, left := range statement.Lhs {
543
+ name, ok := left.(*ast.Ident)
544
+ if ok && index < len(resultTypes) && s.typeHasMethods(resultTypes[index]) {
545
+ locals[name.Name] = resultTypes[index]
546
+ }
547
+ }
548
+ return true
549
+ }
550
+ for index, left := range statement.Lhs {
551
+ name, ok := left.(*ast.Ident)
552
+ if !ok || index >= len(statement.Rhs) {
553
+ continue
554
+ }
555
+ if typ, ok := s.concreteExpressionType(declaration, statement.Rhs[index], locals); ok && s.typeHasMethods(typ) {
556
+ locals[name.Name] = typ
557
+ }
558
+ }
559
+ case *ast.DeclStmt:
560
+ generic, ok := statement.Decl.(*ast.GenDecl)
561
+ if !ok {
562
+ return true
563
+ }
564
+ for _, raw := range generic.Specs {
565
+ spec, ok := raw.(*ast.ValueSpec)
566
+ if !ok || spec.Type == nil {
567
+ continue
568
+ }
569
+ typ, ok := s.typeExpression(declaration.file, spec.Type)
570
+ if !ok || !s.typeHasMethods(typ) {
571
+ continue
572
+ }
573
+ for _, name := range spec.Names {
574
+ locals[name.Name] = typ
575
+ }
576
+ }
577
+ }
578
+ return true
579
+ })
580
+ return locals
581
+ }
582
+
583
+ func (s *scanner) concreteExpressionType(declaration *functionDecl, expr ast.Expr, locals map[string]endpointType) (endpointType, bool) {
584
+ switch value := expr.(type) {
585
+ case *ast.ParenExpr:
586
+ return s.concreteExpressionType(declaration, value.X, locals)
587
+ case *ast.UnaryExpr:
588
+ return s.concreteExpressionType(declaration, value.X, locals)
589
+ case *ast.Ident:
590
+ typ, ok := locals[value.Name]
591
+ return typ, ok
592
+ case *ast.CallExpr:
593
+ results := s.expressionResultTypes(declaration, value, locals)
594
+ if len(results) > 0 {
595
+ return results[0], true
596
+ }
597
+ }
598
+ if literal := compositeLiteral(expr); literal != nil {
599
+ return s.typeExpression(declaration.file, literal.Type)
600
+ }
601
+ return endpointType{}, false
602
+ }
603
+
604
+ func (s *scanner) expressionResultTypes(declaration *functionDecl, expr ast.Expr, locals map[string]endpointType) []endpointType {
605
+ call, ok := expr.(*ast.CallExpr)
606
+ if !ok {
607
+ if typ, ok := s.concreteExpressionType(declaration, expr, locals); ok {
608
+ return []endpointType{typ}
609
+ }
610
+ return nil
611
+ }
612
+ target := s.localTarget(declaration, call.Fun)
613
+ callee := s.functions[target]
614
+ if callee == nil || callee.fn.Type.Results == nil {
615
+ return nil
616
+ }
617
+ var out []endpointType
618
+ for _, field := range callee.fn.Type.Results.List {
619
+ typ, ok := s.typeExpression(callee.file, field.Type)
620
+ if !ok {
621
+ typ = endpointType{}
622
+ }
623
+ count := len(field.Names)
624
+ if count == 0 {
625
+ count = 1
626
+ }
627
+ for range count {
628
+ out = append(out, typ)
629
+ }
630
+ }
631
+ return out
632
+ }
633
+
634
+ func (s *scanner) typeHasMethods(typ endpointType) bool {
635
+ if typ.name == "" {
636
+ return false
637
+ }
638
+ prefix := typ.name + "."
639
+ if typ.dir != "." && typ.dir != "" {
640
+ prefix = typ.dir + ":" + prefix
641
+ }
642
+ for key := range s.functions {
643
+ if strings.HasPrefix(key, prefix) {
644
+ return true
645
+ }
646
+ }
647
+ return false
648
+ }
649
+
650
+ func appendEndpointType(items []endpointType, value endpointType) []endpointType {
651
+ for _, item := range items {
652
+ if item == value {
653
+ return items
654
+ }
655
+ }
656
+ return append(items, value)
657
+ }
658
+
659
+ func fieldTypeKey(owner endpointType, field string) string {
660
+ return owner.dir + ":" + owner.name + "." + field
661
+ }
662
+
663
+ // indexFieldOrigins records where URL-shaped client fields are initialized.
664
+ // The result is deliberately symbolic: configuration is commonly loaded at
665
+ // runtime, but "Config.BaseURL" is still substantially better evidence than
666
+ // the receiver expression "c.baseURL".
667
+ func (s *scanner) indexFieldOrigins() {
668
+ for _, declaration := range s.functions {
669
+ locals := s.symbolicLocals(declaration)
670
+ ast.Inspect(declaration.fn.Body, func(node ast.Node) bool {
671
+ ret, ok := node.(*ast.ReturnStmt)
672
+ if !ok {
673
+ return true
674
+ }
675
+ for _, result := range ret.Results {
676
+ literal := compositeLiteral(result)
677
+ if literal == nil {
678
+ continue
679
+ }
680
+ typeName := receiverName(literal.Type)
681
+ for _, element := range literal.Elts {
682
+ field, ok := element.(*ast.KeyValueExpr)
683
+ if !ok {
684
+ continue
685
+ }
686
+ name, ok := field.Key.(*ast.Ident)
687
+ if !ok || !looksLikeURLName(name.Name) {
688
+ continue
689
+ }
690
+ value := s.symbolicValue(declaration, field.Value, locals, map[string]bool{})
691
+ if value == "" {
692
+ continue
693
+ }
694
+ label := typeName + "." + name.Name
695
+ s.fields[fieldKey(declaration.file, typeName, name.Name)] = fieldOrigin{value: value, chain: uniqueStrings([]string{value, displayFunction(declaration.key), label}), constructor: declaration.key}
696
+ }
697
+ }
698
+ return true
699
+ })
700
+ }
701
+ }
702
+
703
+ // resolveFieldOriginsAtCallSites replaces an intermediate constructor input
704
+ // such as Params.BaseURL with the value that populates it at the unique call
705
+ // site, for example connectionVal["url"]. Ambiguous call sites retain the
706
+ // constructor-level origin instead of choosing one arbitrarily.
707
+ func (s *scanner) resolveFieldOriginsAtCallSites() {
708
+ type candidate struct {
709
+ value string
710
+ caller string
711
+ }
712
+ byField := map[string]map[string]candidate{}
713
+ for _, caller := range s.functions {
714
+ locals := s.symbolicLocals(caller)
715
+ ast.Inspect(caller.fn.Body, func(node ast.Node) bool {
716
+ call, ok := node.(*ast.CallExpr)
717
+ if !ok {
718
+ return true
719
+ }
720
+ target := s.localTarget(caller, call.Fun)
721
+ if target == "" {
722
+ return true
723
+ }
724
+ declaration := s.functions[target]
725
+ if declaration == nil || declaration.fn.Type.Params == nil {
726
+ return true
727
+ }
728
+ paramTypes := functionParamTypes(declaration.fn)
729
+ for key, origin := range s.fields {
730
+ if origin.constructor != target {
731
+ continue
732
+ }
733
+ container, field, ok := strings.Cut(origin.value, ".")
734
+ if !ok {
735
+ continue
736
+ }
737
+ for index, typ := range paramTypes {
738
+ if typ != container || index >= len(call.Args) {
739
+ continue
740
+ }
741
+ literal := compositeLiteral(call.Args[index])
742
+ if literal == nil {
743
+ continue
744
+ }
745
+ for _, element := range literal.Elts {
746
+ pair, ok := element.(*ast.KeyValueExpr)
747
+ if !ok || expression(pair.Key) != field {
748
+ continue
749
+ }
750
+ value := s.symbolicValue(caller, pair.Value, locals, map[string]bool{})
751
+ if value == "" || value == origin.value {
752
+ continue
753
+ }
754
+ if byField[key] == nil {
755
+ byField[key] = map[string]candidate{}
756
+ }
757
+ byField[key][value] = candidate{value: value, caller: displayFunction(caller.key)}
758
+ }
759
+ }
760
+ }
761
+ return true
762
+ })
763
+ }
764
+ for key, candidates := range byField {
765
+ if len(candidates) != 1 {
766
+ continue
767
+ }
768
+ for _, candidate := range candidates {
769
+ origin := s.fields[key]
770
+ origin.value = candidate.value
771
+ origin.chain = uniqueStrings(append([]string{candidate.value, candidate.caller}, origin.chain[1:]...))
772
+ s.fields[key] = origin
773
+ }
774
+ }
775
+ }
776
+
777
+ func functionParamTypes(fn *ast.FuncDecl) []string {
778
+ var out []string
779
+ if fn.Type.Params == nil {
780
+ return out
781
+ }
782
+ for _, field := range fn.Type.Params.List {
783
+ typeName := receiverName(field.Type)
784
+ if len(field.Names) == 0 {
785
+ out = append(out, typeName)
786
+ continue
787
+ }
788
+ for range field.Names {
789
+ out = append(out, typeName)
790
+ }
791
+ }
792
+ return out
793
+ }
794
+
795
+ func fieldKey(file *parsedFile, typeName, field string) string {
796
+ return file.dir + ":" + typeName + "." + field
797
+ }
798
+
799
+ func compositeLiteral(expr ast.Expr) *ast.CompositeLit {
800
+ switch value := expr.(type) {
801
+ case *ast.CompositeLit:
802
+ return value
803
+ case *ast.UnaryExpr:
804
+ return compositeLiteral(value.X)
805
+ }
806
+ return nil
807
+ }
808
+
809
+ func looksLikeURLName(name string) bool {
810
+ name = strings.ToLower(name)
811
+ return strings.Contains(name, "url") || strings.Contains(name, "endpoint") || strings.Contains(name, "host")
812
+ }
813
+
814
+ func (s *scanner) symbolicLocals(declaration *functionDecl) map[string]string {
815
+ locals := map[string]string{}
816
+ parameterTypes := map[string]string{}
817
+ if declaration.fn.Type.Params != nil {
818
+ for _, field := range declaration.fn.Type.Params.List {
819
+ for _, name := range field.Names {
820
+ parameterTypes[name.Name] = receiverName(field.Type)
821
+ }
822
+ }
823
+ }
824
+ ast.Inspect(declaration.fn.Body, func(node ast.Node) bool {
825
+ assign, ok := node.(*ast.AssignStmt)
826
+ if !ok || len(assign.Rhs) == 0 {
827
+ return true
828
+ }
829
+ for index, left := range assign.Lhs {
830
+ id, ok := left.(*ast.Ident)
831
+ if !ok {
832
+ continue
833
+ }
834
+ rightAt := index
835
+ if len(assign.Rhs) == 1 {
836
+ rightAt = 0
837
+ }
838
+ if rightAt >= len(assign.Rhs) {
839
+ continue
840
+ }
841
+ if value := s.symbolicValueWithTypes(declaration, assign.Rhs[rightAt], locals, parameterTypes, map[string]bool{}); value != "" {
842
+ if assign.Tok == token.ADD_ASSIGN {
843
+ value = locals[id.Name] + value
844
+ }
845
+ locals[id.Name] = value
846
+ }
847
+ }
848
+ return true
849
+ })
850
+ return locals
851
+ }
852
+
853
+ func (s *scanner) symbolicValue(declaration *functionDecl, expr ast.Expr, locals map[string]string, seen map[string]bool) string {
854
+ parameterTypes := map[string]string{}
855
+ if declaration.fn.Type.Params != nil {
856
+ for _, field := range declaration.fn.Type.Params.List {
857
+ for _, name := range field.Names {
858
+ parameterTypes[name.Name] = receiverName(field.Type)
859
+ }
860
+ }
861
+ }
862
+ return s.symbolicValueWithTypes(declaration, expr, locals, parameterTypes, seen)
863
+ }
864
+
865
+ func (s *scanner) symbolicValueWithTypes(declaration *functionDecl, expr ast.Expr, locals, parameterTypes map[string]string, seen map[string]bool) string {
866
+ switch value := expr.(type) {
867
+ case *ast.ParenExpr:
868
+ return s.symbolicValueWithTypes(declaration, value.X, locals, parameterTypes, seen)
869
+ case *ast.UnaryExpr:
870
+ return s.symbolicValueWithTypes(declaration, value.X, locals, parameterTypes, seen)
871
+ case *ast.StarExpr:
872
+ return s.symbolicValueWithTypes(declaration, value.X, locals, parameterTypes, seen)
873
+ case *ast.TypeAssertExpr:
874
+ return s.symbolicValueWithTypes(declaration, value.X, locals, parameterTypes, seen)
875
+ case *ast.BasicLit:
876
+ if value.Kind == token.STRING {
877
+ literal, _ := strconv.Unquote(value.Value)
878
+ return literal
879
+ }
880
+ case *ast.Ident:
881
+ if local := locals[value.Name]; local != "" {
882
+ return local
883
+ }
884
+ return value.Name
885
+ case *ast.IndexExpr:
886
+ return expression(value)
887
+ case *ast.SelectorExpr:
888
+ if owner, ok := value.X.(*ast.Ident); ok {
889
+ if local := locals[owner.Name+"."+value.Sel.Name]; local != "" {
890
+ return local
891
+ }
892
+ if typ := parameterTypes[owner.Name]; typ != "" {
893
+ return typ + "." + value.Sel.Name
894
+ }
895
+ }
896
+ return expression(value)
897
+ case *ast.BinaryExpr:
898
+ if value.Op == token.ADD {
899
+ return s.symbolicValueWithTypes(declaration, value.X, locals, parameterTypes, seen) + s.symbolicValueWithTypes(declaration, value.Y, locals, parameterTypes, seen)
900
+ }
901
+ case *ast.CallExpr:
902
+ name := selectorName(value.Fun)
903
+ if name == "String" {
904
+ if selector, ok := value.Fun.(*ast.SelectorExpr); ok {
905
+ return s.symbolicValueWithTypes(declaration, selector.X, locals, parameterTypes, seen)
906
+ }
907
+ }
908
+ if (name == "Parse" || strings.HasPrefix(name, "Trim")) && len(value.Args) > 0 {
909
+ return s.symbolicValueWithTypes(declaration, value.Args[0], locals, parameterTypes, seen)
910
+ }
911
+ }
912
+ return ""
913
+ }
914
+
915
+ // indexSOAPWrappers learns the public signature of local SOAP adapters from
916
+ // their implementation. It does not assume that action/request/response occupy
917
+ // fixed argument positions: generated and hand-written clients differ there.
918
+ // A wrapper is accepted only when its body proves SOAP through a gowsdl call or
919
+ // a SOAPAction header.
920
+ func (s *scanner) indexSOAPWrappers() {
921
+ for {
922
+ progress := false
923
+ for _, declaration := range s.functions {
924
+ if s.soapFns[declaration.key] {
925
+ continue
926
+ }
927
+ params := functionParams(declaration.fn)
928
+ if len(params) == 0 {
929
+ continue
930
+ }
931
+ paramAt := map[string]int{}
932
+ for index, name := range params {
933
+ if name != "" {
934
+ paramAt[name] = index
935
+ }
936
+ }
937
+ wrapper := soapWrapper{
938
+ function: declaration.key, name: declaration.fn.Name.Name, arity: len(params), variadic: functionVariadic(declaration.fn),
939
+ request: -1, response: -1,
940
+ }
941
+ proved := false
942
+ ast.Inspect(declaration.fn.Body, func(n ast.Node) bool {
943
+ call, ok := n.(*ast.CallExpr)
944
+ if !ok {
945
+ return true
946
+ }
947
+ name := selectorName(call.Fun)
948
+ if (name == "Call" || name == "CallContext") && importsSOAP(declaration.file) {
949
+ actionAt := 0
950
+ if name == "CallContext" {
951
+ actionAt = 1
952
+ }
953
+ if len(call.Args) > actionAt+2 {
954
+ if parts, ok := templateOf(call.Args[actionAt], declaration.file, paramAt, s); ok {
955
+ wrapper.action = parts
956
+ }
957
+ wrapper.request = parameterOf(call.Args[actionAt+1], paramAt)
958
+ wrapper.response = parameterOf(call.Args[actionAt+2], paramAt)
959
+ wrapper.version = "1.1"
960
+ proved = true
961
+ }
962
+ }
963
+ if inner := s.oneSOAPWrapper(name, len(call.Args)); inner != nil {
964
+ if parts := composeTemplate(inner.action, call.Args, declaration.file, paramAt, s); len(parts) > 0 {
965
+ wrapper.action = parts
966
+ wrapper.contentTypeAction = inner.contentTypeAction
967
+ wrapper.version = inner.version
968
+ proved = true
969
+ }
970
+ wrapper.request = composedParameter(inner.request, call.Args, paramAt)
971
+ wrapper.response = composedParameter(inner.response, call.Args, paramAt)
972
+ wrapper.endpoint = composeTemplate(inner.endpoint, call.Args, declaration.file, paramAt, s)
973
+ }
974
+ if (name == "Add" || name == "Set") && len(call.Args) >= 2 {
975
+ header := strings.ToLower(strings.Trim(s.value(declaration.file, call.Args[0], nil, map[string]bool{}), `"`))
976
+ if header == "soapaction" {
977
+ if parts, ok := templateOf(call.Args[1], declaration.file, paramAt, s); ok {
978
+ wrapper.action = parts
979
+ wrapper.version = "1.1"
980
+ proved = true
981
+ }
982
+ }
983
+ if header == "content-type" && strings.Contains(strings.ToLower(expression(call.Args[1])), "application/soap+xml") && strings.Contains(strings.ToLower(expression(call.Args[1])), "action") {
984
+ if parts, ok := templateOf(call.Args[1], declaration.file, paramAt, s); ok {
985
+ wrapper.action = parts
986
+ wrapper.contentTypeAction = true
987
+ wrapper.version = "1.2"
988
+ proved = true
989
+ }
990
+ }
991
+ }
992
+ if (name == "NewRequest" || name == "NewRequestWithContext") && s.netHTTPCall(declaration.file, call.Fun) {
993
+ urlAt := 1
994
+ if name == "NewRequestWithContext" {
995
+ urlAt = 2
996
+ }
997
+ if len(call.Args) > urlAt {
998
+ wrapper.endpoint, _ = templateOf(call.Args[urlAt], declaration.file, paramAt, s)
999
+ }
1000
+ }
1001
+ return true
1002
+ })
1003
+ if !proved || len(wrapper.action) == 0 || !templateHasParam(wrapper.action) {
1004
+ continue
1005
+ }
1006
+ if wrapper.request < 0 || wrapper.response < 0 {
1007
+ wrapper.request, wrapper.response = namedPayloadParams(params)
1008
+ }
1009
+ s.soap[wrapper.name] = append(s.soap[wrapper.name], wrapper)
1010
+ s.soapFns[wrapper.function] = true
1011
+ progress = true
1012
+ }
1013
+ if !progress {
1014
+ return
1015
+ }
1016
+ }
1017
+ }
1018
+
1019
+ func functionVariadic(fn *ast.FuncDecl) bool {
1020
+ if fn.Type.Params == nil || len(fn.Type.Params.List) == 0 {
1021
+ return false
1022
+ }
1023
+ _, ok := fn.Type.Params.List[len(fn.Type.Params.List)-1].Type.(*ast.Ellipsis)
1024
+ return ok
1025
+ }
1026
+
1027
+ func (s *scanner) matchingSOAPWrappers(name string, arity int) []soapWrapper {
1028
+ var out []soapWrapper
1029
+ for _, wrapper := range s.soap[name] {
1030
+ minimum := wrapper.arity
1031
+ if wrapper.variadic {
1032
+ minimum--
1033
+ }
1034
+ if (!wrapper.variadic && arity == wrapper.arity) || (wrapper.variadic && arity >= minimum) {
1035
+ out = append(out, wrapper)
1036
+ }
1037
+ }
1038
+ return out
1039
+ }
1040
+
1041
+ func (s *scanner) oneSOAPWrapper(name string, arity int) *soapWrapper {
1042
+ matches := s.matchingSOAPWrappers(name, arity)
1043
+ if len(matches) != 1 {
1044
+ return nil
1045
+ }
1046
+ return &matches[0]
1047
+ }
1048
+
1049
+ func composeTemplate(parts []valuePart, args []ast.Expr, file *parsedFile, params map[string]int, s *scanner) []valuePart {
1050
+ var out []valuePart
1051
+ for _, part := range parts {
1052
+ if !part.isParam {
1053
+ out = append(out, part)
1054
+ continue
1055
+ }
1056
+ if part.param < 0 || part.param >= len(args) {
1057
+ continue
1058
+ }
1059
+ resolved, ok := templateOf(args[part.param], file, params, s)
1060
+ if ok {
1061
+ out = append(out, resolved...)
1062
+ }
1063
+ }
1064
+ return out
1065
+ }
1066
+
1067
+ func composedParameter(index int, args []ast.Expr, params map[string]int) int {
1068
+ if index < 0 || index >= len(args) {
1069
+ return -1
1070
+ }
1071
+ return parameterOf(args[index], params)
1072
+ }
1073
+
1074
+ func templateHasParam(parts []valuePart) bool {
1075
+ for _, part := range parts {
1076
+ if part.isParam {
1077
+ return true
1078
+ }
1079
+ }
1080
+ return false
1081
+ }
1082
+
1083
+ func functionParams(fn *ast.FuncDecl) []string {
1084
+ var out []string
1085
+ if fn.Type.Params == nil {
1086
+ return out
1087
+ }
1088
+ for _, field := range fn.Type.Params.List {
1089
+ if len(field.Names) == 0 {
1090
+ out = append(out, "")
1091
+ continue
1092
+ }
1093
+ for _, name := range field.Names {
1094
+ out = append(out, name.Name)
1095
+ }
1096
+ }
1097
+ return out
1098
+ }
1099
+
1100
+ func namedPayloadParams(params []string) (request, response int) {
1101
+ request, response = -1, -1
1102
+ for index, name := range params {
1103
+ lower := strings.ToLower(name)
1104
+ if request < 0 && (strings.Contains(lower, "request") || strings.Contains(lower, "query") || strings.Contains(lower, "payload")) {
1105
+ request = index
1106
+ }
1107
+ if response < 0 && (strings.Contains(lower, "response") || strings.Contains(lower, "reply") || strings.Contains(lower, "result")) {
1108
+ response = index
1109
+ }
1110
+ }
1111
+ return request, response
1112
+ }
1113
+
1114
+ func importsSOAP(file *parsedFile) bool {
1115
+ for _, imported := range file.imports {
1116
+ if strings.Contains(strings.ToLower(imported), "soap") {
1117
+ return true
1118
+ }
1119
+ }
1120
+ return false
1121
+ }
1122
+
1123
+ func parameterOf(expr ast.Expr, params map[string]int) int {
1124
+ id, ok := expr.(*ast.Ident)
1125
+ if !ok {
1126
+ return -1
1127
+ }
1128
+ if index, ok := params[id.Name]; ok {
1129
+ return index
1130
+ }
1131
+ return -1
1132
+ }
1133
+
1134
+ func templateOf(expr ast.Expr, file *parsedFile, params map[string]int, s *scanner) ([]valuePart, bool) {
1135
+ switch value := expr.(type) {
1136
+ case *ast.ParenExpr:
1137
+ return templateOf(value.X, file, params, s)
1138
+ case *ast.BinaryExpr:
1139
+ if value.Op != token.ADD {
1140
+ return nil, false
1141
+ }
1142
+ left, leftOK := templateOf(value.X, file, params, s)
1143
+ right, rightOK := templateOf(value.Y, file, params, s)
1144
+ return append(left, right...), leftOK && rightOK
1145
+ case *ast.BasicLit:
1146
+ if value.Kind == token.STRING {
1147
+ literal, err := strconv.Unquote(value.Value)
1148
+ return []valuePart{{literal: literal}}, err == nil
1149
+ }
1150
+ case *ast.Ident:
1151
+ if index, ok := params[value.Name]; ok {
1152
+ return []valuePart{{param: index, isParam: true}}, true
1153
+ }
1154
+ if literal := s.value(file, value, nil, map[string]bool{}); literal != "" {
1155
+ return []valuePart{{literal: literal}}, true
1156
+ }
1157
+ }
1158
+ return nil, false
1159
+ }
1160
+
1161
+ func (s *scanner) readWSDLContracts() {
1162
+ result, err := wsdl.Discover(s.root)
1163
+ if err != nil {
1164
+ s.warnings = append(s.warnings, "WSDL discovery: "+err.Error())
1165
+ return
1166
+ }
1167
+ s.warnings = append(s.warnings, result.Warnings...)
1168
+ apiIDs := wsdl.APIIDs(result.Contracts)
1169
+ for _, document := range result.Contracts {
1170
+ api := apiIDs[wsdl.ContractKey(document)]
1171
+ external := wsdl.ExternalID(document)
1172
+ for _, iface := range document.Interfaces {
1173
+ operations := make([]SOAPOperation, 0, len(iface.Operations))
1174
+ for _, operation := range iface.Operations {
1175
+ operations = append(operations, SOAPOperation{
1176
+ ID: operation.Name, Interface: wsdl.InterfaceID(api, iface), Action: operation.Action,
1177
+ Request: operation.Request, Response: operation.Response,
1178
+ Version: iface.Version, Style: iface.Style, Binding: iface.Binding, Endpoint: iface.Endpoint,
1179
+ Faults: append([]string(nil), operation.Faults...), Headers: append([]string(nil), operation.Headers...),
1180
+ })
1181
+ }
1182
+ if len(operations) == 0 {
1183
+ continue
1184
+ }
1185
+ s.contracts = append(s.contracts, Contract{
1186
+ Protocol: "SOAP", API: api, External: external, Name: document.Name,
1187
+ Summary: document.Summary, URL: iface.Endpoint, Source: document.Source, SOAP: operations,
1188
+ })
1189
+ }
1190
+ }
1191
+ }
1192
+
1193
+ func (s *scanner) read() error {
1194
+ var names []string
1195
+ err := filepath.WalkDir(s.root, func(name string, entry fs.DirEntry, err error) error {
1196
+ if err != nil {
1197
+ return err
1198
+ }
1199
+ if entry.IsDir() {
1200
+ if name != s.root && (strings.HasPrefix(entry.Name(), ".") || entry.Name() == "vendor" || entry.Name() == "node_modules") {
1201
+ return filepath.SkipDir
1202
+ }
1203
+ return nil
1204
+ }
1205
+ if !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") {
1206
+ return nil
1207
+ }
1208
+ names = append(names, name)
1209
+ return nil
1210
+ })
1211
+ if err != nil {
1212
+ return err
1213
+ }
1214
+ sort.Strings(names)
1215
+ for _, name := range names {
1216
+ node, err := parser.ParseFile(s.fset, name, nil, parser.ParseComments)
1217
+ if err != nil {
1218
+ return fmt.Errorf("parse %s: %w", name, err)
1219
+ }
1220
+ rel, _ := filepath.Rel(s.root, name)
1221
+ file := &parsedFile{
1222
+ abs: name,
1223
+ rel: filepath.ToSlash(rel),
1224
+ dir: filepath.ToSlash(filepath.Dir(rel)),
1225
+ pkg: node.Name.Name,
1226
+ imports: importsOf(node),
1227
+ node: node,
1228
+ generated: generatedFile(name, node),
1229
+ }
1230
+ s.files = append(s.files, file)
1231
+ }
1232
+ return nil
1233
+ }
1234
+
1235
+ func generatedFile(name string, node *ast.File) bool {
1236
+ base := filepath.Base(name)
1237
+ if strings.HasSuffix(base, ".gen.go") || strings.HasSuffix(base, ".generated.go") || strings.HasSuffix(base, "_generated.go") {
1238
+ return true
1239
+ }
1240
+ for _, group := range node.Comments {
1241
+ if strings.Contains(group.Text(), "Code generated") && strings.Contains(group.Text(), "DO NOT EDIT") {
1242
+ return true
1243
+ }
1244
+ }
1245
+ return false
1246
+ }
1247
+
1248
+ func importsOf(node *ast.File) map[string]string {
1249
+ out := map[string]string{}
1250
+ for _, spec := range node.Imports {
1251
+ path, err := strconv.Unquote(spec.Path.Value)
1252
+ if err != nil {
1253
+ continue
1254
+ }
1255
+ name := filepath.Base(path)
1256
+ if spec.Name != nil {
1257
+ name = spec.Name.Name
1258
+ }
1259
+ out[name] = path
1260
+ }
1261
+ return out
1262
+ }
1263
+
1264
+ func (s *scanner) indexConstants() {
1265
+ for _, file := range s.files {
1266
+ for _, decl := range file.node.Decls {
1267
+ gen, ok := decl.(*ast.GenDecl)
1268
+ if !ok || gen.Tok != token.CONST {
1269
+ continue
1270
+ }
1271
+ for _, item := range gen.Specs {
1272
+ value, ok := item.(*ast.ValueSpec)
1273
+ if !ok {
1274
+ continue
1275
+ }
1276
+ for i, name := range value.Names {
1277
+ if i >= len(value.Values) {
1278
+ continue
1279
+ }
1280
+ v := constValue{expr: value.Values[i], file: file}
1281
+ s.constants[file.dir+"."+name.Name] = v
1282
+ s.constants[file.pkg+"."+name.Name] = v
1283
+ }
1284
+ }
1285
+ }
1286
+ }
1287
+ }
1288
+
1289
+ func (s *scanner) readContracts() {
1290
+ byDir := map[string][]*parsedFile{}
1291
+ for _, file := range s.files {
1292
+ byDir[file.dir] = append(byDir[file.dir], file)
1293
+ }
1294
+ for dir, files := range byDir {
1295
+ if !looksLikeOAPIGeneratedClient(files) {
1296
+ continue
1297
+ }
1298
+ for _, base := range []string{"openapi.yaml", "openapi.yml", "openapi.json", "swagger.yaml", "swagger.yml", "swagger.json"} {
1299
+ name := filepath.Join(s.root, filepath.FromSlash(dir), base)
1300
+ if _, err := os.Stat(name); err != nil {
1301
+ continue
1302
+ }
1303
+ spec, err := openapi.Read(name)
1304
+ if err != nil {
1305
+ s.warnings = append(s.warnings, err.Error())
1306
+ break
1307
+ }
1308
+ rel, _ := filepath.Rel(s.root, name)
1309
+ contract := Contract{
1310
+ Protocol: "HTTP",
1311
+ API: spec.API, External: openapi.ExternalID(spec.Title), Name: spec.Title,
1312
+ Summary: spec.Description, URL: spec.DocsURL, Source: filepath.ToSlash(rel),
1313
+ Operations: spec.Operations, MethodCalls: map[string]openapi.Operation{},
1314
+ }
1315
+ matched := 0
1316
+ firstBuilder := ""
1317
+ for _, file := range files {
1318
+ for _, decl := range file.node.Decls {
1319
+ fn, ok := decl.(*ast.FuncDecl)
1320
+ if !ok || fn.Recv != nil {
1321
+ continue
1322
+ }
1323
+ opName := requestBuilderOp(fn.Name.Name)
1324
+ if opName == "" {
1325
+ continue
1326
+ }
1327
+ method, path := routeOf(fn)
1328
+ if firstBuilder == "" && (method != "" || path != "") {
1329
+ firstBuilder = method + " " + path
1330
+ }
1331
+ op, ok := spec.Find(method, path)
1332
+ if !ok {
1333
+ continue
1334
+ }
1335
+ matched++
1336
+ for _, variant := range []string{opName, opName + "WithBody", opName + "WithResponse", opName + "WithBodyWithResponse"} {
1337
+ contract.MethodCalls[variant] = op
1338
+ }
1339
+ }
1340
+ }
1341
+ if matched == 0 {
1342
+ sample := ""
1343
+ if len(spec.Operations) > 0 {
1344
+ sample = "; for example client `" + firstBuilder + "`, document `" + spec.Operations[0].Verb + " " + spec.Operations[0].Path + "`"
1345
+ }
1346
+ s.warnings = append(s.warnings, "the generated client in "+dir+" could not be matched to any route in "+contract.Source+sample)
1347
+ }
1348
+ s.contracts = append(s.contracts, contract)
1349
+ break
1350
+ }
1351
+ }
1352
+ sort.Slice(s.contracts, func(i, j int) bool { return s.contracts[i].API < s.contracts[j].API })
1353
+ }
1354
+
1355
+ func looksLikeOAPIGeneratedClient(files []*parsedFile) bool {
1356
+ var iface, builder bool
1357
+ for _, file := range files {
1358
+ for _, decl := range file.node.Decls {
1359
+ switch d := decl.(type) {
1360
+ case *ast.GenDecl:
1361
+ for _, spec := range d.Specs {
1362
+ if ts, ok := spec.(*ast.TypeSpec); ok && ts.Name.Name == "ClientInterface" {
1363
+ _, iface = ts.Type.(*ast.InterfaceType)
1364
+ }
1365
+ }
1366
+ case *ast.FuncDecl:
1367
+ builder = builder || requestBuilderOp(d.Name.Name) != ""
1368
+ }
1369
+ }
1370
+ }
1371
+ return iface && builder
1372
+ }
1373
+
1374
+ func requestBuilderOp(name string) string {
1375
+ rest, ok := strings.CutPrefix(name, "New")
1376
+ if !ok {
1377
+ return ""
1378
+ }
1379
+ if op, ok := strings.CutSuffix(rest, "RequestWithBody"); ok {
1380
+ return op
1381
+ }
1382
+ if op, ok := strings.CutSuffix(rest, "Request"); ok {
1383
+ return op
1384
+ }
1385
+ return ""
1386
+ }
1387
+
1388
+ func routeOf(fn *ast.FuncDecl) (method, path string) {
1389
+ ast.Inspect(fn.Body, func(node ast.Node) bool {
1390
+ switch n := node.(type) {
1391
+ case *ast.AssignStmt:
1392
+ if len(n.Lhs) == 1 && len(n.Rhs) == 1 {
1393
+ if id, ok := n.Lhs[0].(*ast.Ident); ok && (id.Name == "operationPath" || id.Name == "path") {
1394
+ if candidate := firstPathLiteral(n.Rhs[0]); candidate != "" {
1395
+ path = candidate
1396
+ }
1397
+ }
1398
+ }
1399
+ case *ast.CallExpr:
1400
+ if selectorName(n.Fun) == "NewRequest" || selectorName(n.Fun) == "NewRequestWithContext" {
1401
+ at := 0
1402
+ if selectorName(n.Fun) == "NewRequestWithContext" {
1403
+ at = 1
1404
+ }
1405
+ if len(n.Args) > at {
1406
+ method = httpMethod(n.Args[at])
1407
+ }
1408
+ }
1409
+ }
1410
+ return true
1411
+ })
1412
+ return method, path
1413
+ }
1414
+
1415
+ func (s *scanner) localStrings(file *parsedFile, fn *ast.FuncDecl) map[string]string {
1416
+ return s.localStringsBound(file, fn, nil)
1417
+ }
1418
+
1419
+ func (s *scanner) localStringsBound(file *parsedFile, fn *ast.FuncDecl, bindings map[string]string) map[string]string {
1420
+ out := map[string]string{}
1421
+ for name, value := range bindings {
1422
+ out[name] = value
1423
+ }
1424
+ if fn.Recv != nil && len(fn.Recv.List) > 0 && len(fn.Recv.List[0].Names) > 0 {
1425
+ receiver := fn.Recv.List[0].Names[0].Name
1426
+ typeName := receiverName(fn.Recv.List[0].Type)
1427
+ prefix := file.dir + ":" + typeName + "."
1428
+ for field, origin := range s.fields {
1429
+ if strings.HasPrefix(field, prefix) {
1430
+ out[receiver+"."+strings.TrimPrefix(field, prefix)] = origin.value
1431
+ }
1432
+ }
1433
+ }
1434
+ ast.Inspect(fn.Body, func(node ast.Node) bool {
1435
+ assign, ok := node.(*ast.AssignStmt)
1436
+ if !ok || len(assign.Rhs) == 0 {
1437
+ return true
1438
+ }
1439
+ for i, left := range assign.Lhs {
1440
+ name := expression(left)
1441
+ if name == "" || name == "_" {
1442
+ continue
1443
+ }
1444
+ rightAt := i
1445
+ if len(assign.Rhs) == 1 {
1446
+ rightAt = 0
1447
+ }
1448
+ if rightAt >= len(assign.Rhs) {
1449
+ continue
1450
+ }
1451
+ if value := s.value(file, assign.Rhs[rightAt], out, map[string]bool{}); value != "" {
1452
+ if assign.Tok == token.ADD_ASSIGN {
1453
+ value = out[name] + value
1454
+ }
1455
+ out[name] = value
1456
+ }
1457
+ }
1458
+ return true
1459
+ })
1460
+ return out
1461
+ }
1462
+
1463
+ func (s *scanner) walkStatements(file *parsedFile, function string, statements []ast.Stmt, conditions []string, locals map[string]string, out *[]Call) {
1464
+ for _, statement := range statements {
1465
+ switch item := statement.(type) {
1466
+ case *ast.IfStmt:
1467
+ if item.Init != nil {
1468
+ s.scanNode(file, function, item.Init, conditions, locals, out)
1469
+ }
1470
+ condition := expression(item.Cond)
1471
+ s.walkStatements(file, function, item.Body.List, appendCopy(conditions, condition), locals, out)
1472
+ if item.Else != nil {
1473
+ opposite := appendCopy(conditions, "not ("+condition+")")
1474
+ switch branch := item.Else.(type) {
1475
+ case *ast.BlockStmt:
1476
+ s.walkStatements(file, function, branch.List, opposite, locals, out)
1477
+ case *ast.IfStmt:
1478
+ s.walkStatements(file, function, []ast.Stmt{branch}, opposite, locals, out)
1479
+ }
1480
+ } else if blockTerminates(item.Body.List) {
1481
+ // An early-return guard proves that every statement following it
1482
+ // runs on the opposite path.
1483
+ conditions = appendCopy(conditions, "not ("+condition+")")
1484
+ }
1485
+ case *ast.ForStmt:
1486
+ title := "loop"
1487
+ if item.Cond != nil {
1488
+ title = expression(item.Cond)
1489
+ }
1490
+ s.walkStatements(file, function, item.Body.List, appendCopy(conditions, title), locals, out)
1491
+ case *ast.RangeStmt:
1492
+ s.walkStatements(file, function, item.Body.List, appendCopy(conditions, "for "+expression(item.X)), locals, out)
1493
+ default:
1494
+ s.scanNode(file, function, statement, conditions, locals, out)
1495
+ }
1496
+ }
1497
+ }
1498
+
1499
+ func blockTerminates(statements []ast.Stmt) bool {
1500
+ if len(statements) == 0 {
1501
+ return false
1502
+ }
1503
+ switch last := statements[len(statements)-1].(type) {
1504
+ case *ast.ReturnStmt, *ast.BranchStmt:
1505
+ return true
1506
+ case *ast.BlockStmt:
1507
+ return blockTerminates(last.List)
1508
+ }
1509
+ return false
1510
+ }
1511
+
1512
+ func (s *scanner) scanNode(file *parsedFile, function string, node ast.Node, conditions []string, locals map[string]string, out *[]Call) {
1513
+ ast.Inspect(node, func(n ast.Node) bool {
1514
+ call, ok := n.(*ast.CallExpr)
1515
+ if !ok {
1516
+ return true
1517
+ }
1518
+ if found, ok := s.call(file, function, call, conditions, locals); ok {
1519
+ *out = append(*out, found)
1520
+ }
1521
+ return true
1522
+ })
1523
+ }
1524
+
1525
+ func (s *scanner) call(file *parsedFile, function string, call *ast.CallExpr, conditions []string, locals map[string]string) (Call, bool) {
1526
+ name := selectorName(call.Fun)
1527
+ for _, contract := range s.contracts {
1528
+ if op, ok := contract.MethodCalls[name]; ok {
1529
+ return Call{
1530
+ Function: function, Source: s.source(file, call.Pos()), Protocol: "HTTP",
1531
+ Method: op.Verb, Path: op.Path, API: contract.API, ID: op.CallID(contract.API),
1532
+ External: contract.External, Contract: contract.Source, Conditions: append([]string(nil), conditions...),
1533
+ }, true
1534
+ }
1535
+ }
1536
+
1537
+ if wrappers := s.matchingSOAPWrappers(name, len(call.Args)); len(wrappers) > 0 {
1538
+ if s.soapFns[function] {
1539
+ return Call{}, false
1540
+ }
1541
+ if found, ok := s.wrapperCall(file, function, call, conditions, locals, wrappers); ok {
1542
+ return found, true
1543
+ }
1544
+ // The implementation proved that this is a SOAP wrapper, but more than
1545
+ // one local receiver exposes the same Go method shape and syntax alone
1546
+ // cannot choose one. Dropping it is more honest than applying the fixed
1547
+ // positions of a transport it may not be calling.
1548
+ return Call{}, false
1549
+ }
1550
+
1551
+ if (name == "NewRequest" || name == "NewRequestWithContext") && s.netHTTPCall(file, call.Fun) {
1552
+ if s.soapFns[function] || s.soapTransportFile(file) {
1553
+ return Call{}, false
1554
+ }
1555
+ methodAt, urlAt := 0, 1
1556
+ if name == "NewRequestWithContext" {
1557
+ methodAt, urlAt = 1, 2
1558
+ }
1559
+ if len(call.Args) <= urlAt {
1560
+ return Call{}, false
1561
+ }
1562
+ method := httpMethod(call.Args[methodAt])
1563
+ if method == "" {
1564
+ method = "HTTP"
1565
+ }
1566
+ endpoint := s.value(file, call.Args[urlAt], locals, map[string]bool{})
1567
+ path := pathOf(call.Args[urlAt], endpoint)
1568
+ return Call{
1569
+ Function: function, Source: s.source(file, call.Pos()), Protocol: "HTTP", Method: method,
1570
+ Path: path, Endpoint: endpoint, ID: rawCallID(method, path), Conditions: append([]string(nil), conditions...),
1571
+ URLTrace: s.urlTrace(function), template: &callTemplate{method: call.Args[methodAt], endpoint: call.Args[urlAt]},
1572
+ }, true
1573
+ }
1574
+
1575
+ if (name == "Get" || name == "Post" || name == "PostForm" || name == "Head") && s.netHTTPCall(file, call.Fun) {
1576
+ if s.soapFns[function] {
1577
+ return Call{}, false
1578
+ }
1579
+ if len(call.Args) == 0 {
1580
+ return Call{}, false
1581
+ }
1582
+ endpoint := s.value(file, call.Args[0], locals, map[string]bool{})
1583
+ path := pathOf(call.Args[0], endpoint)
1584
+ return Call{Function: function, Source: s.source(file, call.Pos()), Protocol: "HTTP", Method: strings.ToUpper(strings.TrimSuffix(name, "Form")), Path: path, Endpoint: endpoint, ID: rawCallID(strings.ToUpper(strings.TrimSuffix(name, "Form")), path), Conditions: append([]string(nil), conditions...), URLTrace: s.urlTrace(function), template: &callTemplate{endpoint: call.Args[0]}}, true
1585
+ }
1586
+
1587
+ if (name == "Call" || name == "CallContext") && s.looksLikeSOAP(file, call) {
1588
+ actionAt := 0
1589
+ if name == "CallContext" {
1590
+ actionAt = 1
1591
+ }
1592
+ if len(call.Args) <= actionAt+2 {
1593
+ return Call{}, false
1594
+ }
1595
+ if s.soapFns[function] {
1596
+ return Call{}, false
1597
+ }
1598
+ action := s.value(file, call.Args[actionAt], locals, map[string]bool{})
1599
+ if action == "" {
1600
+ action = expression(call.Args[actionAt])
1601
+ }
1602
+ request, response := expression(call.Args[actionAt+1]), expression(call.Args[actionAt+2])
1603
+ idName := action
1604
+ if idName == "" {
1605
+ idName = name
1606
+ }
1607
+ found := Call{Function: function, Source: s.source(file, call.Pos()), Protocol: "SOAP", Action: action, SOAPVersion: "1.1", Request: request, Response: response, ID: "soap/" + idName, Conditions: append([]string(nil), conditions...)}
1608
+ return s.bindSOAP(found), true
1609
+ }
1610
+ return Call{}, false
1611
+ }
1612
+
1613
+ func (s *scanner) urlTrace(function string) []string {
1614
+ declaration := s.functions[function]
1615
+ if declaration == nil || declaration.fn.Recv == nil || len(declaration.fn.Recv.List) == 0 {
1616
+ return nil
1617
+ }
1618
+ typeName := receiverName(declaration.fn.Recv.List[0].Type)
1619
+ var trace []string
1620
+ ast.Inspect(declaration.fn.Body, func(node ast.Node) bool {
1621
+ selector, ok := node.(*ast.SelectorExpr)
1622
+ if !ok {
1623
+ return true
1624
+ }
1625
+ origin, ok := s.fields[fieldKey(declaration.file, typeName, selector.Sel.Name)]
1626
+ if !ok {
1627
+ return true
1628
+ }
1629
+ trace = append(trace, origin.chain...)
1630
+ return false
1631
+ })
1632
+ if len(trace) == 0 {
1633
+ return nil
1634
+ }
1635
+ return uniqueStrings(append(trace, "request URL"))
1636
+ }
1637
+
1638
+ func (s *scanner) wrapperCall(file *parsedFile, function string, call *ast.CallExpr, conditions []string, locals map[string]string, wrappers []soapWrapper) (Call, bool) {
1639
+ type candidate struct {
1640
+ call Call
1641
+ score int
1642
+ }
1643
+ var candidates []candidate
1644
+ for _, wrapper := range wrappers {
1645
+ resolvedAction := s.templateValue(file, call, wrapper.action, locals)
1646
+ if wrapper.contentTypeAction {
1647
+ resolvedAction = soap12Action(resolvedAction)
1648
+ }
1649
+ action := resolvedAction
1650
+ if action == "" {
1651
+ action = expressionAt(call.Args, wrapper.action)
1652
+ }
1653
+ found := Call{
1654
+ Function: function, Source: s.source(file, call.Pos()), Protocol: "SOAP",
1655
+ Action: action, SOAPVersion: wrapper.version, ID: "soap/" + firstString(action, wrapper.name),
1656
+ Request: expressionArg(call.Args, wrapper.request), Response: expressionArg(call.Args, wrapper.response),
1657
+ Endpoint: s.templateValue(file, call, wrapper.endpoint, locals), Conditions: append([]string(nil), conditions...),
1658
+ }
1659
+ score := 0
1660
+ if resolvedAction != "" {
1661
+ score += 4
1662
+ }
1663
+ if _, _, matched := s.soapContract(action, wrapper.version); matched {
1664
+ score += 6
1665
+ }
1666
+ if plausiblePayload(found.Request) {
1667
+ score++
1668
+ }
1669
+ if plausiblePayload(found.Response) {
1670
+ score++
1671
+ }
1672
+ candidates = append(candidates, candidate{call: s.bindSOAP(found), score: score})
1673
+ }
1674
+ sort.SliceStable(candidates, func(i, j int) bool { return candidates[i].score > candidates[j].score })
1675
+ if len(candidates) == 0 || (len(candidates) > 1 && candidates[0].score == candidates[1].score) {
1676
+ return Call{}, false
1677
+ }
1678
+ return candidates[0].call, true
1679
+ }
1680
+
1681
+ func plausiblePayload(expression string) bool {
1682
+ lower := strings.ToLower(strings.TrimSpace(expression))
1683
+ if lower == "" || strings.Contains(lower, "client") || strings.Contains(lower, "context") || lower == "ctx" || strings.Contains(lower, "messageid") || strings.Contains(lower, "stage") {
1684
+ return false
1685
+ }
1686
+ return !strings.HasPrefix(lower, `"`) && !strings.HasPrefix(lower, "`")
1687
+ }
1688
+
1689
+ func soap12Action(contentType string) string {
1690
+ _, params, err := mime.ParseMediaType(contentType)
1691
+ if err != nil {
1692
+ return ""
1693
+ }
1694
+ return strings.Trim(params["action"], `"`)
1695
+ }
1696
+
1697
+ func (s *scanner) bindSOAP(found Call) Call {
1698
+ if contract, operation, ok := s.soapContract(found.Action, found.SOAPVersion); ok {
1699
+ found.API = contract.API
1700
+ found.ID = operation.Interface + "/" + operation.ID
1701
+ found.External = contract.External
1702
+ found.Contract = contract.Source
1703
+ if found.Endpoint == "" {
1704
+ found.Endpoint = operation.Endpoint
1705
+ }
1706
+ if found.Endpoint == "" {
1707
+ found.Endpoint = contract.URL
1708
+ }
1709
+ if found.Request == "" {
1710
+ found.Request = operation.Request
1711
+ }
1712
+ if found.Response == "" {
1713
+ found.Response = operation.Response
1714
+ }
1715
+ }
1716
+ return found
1717
+ }
1718
+
1719
+ func (s *scanner) templateValue(file *parsedFile, call *ast.CallExpr, parts []valuePart, locals map[string]string) string {
1720
+ var out strings.Builder
1721
+ for _, part := range parts {
1722
+ if !part.isParam {
1723
+ out.WriteString(part.literal)
1724
+ continue
1725
+ }
1726
+ if part.param < 0 || part.param >= len(call.Args) {
1727
+ return ""
1728
+ }
1729
+ value := s.value(file, call.Args[part.param], locals, map[string]bool{})
1730
+ if value == "" {
1731
+ return ""
1732
+ }
1733
+ out.WriteString(value)
1734
+ }
1735
+ return out.String()
1736
+ }
1737
+
1738
+ func expressionAt(args []ast.Expr, parts []valuePart) string {
1739
+ var out strings.Builder
1740
+ for _, part := range parts {
1741
+ if !part.isParam {
1742
+ out.WriteString(part.literal)
1743
+ } else if part.param >= 0 && part.param < len(args) {
1744
+ out.WriteString(expression(args[part.param]))
1745
+ }
1746
+ }
1747
+ return out.String()
1748
+ }
1749
+
1750
+ func expressionArg(args []ast.Expr, index int) string {
1751
+ if index < 0 || index >= len(args) {
1752
+ return ""
1753
+ }
1754
+ return expression(args[index])
1755
+ }
1756
+
1757
+ func firstString(values ...string) string {
1758
+ for _, value := range values {
1759
+ if value != "" {
1760
+ return value
1761
+ }
1762
+ }
1763
+ return ""
1764
+ }
1765
+
1766
+ func (s *scanner) soapContract(action, version string) (Contract, SOAPOperation, bool) {
1767
+ if action == "" {
1768
+ return Contract{}, SOAPOperation{}, false
1769
+ }
1770
+ for _, contract := range s.contracts {
1771
+ for _, operation := range contract.SOAP {
1772
+ if version != "" && operation.Version != "" && operation.Version != version {
1773
+ continue
1774
+ }
1775
+ if operation.Action == action || operation.ID == action || strings.HasSuffix(action, "/"+operation.ID) || strings.HasSuffix(operation.Action, "/"+action) {
1776
+ return contract, operation, true
1777
+ }
1778
+ }
1779
+ }
1780
+ return Contract{}, SOAPOperation{}, false
1781
+ }
1782
+
1783
+ func (s *scanner) netHTTPCall(file *parsedFile, fun ast.Expr) bool {
1784
+ sel, ok := fun.(*ast.SelectorExpr)
1785
+ if !ok {
1786
+ return false
1787
+ }
1788
+ id, ok := sel.X.(*ast.Ident)
1789
+ return ok && file.imports[id.Name] == "net/http"
1790
+ }
1791
+
1792
+ func (s *scanner) looksLikeSOAP(file *parsedFile, call *ast.CallExpr) bool {
1793
+ if importsSOAP(file) {
1794
+ return true
1795
+ }
1796
+ actionAt := 0
1797
+ if selectorName(call.Fun) == "CallContext" {
1798
+ actionAt = 1
1799
+ }
1800
+ if len(call.Args) <= actionAt {
1801
+ return false
1802
+ }
1803
+ action := strings.ToLower(expression(call.Args[actionAt]))
1804
+ return strings.Contains(action, "action") || strings.Contains(action, "soap") || strings.HasPrefix(strings.Trim(action, `"`), "urn:")
1805
+ }
1806
+
1807
+ func (s *scanner) soapTransportFile(file *parsedFile) bool {
1808
+ if !strings.Contains(strings.ToLower(file.rel), "soap") {
1809
+ return false
1810
+ }
1811
+ for _, imported := range file.imports {
1812
+ if strings.Contains(imported, "gowsdl/soap") {
1813
+ return true
1814
+ }
1815
+ }
1816
+ return false
1817
+ }
1818
+
1819
+ func (s *scanner) source(file *parsedFile, pos token.Pos) Source {
1820
+ return Source{File: file.rel, Line: s.fset.Position(pos).Line}
1821
+ }
1822
+
1823
+ func (s *scanner) value(file *parsedFile, expr ast.Expr, locals map[string]string, seen map[string]bool) string {
1824
+ switch x := expr.(type) {
1825
+ case *ast.BasicLit:
1826
+ if x.Kind == token.STRING {
1827
+ value, _ := strconv.Unquote(x.Value)
1828
+ return value
1829
+ }
1830
+ case *ast.Ident:
1831
+ if value := locals[x.Name]; value != "" {
1832
+ return value
1833
+ }
1834
+ key := file.dir + "." + x.Name
1835
+ if seen[key] {
1836
+ return ""
1837
+ }
1838
+ if constant, ok := s.constants[key]; ok {
1839
+ seen[key] = true
1840
+ return s.value(constant.file, constant.expr, locals, seen)
1841
+ }
1842
+ case *ast.SelectorExpr:
1843
+ if value := locals[expression(x)]; value != "" {
1844
+ return value
1845
+ }
1846
+ owner, ok := x.X.(*ast.Ident)
1847
+ if !ok {
1848
+ return ""
1849
+ }
1850
+ key := owner.Name + "." + x.Sel.Name
1851
+ if imported := file.imports[owner.Name]; imported != "" {
1852
+ key = filepath.Base(imported) + "." + x.Sel.Name
1853
+ }
1854
+ if constant, ok := s.constants[key]; ok && !seen[key] {
1855
+ seen[key] = true
1856
+ return s.value(constant.file, constant.expr, locals, seen)
1857
+ }
1858
+ case *ast.BinaryExpr:
1859
+ if x.Op == token.ADD {
1860
+ left := s.value(file, x.X, locals, seen)
1861
+ right := s.value(file, x.Y, locals, seen)
1862
+ if left != "" && right != "" {
1863
+ return left + right
1864
+ }
1865
+ if right != "" {
1866
+ return expression(x.X) + right
1867
+ }
1868
+ if left != "" {
1869
+ return left + expression(x.Y)
1870
+ }
1871
+ }
1872
+ case *ast.CallExpr:
1873
+ if selectorName(x.Fun) == "Join" && len(x.Args) > 0 {
1874
+ var parts []string
1875
+ for _, argument := range x.Args {
1876
+ if part := s.value(file, argument, locals, seen); part != "" {
1877
+ parts = append(parts, part)
1878
+ }
1879
+ }
1880
+ if len(parts) > 0 {
1881
+ return joinURLParts(parts)
1882
+ }
1883
+ }
1884
+ if selectorName(x.Fun) == "Sprintf" && len(x.Args) > 0 {
1885
+ format := s.value(file, x.Args[0], locals, seen)
1886
+ if format != "" {
1887
+ return format
1888
+ }
1889
+ }
1890
+ if selectorName(x.Fun) == "String" {
1891
+ if selector, ok := x.Fun.(*ast.SelectorExpr); ok {
1892
+ base := s.value(file, selector.X, locals, seen)
1893
+ path := locals[expression(selector.X)+".Path"]
1894
+ if base != "" || path != "" {
1895
+ return joinURLParts([]string{base, path})
1896
+ }
1897
+ }
1898
+ return expression(x)
1899
+ }
1900
+ if value := s.localReturnValue(file, x, locals, seen); value != "" {
1901
+ return value
1902
+ }
1903
+ }
1904
+ return ""
1905
+ }
1906
+
1907
+ func (s *scanner) localReturnValue(file *parsedFile, call *ast.CallExpr, locals map[string]string, seen map[string]bool) string {
1908
+ name, ok := call.Fun.(*ast.Ident)
1909
+ if !ok {
1910
+ return ""
1911
+ }
1912
+ key := name.Name
1913
+ if file.dir != "." && file.dir != "" {
1914
+ key = file.dir + ":" + key
1915
+ }
1916
+ declaration := s.functions[key]
1917
+ if declaration == nil || seen["return:"+key] {
1918
+ return ""
1919
+ }
1920
+ bound := map[string]string{}
1921
+ for index, param := range functionParams(declaration.fn) {
1922
+ if param == "" || index >= len(call.Args) {
1923
+ continue
1924
+ }
1925
+ bound[param] = s.value(file, call.Args[index], locals, seen)
1926
+ }
1927
+ seen["return:"+key] = true
1928
+ defer delete(seen, "return:"+key)
1929
+ var result string
1930
+ ast.Inspect(declaration.fn.Body, func(node ast.Node) bool {
1931
+ ret, ok := node.(*ast.ReturnStmt)
1932
+ if !ok || len(ret.Results) == 0 {
1933
+ return true
1934
+ }
1935
+ if value := s.value(declaration.file, ret.Results[0], bound, seen); value != "" {
1936
+ result = value
1937
+ return false
1938
+ }
1939
+ return true
1940
+ })
1941
+ return result
1942
+ }
1943
+
1944
+ func joinURLParts(parts []string) string {
1945
+ var out string
1946
+ for _, part := range parts {
1947
+ if part == "" {
1948
+ continue
1949
+ }
1950
+ if out == "" {
1951
+ out = part
1952
+ continue
1953
+ }
1954
+ out = strings.TrimRight(out, "/") + "/" + strings.TrimLeft(part, "/")
1955
+ }
1956
+ return out
1957
+ }
1958
+
1959
+ func pathOf(expr ast.Expr, endpoint string) string {
1960
+ if endpoint != "" {
1961
+ if !strings.Contains(endpoint, "://") {
1962
+ if at := strings.Index(endpoint, "/"); at >= 0 {
1963
+ candidate := endpoint[at:]
1964
+ if parsed, err := url.Parse(candidate); err == nil && parsed.Path != "" {
1965
+ return parsed.Path
1966
+ }
1967
+ return candidate
1968
+ }
1969
+ }
1970
+ if parsed, err := url.Parse(endpoint); err == nil && parsed.Path != "" {
1971
+ if strings.HasPrefix(parsed.Path, "/") || strings.Contains(endpoint, "://") {
1972
+ return parsed.Path
1973
+ }
1974
+ }
1975
+ }
1976
+ return firstPathLiteral(expr)
1977
+ }
1978
+
1979
+ func firstPathLiteral(expr ast.Expr) string {
1980
+ var found string
1981
+ ast.Inspect(expr, func(node ast.Node) bool {
1982
+ if found != "" {
1983
+ return false
1984
+ }
1985
+ lit, ok := node.(*ast.BasicLit)
1986
+ if !ok || lit.Kind != token.STRING {
1987
+ return true
1988
+ }
1989
+ value, _ := strconv.Unquote(lit.Value)
1990
+ if strings.HasPrefix(value, "/") {
1991
+ found = value
1992
+ }
1993
+ return true
1994
+ })
1995
+ return found
1996
+ }
1997
+
1998
+ func httpMethod(expr ast.Expr) string {
1999
+ switch x := expr.(type) {
2000
+ case *ast.BasicLit:
2001
+ value, _ := strconv.Unquote(x.Value)
2002
+ return strings.ToUpper(value)
2003
+ case *ast.SelectorExpr:
2004
+ return strings.ToUpper(strings.TrimPrefix(x.Sel.Name, "Method"))
2005
+ case *ast.Ident:
2006
+ return ""
2007
+ }
2008
+ return expression(expr)
2009
+ }
2010
+
2011
+ func rawCallID(method, path string) string {
2012
+ if method == "" {
2013
+ method = "HTTP"
2014
+ }
2015
+ if path == "" {
2016
+ path = "dynamic endpoint"
2017
+ }
2018
+ return "http-client/" + method + " " + path
2019
+ }
2020
+
2021
+ func selectorName(expr ast.Expr) string {
2022
+ if sel, ok := expr.(*ast.SelectorExpr); ok {
2023
+ return sel.Sel.Name
2024
+ }
2025
+ if id, ok := expr.(*ast.Ident); ok {
2026
+ return id.Name
2027
+ }
2028
+ return ""
2029
+ }
2030
+
2031
+ func receiverName(expr ast.Expr) string {
2032
+ switch x := expr.(type) {
2033
+ case *ast.Ident:
2034
+ return x.Name
2035
+ case *ast.StarExpr:
2036
+ return receiverName(x.X)
2037
+ case *ast.IndexExpr:
2038
+ return receiverName(x.X)
2039
+ case *ast.IndexListExpr:
2040
+ return receiverName(x.X)
2041
+ }
2042
+ return expression(expr)
2043
+ }
2044
+
2045
+ func expression(expr ast.Expr) string {
2046
+ if expr == nil {
2047
+ return ""
2048
+ }
2049
+ var out bytes.Buffer
2050
+ _ = printer.Fprint(&out, token.NewFileSet(), expr)
2051
+ return out.String()
2052
+ }
2053
+
2054
+ func appendCopy(items []string, value string) []string {
2055
+ out := append([]string(nil), items...)
2056
+ if value != "" {
2057
+ out = append(out, value)
2058
+ }
2059
+ return out
2060
+ }
2061
+
2062
+ func uniqueStrings(items []string) []string {
2063
+ seen := map[string]bool{}
2064
+ out := make([]string, 0, len(items))
2065
+ for _, item := range items {
2066
+ if item != "" && !seen[item] {
2067
+ seen[item] = true
2068
+ out = append(out, item)
2069
+ }
2070
+ }
2071
+ return out
2072
+ }
2073
+
2074
+ func uniqueCalls(in []Call) []Call {
2075
+ seen := map[string]bool{}
2076
+ out := make([]Call, 0, len(in))
2077
+ for _, call := range in {
2078
+ key := call.Source.String() + "\x00" + call.ID
2079
+ if seen[key] {
2080
+ continue
2081
+ }
2082
+ seen[key] = true
2083
+ out = append(out, call)
2084
+ }
2085
+ return out
2086
+ }
2087
+
2088
+ func (s *scanner) flowGroups(calls []Call) []FlowGroup {
2089
+ direct := map[string][]Call{}
2090
+ for _, call := range calls {
2091
+ direct[call.Function] = append(direct[call.Function], call)
2092
+ }
2093
+ edges := map[string][]localEdge{}
2094
+ for key, fn := range s.functions {
2095
+ ast.Inspect(fn.fn.Body, func(node ast.Node) bool {
2096
+ call, ok := node.(*ast.CallExpr)
2097
+ if !ok {
2098
+ return true
2099
+ }
2100
+ if target := s.localTarget(fn, call.Fun); target != "" && target != key {
2101
+ args := make([]ast.Expr, len(call.Args))
2102
+ for index, argument := range call.Args {
2103
+ args[index] = closureArgument(fn.fn, call.Pos(), argument)
2104
+ }
2105
+ edges[key] = append(edges[key], localEdge{target: target, line: s.fset.Position(call.Pos()).Line, args: args})
2106
+ }
2107
+ return true
2108
+ })
2109
+ }
2110
+ groups := map[string][]Call{}
2111
+ for key := range s.functions {
2112
+ collected := s.collectCalls(key, edges, direct, nil, nil, map[string]bool{}, 0)
2113
+ if len(collected) > 0 {
2114
+ groups[key] = collected
2115
+ }
2116
+ }
2117
+
2118
+ // Keep the highest source-backed caller for each resolvable call chain. This
2119
+ // removes duplicate transport flows such as Search → doRequest → RoundTrip,
2120
+ // while retaining a leaf when syntax cannot prove a caller for it.
2121
+ shadowed := map[string]bool{}
2122
+ for caller, outgoing := range edges {
2123
+ if len(groups[caller]) == 0 || s.technicalFlowFunction(caller) {
2124
+ continue
2125
+ }
2126
+ for _, edge := range outgoing {
2127
+ if len(groups[edge.target]) > 0 && !exportedFlowFunction(edge.target) {
2128
+ shadowed[edge.target] = true
2129
+ }
2130
+ }
2131
+ }
2132
+ for key := range shadowed {
2133
+ delete(groups, key)
2134
+ }
2135
+ for key := range groups {
2136
+ if s.technicalFlowFunction(key) {
2137
+ delete(groups, key)
2138
+ }
2139
+ }
2140
+
2141
+ keys := make([]string, 0, len(groups))
2142
+ for key := range groups {
2143
+ keys = append(keys, key)
2144
+ }
2145
+ sort.Strings(keys)
2146
+ out := make([]FlowGroup, 0, len(keys))
2147
+ callers := map[string][]string{}
2148
+ for caller, outgoing := range edges {
2149
+ for _, edge := range outgoing {
2150
+ callers[edge.target] = append(callers[edge.target], caller)
2151
+ }
2152
+ }
2153
+ for _, key := range keys {
2154
+ source := Source{}
2155
+ if fn := s.functions[key]; fn != nil {
2156
+ source = s.source(fn.file, fn.fn.Pos())
2157
+ }
2158
+ groupCallers := uniqueStrings(callers[key])
2159
+ sort.Strings(groupCallers)
2160
+ out = append(out, FlowGroup{Function: key, Calls: groups[key], Source: source, Callers: groupCallers})
2161
+ }
2162
+ return out
2163
+ }
2164
+
2165
+ func closureArgument(fn *ast.FuncDecl, position token.Pos, argument ast.Expr) ast.Expr {
2166
+ identifier, ok := argument.(*ast.Ident)
2167
+ if !ok {
2168
+ return argument
2169
+ }
2170
+ resolved := argument
2171
+ bestSize := token.Pos(0)
2172
+ ast.Inspect(fn.Body, func(node ast.Node) bool {
2173
+ invocation, ok := node.(*ast.CallExpr)
2174
+ if !ok {
2175
+ return true
2176
+ }
2177
+ literal, ok := invocation.Fun.(*ast.FuncLit)
2178
+ if !ok || position < literal.Body.Pos() || position > literal.Body.End() {
2179
+ return true
2180
+ }
2181
+ params := fieldListNames(literal.Type.Params)
2182
+ for index, name := range params {
2183
+ if name == identifier.Name && index < len(invocation.Args) {
2184
+ size := literal.Body.End() - literal.Body.Pos()
2185
+ if bestSize == 0 || size < bestSize {
2186
+ resolved = invocation.Args[index]
2187
+ bestSize = size
2188
+ }
2189
+ }
2190
+ }
2191
+ return true
2192
+ })
2193
+ return resolved
2194
+ }
2195
+
2196
+ func fieldListNames(fields *ast.FieldList) []string {
2197
+ if fields == nil {
2198
+ return nil
2199
+ }
2200
+ var out []string
2201
+ for _, field := range fields.List {
2202
+ for _, name := range field.Names {
2203
+ out = append(out, name.Name)
2204
+ }
2205
+ }
2206
+ return out
2207
+ }
2208
+
2209
+ func (s *scanner) collectCalls(key string, edges map[string][]localEdge, direct map[string][]Call, chain []string, bindings map[string]string, visiting map[string]bool, depth int) []Call {
2210
+ if depth > 6 || visiting[key] {
2211
+ return nil
2212
+ }
2213
+ visiting[key] = true
2214
+ defer delete(visiting, key)
2215
+ path := appendCopy(chain, displayFunction(key))
2216
+ declaration := s.functions[key]
2217
+ locals := map[string]string{}
2218
+ if declaration != nil {
2219
+ locals = s.localStringsBound(declaration.file, declaration.fn, bindings)
2220
+ }
2221
+ type event struct {
2222
+ line int
2223
+ call Call
2224
+ isCall bool
2225
+ edge *localEdge
2226
+ }
2227
+ var events []event
2228
+ for i := range direct[key] {
2229
+ events = append(events, event{line: direct[key][i].Source.Line, call: direct[key][i], isCall: true})
2230
+ }
2231
+ for i := range edges[key] {
2232
+ events = append(events, event{line: edges[key][i].line, edge: &edges[key][i]})
2233
+ }
2234
+ sort.SliceStable(events, func(i, j int) bool { return events[i].line < events[j].line })
2235
+ var out []Call
2236
+ for _, item := range events {
2237
+ if item.isCall {
2238
+ copy := item.call
2239
+ if copy.template != nil && declaration != nil {
2240
+ if copy.template.method != nil {
2241
+ method := httpMethod(copy.template.method)
2242
+ if method == "" {
2243
+ method = strings.ToUpper(s.value(declaration.file, copy.template.method, locals, map[string]bool{}))
2244
+ }
2245
+ if method != "" {
2246
+ copy.Method = method
2247
+ }
2248
+ }
2249
+ endpoint := s.value(declaration.file, copy.template.endpoint, locals, map[string]bool{})
2250
+ if endpoint != "" {
2251
+ copy.Endpoint = endpoint
2252
+ }
2253
+ if resolvedPath := pathOf(copy.template.endpoint, copy.Endpoint); resolvedPath != "" {
2254
+ copy.Path = resolvedPath
2255
+ }
2256
+ copy.ID = rawCallID(copy.Method, copy.Path)
2257
+ }
2258
+ copy.Chain = append([]string(nil), path...)
2259
+ out = append(out, copy)
2260
+ continue
2261
+ }
2262
+ nextBindings := map[string]string{}
2263
+ if target := s.functions[item.edge.target]; target != nil && declaration != nil {
2264
+ params := functionParams(target.fn)
2265
+ for index, name := range params {
2266
+ if name == "" || index >= len(item.edge.args) {
2267
+ continue
2268
+ }
2269
+ value := s.value(declaration.file, item.edge.args[index], locals, map[string]bool{})
2270
+ if value == "" {
2271
+ value = httpMethod(item.edge.args[index])
2272
+ }
2273
+ if value != "" {
2274
+ nextBindings[name] = value
2275
+ }
2276
+ }
2277
+ }
2278
+ out = append(out, s.collectCalls(item.edge.target, edges, direct, path, nextBindings, visiting, depth+1)...)
2279
+ }
2280
+ return uniqueFlowCalls(out)
2281
+ }
2282
+
2283
+ func (s *scanner) localTarget(owner *functionDecl, expr ast.Expr) string {
2284
+ switch call := expr.(type) {
2285
+ case *ast.Ident:
2286
+ candidate := call.Name
2287
+ if owner.file.dir != "." && owner.file.dir != "" {
2288
+ candidate = owner.file.dir + ":" + candidate
2289
+ }
2290
+ if s.functions[candidate] != nil {
2291
+ return candidate
2292
+ }
2293
+ case *ast.SelectorExpr:
2294
+ if ident, ok := call.X.(*ast.Ident); ok {
2295
+ if imported := owner.file.imports[ident.Name]; imported != "" {
2296
+ for key, fn := range s.functions {
2297
+ if strings.HasSuffix(imported, "/"+fn.file.dir) && displayFunction(key) == call.Sel.Name {
2298
+ return key
2299
+ }
2300
+ }
2301
+ }
2302
+ if receiverVariable(owner.fn) == ident.Name {
2303
+ receiver := receiverName(owner.fn.Recv.List[0].Type)
2304
+ candidate := owner.file.dir + ":" + receiver + "." + call.Sel.Name
2305
+ if owner.file.dir == "." || owner.file.dir == "" {
2306
+ candidate = receiver + "." + call.Sel.Name
2307
+ }
2308
+ if s.functions[candidate] != nil {
2309
+ return candidate
2310
+ }
2311
+ }
2312
+ }
2313
+ // Resolve methods invoked through a receiver field from that field's
2314
+ // declared type. This covers both concrete clients and imported client
2315
+ // interfaces without linking arbitrary selectors such as Body.Close to
2316
+ // an unrelated method that merely has the same name.
2317
+ if target := s.receiverFieldMethodTarget(owner, call); target != "" {
2318
+ return target
2319
+ }
2320
+ }
2321
+ return ""
2322
+ }
2323
+
2324
+ func receiverVariable(fn *ast.FuncDecl) string {
2325
+ if fn.Recv == nil || len(fn.Recv.List) == 0 || len(fn.Recv.List[0].Names) == 0 {
2326
+ return ""
2327
+ }
2328
+ return fn.Recv.List[0].Names[0].Name
2329
+ }
2330
+
2331
+ func (s *scanner) receiverFieldMethodTarget(owner *functionDecl, call *ast.SelectorExpr) string {
2332
+ fieldSelector, ok := call.X.(*ast.SelectorExpr)
2333
+ if !ok {
2334
+ return ""
2335
+ }
2336
+ root, ok := fieldSelector.X.(*ast.Ident)
2337
+ if !ok || root.Name != receiverVariable(owner.fn) {
2338
+ return ""
2339
+ }
2340
+ receiver := receiverName(owner.fn.Recv.List[0].Type)
2341
+ var fieldType endpointType
2342
+ for _, file := range s.files {
2343
+ if file.dir != owner.file.dir {
2344
+ continue
2345
+ }
2346
+ for _, declaration := range file.node.Decls {
2347
+ generic, ok := declaration.(*ast.GenDecl)
2348
+ if !ok || generic.Tok != token.TYPE {
2349
+ continue
2350
+ }
2351
+ for _, spec := range generic.Specs {
2352
+ typeSpec, ok := spec.(*ast.TypeSpec)
2353
+ if !ok || typeSpec.Name.Name != receiver {
2354
+ continue
2355
+ }
2356
+ structure, ok := typeSpec.Type.(*ast.StructType)
2357
+ if !ok {
2358
+ continue
2359
+ }
2360
+ for _, field := range structure.Fields.List {
2361
+ for _, name := range field.Names {
2362
+ if name.Name == fieldSelector.Sel.Name {
2363
+ fieldType, _ = s.typeExpression(file, field.Type)
2364
+ }
2365
+ }
2366
+ }
2367
+ }
2368
+ }
2369
+ }
2370
+ if fieldType.name == "" {
2371
+ return ""
2372
+ }
2373
+ if key := s.methodKey(fieldType, call.Sel.Name); s.functions[key] != nil {
2374
+ return key
2375
+ }
2376
+ if !s.interfaceHasMethod(fieldType, call.Sel.Name) {
2377
+ return ""
2378
+ }
2379
+ ownerType := endpointType{dir: owner.file.dir, name: receiver}
2380
+ var wiredCandidates []string
2381
+ for _, concreteType := range s.fieldTypes[fieldTypeKey(ownerType, fieldSelector.Sel.Name)] {
2382
+ if key := s.methodKey(concreteType, call.Sel.Name); key != "" {
2383
+ wiredCandidates = append(wiredCandidates, key)
2384
+ }
2385
+ }
2386
+ wiredCandidates = uniqueStrings(wiredCandidates)
2387
+ if len(wiredCandidates) == 1 {
2388
+ return wiredCandidates[0]
2389
+ }
2390
+ if len(wiredCandidates) > 1 {
2391
+ return ""
2392
+ }
2393
+ var localCandidates []string
2394
+ for _, key := range s.methods[call.Sel.Name] {
2395
+ dir, _, qualified := strings.Cut(key, ":")
2396
+ if !qualified {
2397
+ dir = "."
2398
+ }
2399
+ if dir == fieldType.dir {
2400
+ localCandidates = append(localCandidates, key)
2401
+ }
2402
+ }
2403
+ if len(localCandidates) == 1 {
2404
+ return localCandidates[0]
2405
+ }
2406
+ if candidates := s.methods[call.Sel.Name]; len(candidates) == 1 {
2407
+ return candidates[0]
2408
+ }
2409
+ return ""
2410
+ }
2411
+
2412
+ func (s *scanner) interfaceHasMethod(typ endpointType, methodName string) bool {
2413
+ for _, file := range s.files {
2414
+ if file.dir != typ.dir {
2415
+ continue
2416
+ }
2417
+ for _, declaration := range file.node.Decls {
2418
+ generic, ok := declaration.(*ast.GenDecl)
2419
+ if !ok || generic.Tok != token.TYPE {
2420
+ continue
2421
+ }
2422
+ for _, spec := range generic.Specs {
2423
+ typeSpec, ok := spec.(*ast.TypeSpec)
2424
+ if !ok || typeSpec.Name.Name != typ.name {
2425
+ continue
2426
+ }
2427
+ iface, ok := typeSpec.Type.(*ast.InterfaceType)
2428
+ if !ok {
2429
+ return false
2430
+ }
2431
+ for _, method := range iface.Methods.List {
2432
+ for _, name := range method.Names {
2433
+ if name.Name == methodName {
2434
+ return true
2435
+ }
2436
+ }
2437
+ }
2438
+ }
2439
+ }
2440
+ }
2441
+ return false
2442
+ }
2443
+
2444
+ func exportedFlowFunction(key string) bool {
2445
+ name := displayFunction(key)
2446
+ if at := strings.LastIndex(name, "."); at >= 0 {
2447
+ name = name[at+1:]
2448
+ }
2449
+ if name == "" {
2450
+ return false
2451
+ }
2452
+ for _, first := range name {
2453
+ return unicode.IsUpper(first)
2454
+ }
2455
+ return false
2456
+ }
2457
+
2458
+ func (s *scanner) technicalFlowFunction(key string) bool {
2459
+ name := strings.ToLower(displayFunction(key))
2460
+ method := false
2461
+ if at := strings.LastIndex(name, "."); at >= 0 {
2462
+ name = name[at+1:]
2463
+ method = true
2464
+ }
2465
+ switch name {
2466
+ case "main", "new", "dorequest", "request", "roundtrip", "call", "do", "send", "flush", "decode", "encode", "getbytes", "postbytes", "postjsonbytes":
2467
+ return true
2468
+ case "init":
2469
+ // Package init is assembly code. An exported method named Init is often
2470
+ // the business operation itself (for example, initializing a booking).
2471
+ return !method
2472
+ }
2473
+ if name == "run" {
2474
+ if declaration := s.functions[key]; declaration != nil {
2475
+ dir := "/" + filepath.ToSlash(declaration.file.dir) + "/"
2476
+ return strings.Contains(dir, "/cmd/") || strings.Contains(dir, "/app/")
2477
+ }
2478
+ }
2479
+ return strings.HasPrefix(name, "new") && strings.HasSuffix(name, "client")
2480
+ }
2481
+
2482
+ func displayFunction(key string) string {
2483
+ if _, name, ok := strings.Cut(key, ":"); ok {
2484
+ return name
2485
+ }
2486
+ return key
2487
+ }
2488
+
2489
+ func uniqueFlowCalls(in []Call) []Call {
2490
+ seen := map[string]bool{}
2491
+ out := make([]Call, 0, len(in))
2492
+ for _, call := range in {
2493
+ key := call.Source.String() + "\x00" + call.ID
2494
+ if !seen[key] {
2495
+ seen[key] = true
2496
+ out = append(out, call)
2497
+ }
2498
+ }
2499
+ return out
2500
+ }