@shortlink-org/portolan 0.2.4 → 0.4.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 (291) hide show
  1. package/README.md +174 -6
  2. package/catalog/enum_test.go +46 -0
  3. package/catalog/evidence_test.go +35 -0
  4. package/catalog/model.go +1213 -0
  5. package/catalog/roundtrip_test.go +210 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/init.test.mjs +6 -1
  8. package/cli/portolan.mjs +14 -1
  9. package/cli/portolan.test.mjs +49 -0
  10. package/go.mod +21 -0
  11. package/go.sum +34 -0
  12. package/internal/gocall/README.md +19 -0
  13. package/internal/gocall/analyze.go +189 -0
  14. package/internal/gocall/analyze_test.go +107 -0
  15. package/internal/gohttp/analyze.go +2562 -0
  16. package/internal/gohttp/destination.go +373 -0
  17. package/internal/gohttp/endpoints.go +1067 -0
  18. package/internal/gohttp/roots.go +320 -0
  19. package/internal/gohttp/typed.go +96 -0
  20. package/internal/goscan/constants.go +85 -0
  21. package/internal/goscan/goscan_test.go +227 -0
  22. package/internal/goscan/index.go +629 -0
  23. package/internal/goscan/index_test.go +66 -0
  24. package/internal/goscan/names.go +52 -0
  25. package/internal/goscan/parse_test.go +11 -0
  26. package/internal/goscan/source.go +37 -0
  27. package/internal/goscan/tree.go +284 -0
  28. package/internal/goscan/types.go +99 -0
  29. package/internal/wsdl/ids.go +127 -0
  30. package/internal/wsdl/ids_test.go +21 -0
  31. package/internal/wsdl/model.go +70 -0
  32. package/internal/wsdl/parse.go +949 -0
  33. package/internal/wsdl/parse_test.go +170 -0
  34. package/package.json +28 -15
  35. package/plugin/describe.go +156 -0
  36. package/plugin/describe_test.go +114 -0
  37. package/plugin/protocol.go +141 -0
  38. package/plugin/schematest/schematest.go +126 -0
  39. package/plugins/README.md +404 -50
  40. package/plugins/cmd/portolan-http-clients/main.go +19 -0
  41. package/plugins/extract-argocd/options.schema.json +44 -0
  42. package/plugins/extract-celery/extract.py +0 -2
  43. package/plugins/extract-celery/extract_test.py +1 -1
  44. package/plugins/extract-celery/main.py +1 -0
  45. package/plugins/extract-csharp-ddd/README.md +213 -0
  46. package/plugins/extract-csharp-ddd/options.schema.json +55 -0
  47. package/plugins/extract-django/README.md +52 -19
  48. package/plugins/extract-django/choices.py +47 -0
  49. package/plugins/extract-django/domain.py +30 -18
  50. package/plugins/extract-django/extract.py +21 -7
  51. package/plugins/extract-django/extract_test.py +68 -2
  52. package/plugins/extract-django/lifecycle.py +4 -28
  53. package/plugins/extract-django/main.py +1 -0
  54. package/plugins/extract-django/operations.py +1 -1
  55. package/plugins/extract-django/routing_test.py +109 -1
  56. package/plugins/extract-django/rules.py +154 -0
  57. package/plugins/extract-django/rules_test.py +158 -0
  58. package/plugins/extract-django/store.py +1 -1
  59. package/plugins/extract-django/transport.py +101 -55
  60. package/plugins/extract-django/verbs.py +241 -0
  61. package/plugins/extract-go/README.md +47 -0
  62. package/plugins/extract-go-sqs/options.schema.json +21 -0
  63. package/plugins/extract-http-clients/describe.go +20 -0
  64. package/plugins/extract-http-clients/describe_test.go +11 -0
  65. package/plugins/extract-http-clients/extract.go +740 -0
  66. package/plugins/extract-http-clients/extract_test.go +1561 -0
  67. package/plugins/extract-http-clients/main.go +41 -0
  68. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  69. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  70. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  71. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  72. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  73. package/plugins/extract-k8s/options.schema.json +32 -0
  74. package/plugins/extract-laravel/Cargo.lock +962 -0
  75. package/plugins/extract-laravel/Cargo.toml +20 -0
  76. package/plugins/extract-laravel/README.md +200 -0
  77. package/plugins/extract-laravel/options.schema.json +68 -0
  78. package/plugins/extract-laravel/rustfmt.toml +1 -0
  79. package/plugins/extract-php-ddd/Cargo.lock +972 -0
  80. package/plugins/extract-php-ddd/Cargo.toml +22 -0
  81. package/plugins/extract-php-ddd/README.md +141 -0
  82. package/plugins/extract-php-ddd/options.schema.json +50 -0
  83. package/plugins/extract-php-ddd/rustfmt.toml +1 -0
  84. package/plugins/extract-python-kafka/extract.py +0 -2
  85. package/plugins/extract-python-kafka/extract_test.py +1 -1
  86. package/plugins/extract-python-kafka/main.py +1 -0
  87. package/plugins/extract-terraform/options.schema.json +26 -0
  88. package/plugins/extract-ts/extract.test.ts +2 -2
  89. package/plugins/extract-ts/extract.ts +4 -5
  90. package/plugins/extract-ts/graphql.test.ts +1 -1
  91. package/plugins/extract-ts/main.ts +1 -0
  92. package/plugins/openapi/ids.go +261 -0
  93. package/plugins/openapi/ids_test.go +98 -0
  94. package/plugins/phpscan/Cargo.lock +953 -0
  95. package/plugins/phpscan/Cargo.toml +19 -0
  96. package/plugins/phpscan/rustfmt.toml +1 -0
  97. package/plugins/portolan-go.wasm +0 -0
  98. package/plugins/pyplugin/catalog.py +24 -1
  99. package/plugins/pyplugin/protocol.py +1 -5
  100. package/plugins/verify-otel/options.schema.json +12 -0
  101. package/portolan.json +132 -4
  102. package/rules/builtin.json +218 -0
  103. package/schema/portolan.schema.json +905 -4
  104. package/scripts/README.md +21 -13
  105. package/scripts/catalog-sources.mjs +6 -0
  106. package/scripts/delivery-presets.mjs +57 -14
  107. package/scripts/diff.mjs +5 -1
  108. package/scripts/django-aggregates.test.mjs +58 -0
  109. package/scripts/gen-likec4.mjs +150 -17
  110. package/scripts/gen-likec4.test.mjs +96 -0
  111. package/scripts/gen.mjs +148 -118
  112. package/scripts/gitops-example.test.mjs +108 -0
  113. package/scripts/go-discovery.test.mjs +30 -0
  114. package/scripts/history.mjs +186 -3
  115. package/scripts/history.test.mjs +1 -1
  116. package/scripts/host-plugins/fetch-argocd.mjs +338 -0
  117. package/scripts/host-plugins/fetch-argocd.options.json +49 -0
  118. package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
  119. package/scripts/host-plugins/fetch-bsr.mjs +1 -0
  120. package/scripts/host-plugins/fetch-csr.mjs +1 -0
  121. package/scripts/host-plugins/fetch-git.mjs +78 -21
  122. package/scripts/host-plugins/fetch-git.test.mjs +62 -8
  123. package/scripts/host-plugins/fetch-k8s.mjs +263 -0
  124. package/scripts/host-plugins/fetch-k8s.options.json +50 -0
  125. package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
  126. package/scripts/host-plugins/k8s-topology.mjs +183 -0
  127. package/scripts/json-format.mjs +192 -0
  128. package/scripts/json-format.test.mjs +97 -0
  129. package/scripts/local-api.mjs +287 -12
  130. package/scripts/local-api.test.mjs +150 -5
  131. package/scripts/local-discovery.mjs +94 -9
  132. package/scripts/manifest.mjs +14 -3
  133. package/scripts/manifest.test.mjs +24 -0
  134. package/scripts/output-diff.mjs +94 -0
  135. package/scripts/output-diff.test.mjs +36 -0
  136. package/scripts/package-smoke.mjs +62 -4
  137. package/scripts/plugin-host.mjs +45 -3
  138. package/scripts/plugin-host.test.mjs +9 -0
  139. package/scripts/plugin-wasm-worker.mjs +4 -1
  140. package/scripts/plugins-fresh.mjs +97 -0
  141. package/scripts/plugins-fresh.test.mjs +64 -0
  142. package/scripts/provenance.mjs +72 -0
  143. package/scripts/provenance.test.mjs +149 -0
  144. package/scripts/run-builtin.mjs +62 -7
  145. package/scripts/schema.mjs +167 -19
  146. package/scripts/trace-trials.mjs +176 -0
  147. package/scripts/trace-trials.test.mjs +142 -0
  148. package/scripts/warning-policy.mjs +167 -0
  149. package/scripts/warning-policy.test.mjs +93 -0
  150. package/src/app/Breadcrumbs.test.ts +3 -0
  151. package/src/app/Breadcrumbs.tsx +3 -0
  152. package/src/app/CatalogApp.tsx +2 -0
  153. package/src/app/Sidebar.tsx +3 -3
  154. package/src/app/SidebarFooter.tsx +20 -4
  155. package/src/catalog-docs.test.ts +64 -0
  156. package/src/catalog-docs.ts +35 -0
  157. package/src/catalog-error.test.ts +15 -0
  158. package/src/catalog-index.ts +25 -0
  159. package/src/catalog-model.ts +293 -5
  160. package/src/catalog-validation.ts +113 -2
  161. package/src/catalog.test.ts +40 -1
  162. package/src/chat/Starter.tsx +5 -11
  163. package/src/chat/tools.test.ts +27 -0
  164. package/src/chat/tools.ts +5 -9
  165. package/src/components/CatalogStamp.tsx +10 -8
  166. package/src/components/ChannelRows.test.tsx +76 -1
  167. package/src/components/ChannelRows.tsx +223 -60
  168. package/src/components/DeploymentRows.tsx +131 -0
  169. package/src/components/DocsLinks.test.tsx +27 -0
  170. package/src/components/DocsLinks.tsx +56 -0
  171. package/src/components/FieldTree.tsx +5 -2
  172. package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
  173. package/src/components/HTTPDestinationEvidence.tsx +31 -0
  174. package/src/components/Integrations.tsx +1 -1
  175. package/src/components/MachineDocs.tsx +6 -5
  176. package/src/components/MethodRows.tsx +9 -2
  177. package/src/components/PluginIcon.tsx +77 -0
  178. package/src/components/ProblemRow.tsx +173 -153
  179. package/src/components/RelationEvidence.test.tsx +14 -0
  180. package/src/components/RelationEvidence.tsx +53 -0
  181. package/src/components/RuleMarks.tsx +22 -0
  182. package/src/components/ShapeRows.tsx +24 -20
  183. package/src/data.ts +46 -7
  184. package/src/enrich.test.ts +459 -4
  185. package/src/enrich.ts +308 -7
  186. package/src/er/ErCanvas.tsx +218 -12
  187. package/src/er/GroupNode.tsx +57 -0
  188. package/src/er/StoreHeader.tsx +1 -0
  189. package/src/er/layout.test.ts +85 -2
  190. package/src/er/layout.ts +140 -5
  191. package/src/er/spec.test.ts +17 -0
  192. package/src/er/spec.ts +23 -10
  193. package/src/flow/Recordings.test.tsx +52 -0
  194. package/src/flow/Recordings.tsx +236 -0
  195. package/src/flow/StepDetail.tsx +59 -0
  196. package/src/flow/TraceTrial.tsx +419 -0
  197. package/src/flow/evidence.test.ts +16 -0
  198. package/src/flow/evidence.ts +34 -0
  199. package/src/flow/examples.test.ts +33 -0
  200. package/src/flow/examples.ts +37 -0
  201. package/src/flow/outline.test.ts +28 -0
  202. package/src/flow/outline.ts +7 -2
  203. package/src/flow/trace-trial-resume.test.ts +42 -0
  204. package/src/flow/trace-trial-resume.ts +74 -0
  205. package/src/graph/elk.ts +78 -0
  206. package/src/index.css +68 -0
  207. package/src/landing/DraggableReveal.tsx +3 -2
  208. package/src/landing/EvidencePipeline.tsx +105 -0
  209. package/src/landing/LandingPage.tsx +17 -70
  210. package/src/landing/ProductTour.tsx +6 -6
  211. package/src/lib/all-problems.ts +27 -17
  212. package/src/lib/catalog-diff.ts +1 -1
  213. package/src/lib/centrality.test.ts +251 -0
  214. package/src/lib/centrality.ts +232 -0
  215. package/src/lib/confluence.test.ts +37 -0
  216. package/src/lib/confluence.ts +41 -0
  217. package/src/lib/context-color.ts +1 -1
  218. package/src/lib/deployment-drift.ts +22 -0
  219. package/src/lib/derive.ts +43 -69
  220. package/src/lib/django-aggregates.d.mts +9 -0
  221. package/src/lib/django-aggregates.mjs +36 -0
  222. package/src/lib/django-aggregates.test.ts +29 -0
  223. package/src/lib/django-aggregates.ts +5 -0
  224. package/src/lib/environments.test.ts +83 -0
  225. package/src/lib/environments.ts +57 -0
  226. package/src/lib/integration-url.test.ts +30 -0
  227. package/src/lib/integration-url.ts +63 -0
  228. package/src/lib/kafka-ui.ts +3 -45
  229. package/src/lib/local-api.ts +116 -4
  230. package/src/lib/notion.ts +13 -0
  231. package/src/lib/plugin-index.json +2765 -0
  232. package/src/lib/plugins.test.ts +68 -0
  233. package/src/lib/plugins.ts +264 -0
  234. package/src/lib/problem-flows.test.ts +61 -0
  235. package/src/lib/problem-flows.ts +78 -0
  236. package/src/lib/problem-rules-cel.d.mts +43 -0
  237. package/src/lib/problem-rules-cel.mjs +407 -0
  238. package/src/lib/problem-rules.test.ts +287 -0
  239. package/src/lib/problem-rules.ts +271 -0
  240. package/src/lib/problem-subjects.ts +737 -0
  241. package/src/lib/rule-entries.ts +39 -0
  242. package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
  243. package/src/lib/rules-deploy.test.ts +133 -0
  244. package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
  245. package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
  246. package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
  247. package/src/lib/rules.test.ts +51 -0
  248. package/src/lib/rules.ts +86 -0
  249. package/src/lib/setup-info.test.ts +17 -0
  250. package/src/lib/setup-info.ts +58 -0
  251. package/src/lib/shape.test.ts +32 -0
  252. package/src/lib/shape.ts +30 -6
  253. package/src/lib/tech.ts +16 -0
  254. package/src/lib/trace-project.test.ts +34 -0
  255. package/src/lib/trace-project.ts +50 -0
  256. package/src/lib/use-problems.ts +23 -0
  257. package/src/lib/warnings.test.ts +63 -0
  258. package/src/lib/warnings.ts +260 -0
  259. package/src/likec4/ids.test.ts +6 -2
  260. package/src/likec4/ids.ts +43 -0
  261. package/src/main.tsx +23 -0
  262. package/src/map/ContextMapGraph.tsx +76 -32
  263. package/src/merge-deployments.test.ts +127 -0
  264. package/src/merge.test.ts +82 -0
  265. package/src/merge.ts +189 -18
  266. package/src/pages/AggregatePage.tsx +65 -14
  267. package/src/pages/ContextMap.tsx +45 -3
  268. package/src/pages/ContextPage.tsx +8 -5
  269. package/src/pages/EventPage.tsx +15 -5
  270. package/src/pages/FlowDetail.tsx +23 -2
  271. package/src/pages/GraphPage.tsx +40 -3
  272. package/src/pages/Overview.tsx +152 -12
  273. package/src/pages/PluginIndex.tsx +190 -0
  274. package/src/pages/Problems.tsx +396 -128
  275. package/src/pages/ServicePage.tsx +62 -5
  276. package/src/pages/Settings.tsx +217 -43
  277. package/src/pages/settings/AboutSettings.tsx +8 -1
  278. package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
  279. package/src/pages/settings/IntegrationsSettings.tsx +63 -17
  280. package/src/pages/settings/RecordingSettings.tsx +138 -0
  281. package/src/pages/settings/RulesSettings.tsx +825 -0
  282. package/src/routes.test.ts +9 -0
  283. package/src/routes.ts +22 -1
  284. package/src/selection/DetailPanel.tsx +15 -0
  285. package/src/virtual-provenance.d.ts +11 -0
  286. package/vite.config.ts +5 -0
  287. package/scripts/vendor-lock.mjs +0 -58
  288. package/scripts/vendor-lock.test.mjs +0 -69
  289. package/src/lib/data-problems.ts +0 -314
  290. package/src/lib/proto-problems.ts +0 -237
  291. package/src/lib/wire-problems.ts +0 -342
