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