@@ -0,0 +1,373 @@
1
+ package gohttp
2
+
3
+ import (
4
+ "go/ast"
5
+ "go/token"
6
+ "net/url"
7
+ "reflect"
8
+ "sort"
9
+ "strconv"
10
+ "strings"
11
+
12
+ "github.com/shortlink-org/portolan/catalog"
13
+ )
14
+
15
+ type destinationObject map[string]catalog.HTTPBaseURL
16
+
17
+ // Keep constructor settings on the owning adapter field, never on the shared
18
+ // HTTP client type. Conflicting or unreadable construction sites invalidate
19
+ // the field instead of borrowing another instance's destination.
20
+ func (s *scanner) indexDestinations() {
21
+ s.destinations = map[string]destinationObject{}
22
+ candidates := map[string][]destinationObject{}
23
+ keys := make([]string, 0, len(s.functions))
24
+ for key := range s.functions {
25
+ keys = append(keys, key)
26
+ }
27
+ sort.Strings(keys)
28
+ for _, key := range keys {
29
+ caller := s.functions[key]
30
+ ast.Inspect(caller.fn.Body, func(n ast.Node) bool {
31
+ call, ok := n.(*ast.CallExpr)
32
+ if !ok {
33
+ return true
34
+ }
35
+ target := s.functions[s.localTarget(caller, call.Fun)]
36
+ if target == nil {
37
+ return true
38
+ }
39
+ results := s.expressionResultTypes(caller, call, nil)
40
+ if len(results) == 0 || results[0].name == "" {
41
+ return true
42
+ }
43
+ object := s.destinationObject(caller, call, nil, map[string]bool{})
44
+ // A wrapper receives a configured client; a bare Client constructor must
45
+ // not establish a type-wide base URL shared by all its instances.
46
+ nested := destinationObject{}
47
+ for field, base := range object {
48
+ if strings.Contains(field, ".") {
49
+ nested[field] = base
50
+ }
51
+ }
52
+ owner := fieldTypeKey(results[0], "")
53
+ candidates[owner] = append(candidates[owner], nested)
54
+ return true
55
+ })
56
+ }
57
+ for owner, objects := range candidates {
58
+ if len(objects) == 0 {
59
+ continue
60
+ }
61
+ common := objects[0]
62
+ for field, base := range common {
63
+ for _, object := range objects[1:] {
64
+ other, ok := object[field]
65
+ if !ok || base.Value != other.Value || base.ConfigField != other.ConfigField || base.EnvironmentVariable != other.EnvironmentVariable || base.Kind != other.Kind {
66
+ delete(common, field)
67
+ break
68
+ }
69
+ }
70
+ }
71
+ if len(common) > 0 {
72
+ s.destinations[owner] = common
73
+ }
74
+ }
75
+ }
76
+
77
+ // Read returned struct fields and the narrow functional-option shape
78
+ // func(value string) Option { return func(c *Client) { c.baseURL = value } }.
79
+ func (s *scanner) destinationObject(owner *functionDecl, expr ast.Expr, bound map[string]destinationObject, seen map[string]bool) destinationObject {
80
+ if id, ok := expr.(*ast.Ident); ok {
81
+ return bound[id.Name]
82
+ }
83
+ if literal := compositeLiteral(expr); literal != nil {
84
+ result := destinationObject{}
85
+ for _, element := range literal.Elts {
86
+ pair, ok := element.(*ast.KeyValueExpr)
87
+ if !ok {
88
+ continue
89
+ }
90
+ for field, base := range s.destinationObject(owner, pair.Value, bound, seen) {
91
+ result[expression(pair.Key)+"."+field] = base
92
+ }
93
+ }
94
+ return result
95
+ }
96
+ call, ok := expr.(*ast.CallExpr)
97
+ if !ok {
98
+ return nil
99
+ }
100
+ key := s.localTarget(owner, call.Fun)
101
+ target := s.functions[key]
102
+ if target == nil || seen[key] {
103
+ return nil
104
+ }
105
+ seen[key] = true
106
+ defer delete(seen, key)
107
+ args := map[string]destinationObject{}
108
+ for i, param := range functionParams(target.fn) {
109
+ if i < len(call.Args) {
110
+ args[param] = s.destinationObject(owner, call.Args[i], bound, seen)
111
+ }
112
+ }
113
+ // Only apply options if the constructor actually invokes each option on
114
+ // the same local object it returns.
115
+ var returned string
116
+ for _, statement := range target.fn.Body.List {
117
+ if ret, ok := statement.(*ast.ReturnStmt); ok && len(ret.Results) == 1 {
118
+ returned = expression(ret.Results[0])
119
+ }
120
+ }
121
+ optionsApplied := false
122
+ variadic := ""
123
+ if functionVariadic(target.fn) {
124
+ params := functionParams(target.fn)
125
+ variadic = params[len(params)-1]
126
+ }
127
+ ast.Inspect(target.fn.Body, func(n ast.Node) bool {
128
+ loop, ok := n.(*ast.RangeStmt)
129
+ if !ok || variadic == "" || expression(loop.X) != variadic {
130
+ return true
131
+ }
132
+ for _, stmt := range loop.Body.List {
133
+ statement, ok := stmt.(*ast.ExprStmt)
134
+ if !ok {
135
+ continue
136
+ }
137
+ invocation, ok := statement.X.(*ast.CallExpr)
138
+ if ok && expression(invocation.Fun) == expression(loop.Value) && len(invocation.Args) == 1 && expression(invocation.Args[0]) == returned {
139
+ optionsApplied = true
140
+ }
141
+ }
142
+ return false
143
+ })
144
+ for _, statement := range target.fn.Body.List {
145
+ if assign, ok := statement.(*ast.AssignStmt); ok {
146
+ for i, left := range assign.Lhs {
147
+ if i < len(assign.Rhs) {
148
+ args[expression(left)] = s.destinationObject(target, assign.Rhs[i], args, seen)
149
+ }
150
+ }
151
+ }
152
+ }
153
+ var result destinationObject
154
+ for _, statement := range target.fn.Body.List {
155
+ if ret, ok := statement.(*ast.ReturnStmt); ok && len(ret.Results) == 1 {
156
+ result = s.destinationObject(target, ret.Results[0], args, seen)
157
+ }
158
+ }
159
+ if result == nil {
160
+ result = destinationObject{}
161
+ }
162
+ if optionsApplied && len(call.Args) >= len(functionParams(target.fn))-1 {
163
+ start := len(functionParams(target.fn)) - 1
164
+ for _, argument := range call.Args[start:] {
165
+ option, ok := argument.(*ast.CallExpr)
166
+ if !ok {
167
+ continue
168
+ }
169
+ setter := s.functions[s.localTarget(owner, option.Fun)]
170
+ if setter == nil {
171
+ continue
172
+ }
173
+ params := functionParams(setter.fn)
174
+ for _, statement := range setter.fn.Body.List {
175
+ ret, ok := statement.(*ast.ReturnStmt)
176
+ if !ok || len(ret.Results) != 1 {
177
+ continue
178
+ }
179
+ closure, ok := ret.Results[0].(*ast.FuncLit)
180
+ if !ok || len(closure.Type.Params.List) != 1 || len(closure.Type.Params.List[0].Names) != 1 {
181
+ continue
182
+ }
183
+ receiver := closure.Type.Params.List[0].Names[0].Name
184
+ for _, stmt := range closure.Body.List {
185
+ assign, ok := stmt.(*ast.AssignStmt)
186
+ if !ok || assign.Tok != token.ASSIGN || len(assign.Lhs) != 1 || len(assign.Rhs) != 1 {
187
+ continue
188
+ }
189
+ field, ok := assign.Lhs[0].(*ast.SelectorExpr)
190
+ if !ok || expression(field.X) != receiver || !looksLikeURLName(field.Sel.Name) {
191
+ continue
192
+ }
193
+ for i, param := range params {
194
+ if expression(assign.Rhs[0]) == param && i < len(option.Args) {
195
+ base := s.destinationBase(owner, option.Args[i])
196
+ base.OptionSource = s.source(owner.file, option.Pos()).String()
197
+ result[field.Sel.Name] = base
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+ return result
205
+ }
206
+
207
+ func (s *scanner) destinationBase(owner *functionDecl, expr ast.Expr) catalog.HTTPBaseURL {
208
+ base := catalog.HTTPBaseURL{Expression: expression(expr), Kind: "symbolic", Source: s.source(owner.file, expr.Pos()).String()}
209
+ if literal, ok := expr.(*ast.BasicLit); ok && literal.Kind == token.STRING {
210
+ base.Value, _ = strconv.Unquote(literal.Value)
211
+ base.Kind = "literal"
212
+ return base
213
+ }
214
+ selector, ok := expr.(*ast.SelectorExpr)
215
+ if !ok {
216
+ return base
217
+ }
218
+ base.ConfigField = s.symbolicValue(owner, expr, nil, map[string]bool{})
219
+ typ, ok := s.destinationExprType(owner, selector.X)
220
+ if !ok {
221
+ return base
222
+ }
223
+ for _, file := range s.files {
224
+ if file.dir != typ.dir {
225
+ continue
226
+ }
227
+ for _, decl := range file.node.Decls {
228
+ gen, ok := decl.(*ast.GenDecl)
229
+ if !ok {
230
+ continue
231
+ }
232
+ for _, spec := range gen.Specs {
233
+ named, ok := spec.(*ast.TypeSpec)
234
+ if !ok || named.Name.Name != typ.name {
235
+ continue
236
+ }
237
+ structure, ok := named.Type.(*ast.StructType)
238
+ if !ok {
239
+ continue
240
+ }
241
+ for _, field := range structure.Fields.List {
242
+ for _, name := range field.Names {
243
+ if name.Name != selector.Sel.Name {
244
+ continue
245
+ }
246
+ base.ConfigField = typ.name + "." + name.Name
247
+ base.Source = s.source(file, field.Pos()).String()
248
+ if field.Tag != nil {
249
+ tag, _ := strconv.Unquote(field.Tag.Value)
250
+ base.EnvironmentVariable = reflect.StructTag(tag).Get("envconfig")
251
+ if value, exists := reflect.StructTag(tag).Lookup("default"); exists {
252
+ base.Value = value
253
+ base.Kind = "config-default"
254
+ }
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+ }
261
+ return base
262
+ }
263
+
264
+ func (s *scanner) destinationExprType(owner *functionDecl, expr ast.Expr) (endpointType, bool) {
265
+ if id, ok := expr.(*ast.Ident); ok {
266
+ if owner.fn.Type.Params != nil {
267
+ for _, field := range owner.fn.Type.Params.List {
268
+ for _, name := range field.Names {
269
+ if name.Name == id.Name {
270
+ return s.typeExpression(owner.file, field.Type)
271
+ }
272
+ }
273
+ }
274
+ }
275
+ typ, exists := s.localConcreteTypes(owner)[id.Name]
276
+ return typ, exists
277
+ }
278
+ if selector, ok := expr.(*ast.SelectorExpr); ok {
279
+ parent, exists := s.destinationExprType(owner, selector.X)
280
+ if !exists {
281
+ return endpointType{}, false
282
+ }
283
+ for _, file := range s.files {
284
+ if file.dir != parent.dir {
285
+ continue
286
+ }
287
+ for _, decl := range file.node.Decls {
288
+ gen, ok := decl.(*ast.GenDecl)
289
+ if !ok {
290
+ continue
291
+ }
292
+ for _, spec := range gen.Specs {
293
+ named, ok := spec.(*ast.TypeSpec)
294
+ if !ok || named.Name.Name != parent.name {
295
+ continue
296
+ }
297
+ structure, ok := named.Type.(*ast.StructType)
298
+ if !ok {
299
+ continue
300
+ }
301
+ for _, field := range structure.Fields.List {
302
+ for _, name := range field.Names {
303
+ if name.Name == selector.Sel.Name {
304
+ return s.typeExpression(file, field.Type)
305
+ }
306
+ }
307
+ }
308
+ }
309
+ }
310
+ }
311
+ types := s.fieldTypes[fieldTypeKey(parent, selector.Sel.Name)]
312
+ if len(types) == 1 {
313
+ return types[0], true
314
+ }
315
+ }
316
+ return endpointType{}, false
317
+ }
318
+
319
+ func (s *scanner) destinationFor(call Call, declaration *functionDecl, origins destinationObject) *catalog.HTTPDestination {
320
+ if call.Protocol != "HTTP" || call.template == nil {
321
+ return nil
322
+ }
323
+ d := &catalog.HTTPDestination{CallSite: call.Source.String(), EndpointExpression: expression(call.template.endpoint), Method: call.Method, LocalPath: call.Path}
324
+ // A recovered receiver binding only proves a join when source explicitly
325
+ // concatenates that field with a path. Later runtime transformations are
326
+ // retained separately; the full path describes this source-backed join.
327
+ receiver := receiverVariable(declaration.fn)
328
+ for field, base := range origins {
329
+ if strings.Contains(field, ".") {
330
+ continue
331
+ }
332
+ var join *ast.BinaryExpr
333
+ ast.Inspect(declaration.fn.Body, func(n ast.Node) bool {
334
+ binary, ok := n.(*ast.BinaryExpr)
335
+ if ok && binary.Op == token.ADD && expression(binary.X) == receiver+"."+field {
336
+ join = binary
337
+ }
338
+ return true
339
+ })
340
+ if join == nil {
341
+ continue
342
+ }
343
+ ast.Inspect(declaration.fn.Body, func(n ast.Node) bool {
344
+ assign, ok := n.(*ast.AssignStmt)
345
+ if !ok || assign.Pos() <= join.Pos() || assign.Pos() >= call.template.endpoint.Pos() || len(assign.Lhs) != 1 || len(assign.Rhs) != 1 || expression(assign.Lhs[0]) != d.EndpointExpression {
346
+ return true
347
+ }
348
+ if _, ok := assign.Rhs[0].(*ast.CallExpr); ok {
349
+ d.Transforms = append(d.Transforms, catalog.HTTPDestinationJoin{Expression: expression(assign.Rhs[0]), Source: s.source(declaration.file, assign.Pos()).String()})
350
+ }
351
+ return true
352
+ })
353
+ d.BaseURL = &base
354
+ d.Join = &catalog.HTTPDestinationJoin{Expression: expression(join), Source: s.source(declaration.file, join.Pos()).String()}
355
+ if base.Value != "" && strings.HasPrefix(call.Endpoint, base.Value) {
356
+ d.LocalPath = strings.TrimPrefix(call.Endpoint, base.Value)
357
+ parsed, err := url.Parse(call.Endpoint)
358
+ if err == nil && parsed.Host != "" && parsed.Path != "" && strings.HasPrefix(d.LocalPath, "/") {
359
+ d.FullPath = parsed.Path
360
+ host := parsed.Hostname()
361
+ if host != "localhost" && host != "127.0.0.1" {
362
+ d.ServiceDiscoveryAlias = host
363
+ }
364
+ }
365
+ }
366
+ return d
367
+ }
368
+ if parsed, err := url.Parse(call.Endpoint); err == nil && parsed.Host != "" && (parsed.Scheme == "http" || parsed.Scheme == "https") {
369
+ d.FullPath = parsed.Path
370
+ d.ServiceDiscoveryAlias = parsed.Hostname()
371
+ }
372
+ return d
373
+ }