@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,1067 @@
1
+ package gohttp
2
+
3
+ import (
4
+ "go/ast"
5
+ "go/token"
6
+ "path/filepath"
7
+ "sort"
8
+ "strconv"
9
+ "strings"
10
+ )
11
+
12
+ // Endpoint flows join the two independently provable halves of a request:
13
+ // route -> handler -> interface operation, and factory branch -> concrete
14
+ // implementation -> outbound call. The join is deliberately conservative;
15
+ // a route without all of that evidence remains represented by the ordinary
16
+ // outbound flows instead of acquiring guessed branches.
17
+ func (s *scanner) endpointFlows(groups []FlowGroup) []EndpointFlow {
18
+ groupByFunction := map[string]FlowGroup{}
19
+ for _, group := range groups {
20
+ groupByFunction[group.Function] = group
21
+ }
22
+
23
+ var out []EndpointFlow
24
+ seen := map[string]bool{}
25
+ for _, owner := range s.functions {
26
+ ast.Inspect(owner.fn.Body, func(node ast.Node) bool {
27
+ call, ok := node.(*ast.CallExpr)
28
+ if !ok {
29
+ return true
30
+ }
31
+ method, path, handler, ok := s.routeCall(owner, call)
32
+ if !ok {
33
+ return true
34
+ }
35
+ var handlerKey, operation string
36
+ var factory *functionDecl
37
+ for _, candidate := range s.routeHandlers(owner, handler) {
38
+ candidateOperation, candidateFactory := s.endpointOperation(candidate)
39
+ if candidateOperation != "" && candidateFactory != nil {
40
+ handlerKey, operation, factory = candidate, candidateOperation, candidateFactory
41
+ break
42
+ }
43
+ }
44
+ if operation == "" || factory == nil {
45
+ return true
46
+ }
47
+ branches := s.endpointBranches(factory, operation, groupByFunction)
48
+ if len(branches) == 0 {
49
+ return true
50
+ }
51
+ key := method + "\x00" + path + "\x00" + handlerKey
52
+ if seen[key] {
53
+ return true
54
+ }
55
+ seen[key] = true
56
+ out = append(out, EndpointFlow{
57
+ Method: method, Path: path, Handler: handlerKey,
58
+ Source: s.source(owner.file, call.Pos()), Branches: branches,
59
+ })
60
+ return true
61
+ })
62
+ }
63
+ sort.Slice(out, func(i, j int) bool {
64
+ if out[i].Path != out[j].Path {
65
+ return out[i].Path < out[j].Path
66
+ }
67
+ return out[i].Method < out[j].Method
68
+ })
69
+ return out
70
+ }
71
+
72
+ var routeMethods = map[string]bool{
73
+ "GET": true, "POST": true, "PUT": true, "PATCH": true,
74
+ "DELETE": true, "HEAD": true, "OPTIONS": true,
75
+ }
76
+
77
+ func (s *scanner) routeCall(owner *functionDecl, call *ast.CallExpr) (string, string, ast.Expr, bool) {
78
+ selector, ok := call.Fun.(*ast.SelectorExpr)
79
+ if !ok || len(call.Args) < 2 {
80
+ return "", "", nil, false
81
+ }
82
+ name := selector.Sel.Name
83
+ method := strings.ToUpper(name)
84
+ pathIndex := 0
85
+ if name == "Method" || name == "MethodFunc" {
86
+ if len(call.Args) < 3 {
87
+ return "", "", nil, false
88
+ }
89
+ method = httpMethod(call.Args[0])
90
+ pathIndex = 1
91
+ } else if name == "Handle" || name == "HandleFunc" {
92
+ method = "HTTP"
93
+ } else if !routeMethods[method] {
94
+ return "", "", nil, false
95
+ }
96
+ if receiver, ok := selector.X.(*ast.Ident); ok && owner.file.imports[receiver.Name] == "net/http" && name != "Handle" && name != "HandleFunc" {
97
+ return "", "", nil, false
98
+ }
99
+ literal, ok := call.Args[pathIndex].(*ast.BasicLit)
100
+ if !ok || literal.Kind != token.STRING {
101
+ return "", "", nil, false
102
+ }
103
+ path, err := strconv.Unquote(literal.Value)
104
+ if err != nil || !strings.HasPrefix(path, "/") {
105
+ return "", "", nil, false
106
+ }
107
+ return method, path, call.Args[len(call.Args)-1], true
108
+ }
109
+
110
+ func (s *scanner) routeHandlers(owner *functionDecl, expr ast.Expr) []string {
111
+ if literal, ok := expr.(*ast.FuncLit); ok {
112
+ var targets []string
113
+ ast.Inspect(literal.Body, func(node ast.Node) bool {
114
+ call, ok := node.(*ast.CallExpr)
115
+ if !ok {
116
+ return true
117
+ }
118
+ if target := s.localTarget(owner, call.Fun); target != "" {
119
+ targets = append(targets, target)
120
+ } else if target := s.localVariableMethod(owner, call.Fun); target != "" {
121
+ targets = append(targets, target)
122
+ }
123
+ return true
124
+ })
125
+ return uniqueStrings(targets)
126
+ }
127
+ if call, ok := expr.(*ast.CallExpr); ok {
128
+ if target := s.localTarget(owner, call.Fun); target != "" {
129
+ return []string{target}
130
+ }
131
+ }
132
+ if target := s.localTarget(owner, expr); target != "" {
133
+ return []string{target}
134
+ }
135
+ if target := s.localVariableMethod(owner, expr); target != "" {
136
+ return []string{target}
137
+ }
138
+ return nil
139
+ }
140
+
141
+ type endpointType struct {
142
+ dir string
143
+ name string
144
+ }
145
+
146
+ func (s *scanner) localVariableMethod(owner *functionDecl, expr ast.Expr) string {
147
+ selector, ok := expr.(*ast.SelectorExpr)
148
+ if !ok {
149
+ return ""
150
+ }
151
+ identifier, ok := selector.X.(*ast.Ident)
152
+ if !ok {
153
+ return ""
154
+ }
155
+ types := s.localConcreteTypes(owner)
156
+ typ, ok := types[identifier.Name]
157
+ if !ok {
158
+ return ""
159
+ }
160
+ return s.methodKey(typ, selector.Sel.Name)
161
+ }
162
+
163
+ func (s *scanner) localTypes(owner *functionDecl) map[string]endpointType {
164
+ out := map[string]endpointType{}
165
+ ast.Inspect(owner.fn.Body, func(node ast.Node) bool {
166
+ switch statement := node.(type) {
167
+ case *ast.AssignStmt:
168
+ for index, left := range statement.Lhs {
169
+ name, ok := left.(*ast.Ident)
170
+ if !ok || index >= len(statement.Rhs) {
171
+ continue
172
+ }
173
+ if typ, ok := s.expressionType(owner.file, statement.Rhs[index]); ok {
174
+ out[name.Name] = typ
175
+ }
176
+ }
177
+ case *ast.DeclStmt:
178
+ declaration, ok := statement.Decl.(*ast.GenDecl)
179
+ if !ok {
180
+ return true
181
+ }
182
+ for _, raw := range declaration.Specs {
183
+ spec, ok := raw.(*ast.ValueSpec)
184
+ if !ok || spec.Type == nil {
185
+ continue
186
+ }
187
+ typ, ok := s.typeExpression(owner.file, spec.Type)
188
+ if !ok {
189
+ continue
190
+ }
191
+ for _, name := range spec.Names {
192
+ out[name.Name] = typ
193
+ }
194
+ }
195
+ }
196
+ return true
197
+ })
198
+ return out
199
+ }
200
+
201
+ func (s *scanner) expressionType(file *parsedFile, expr ast.Expr) (endpointType, bool) {
202
+ if literal := compositeLiteral(expr); literal != nil {
203
+ return s.typeExpression(file, literal.Type)
204
+ }
205
+ return endpointType{}, false
206
+ }
207
+
208
+ func (s *scanner) typeExpression(file *parsedFile, expr ast.Expr) (endpointType, bool) {
209
+ switch value := expr.(type) {
210
+ case *ast.StarExpr:
211
+ return s.typeExpression(file, value.X)
212
+ case *ast.Ident:
213
+ return endpointType{dir: file.dir, name: value.Name}, true
214
+ case *ast.SelectorExpr:
215
+ alias, ok := value.X.(*ast.Ident)
216
+ if !ok {
217
+ return endpointType{}, false
218
+ }
219
+ if dir := s.importDirectory(file, alias.Name); dir != "" {
220
+ return endpointType{dir: dir, name: value.Sel.Name}, true
221
+ }
222
+ }
223
+ return endpointType{}, false
224
+ }
225
+
226
+ func (s *scanner) importDirectory(file *parsedFile, alias string) string {
227
+ path := file.imports[alias]
228
+ best := ""
229
+ for _, candidate := range s.files {
230
+ dir := candidate.dir
231
+ if path == dir || strings.HasSuffix(path, "/"+dir) {
232
+ if len(dir) > len(best) {
233
+ best = dir
234
+ }
235
+ }
236
+ }
237
+ return best
238
+ }
239
+
240
+ func (s *scanner) methodKey(typ endpointType, method string) string {
241
+ key := typ.name + "." + method
242
+ if typ.dir != "." && typ.dir != "" {
243
+ key = typ.dir + ":" + key
244
+ }
245
+ if s.functions[key] != nil {
246
+ return key
247
+ }
248
+ return ""
249
+ }
250
+
251
+ func (s *scanner) endpointOperation(handlerKey string) (string, *functionDecl) {
252
+ handler := s.functions[handlerKey]
253
+ if handler == nil {
254
+ return "", nil
255
+ }
256
+ localTypes := s.localTypes(handler)
257
+ var operation string
258
+ var coordinator *functionDecl
259
+ ast.Inspect(handler.fn.Body, func(node ast.Node) bool {
260
+ if operation != "" {
261
+ return false
262
+ }
263
+ call, ok := node.(*ast.CallExpr)
264
+ if !ok {
265
+ return true
266
+ }
267
+ targetKey := s.localTarget(handler, call.Fun)
268
+ target := s.functions[targetKey]
269
+ if target == nil {
270
+ return true
271
+ }
272
+ params := functionParams(target.fn)
273
+ for index, argument := range call.Args {
274
+ if index >= len(params) {
275
+ continue
276
+ }
277
+ typ, ok := s.endpointArgumentType(handler, argument, localTypes)
278
+ if !ok {
279
+ continue
280
+ }
281
+ for _, method := range methodsCalledOn(target.fn, params[index]) {
282
+ concrete := s.functions[s.methodKey(typ, method)]
283
+ if concrete == nil {
284
+ continue
285
+ }
286
+ factory := s.factoryCalledBy(target)
287
+ if factory == nil {
288
+ continue
289
+ }
290
+ for _, candidate := range interfaceOperations(concrete.fn) {
291
+ if s.factorySupportsOperation(factory, candidate) {
292
+ operation, coordinator = candidate, factory
293
+ return false
294
+ }
295
+ }
296
+ }
297
+ }
298
+ return true
299
+ })
300
+ if operation == "" {
301
+ operation, coordinator = s.directEndpointOperation(handler)
302
+ }
303
+ if operation == "" && len(s.typedEdges) > 0 {
304
+ return s.typedEndpointOperation(handlerKey)
305
+ }
306
+ return operation, coordinator
307
+ }
308
+
309
+ // typedEndpointOperation handles coordinators that pass a concrete request
310
+ // through more than one interface-typed helper before invoking a provider.
311
+ // VTA supplies possible callees; the existing factory analysis keeps the
312
+ // provider set and its source conditions precise and explainable.
313
+ func (s *scanner) typedEndpointOperation(handlerKey string) (string, *functionDecl) {
314
+ type visit struct {
315
+ key string
316
+ depth int
317
+ }
318
+ queue := []visit{{key: handlerKey}}
319
+ seen := map[string]bool{}
320
+ depths := map[string]int{}
321
+ var factories []*functionDecl
322
+ for len(queue) > 0 {
323
+ current := queue[0]
324
+ queue = queue[1:]
325
+ if current.depth > 10 || seen[current.key] {
326
+ continue
327
+ }
328
+ seen[current.key] = true
329
+ depths[current.key] = current.depth
330
+ declaration := s.functions[current.key]
331
+ if declaration != nil {
332
+ if factory := s.factoryCalledBy(declaration); factory != nil {
333
+ factories = append(factories, factory)
334
+ }
335
+ }
336
+ for _, edge := range s.typedEdges[current.key] {
337
+ queue = append(queue, visit{key: edge.target, depth: current.depth + 1})
338
+ }
339
+ }
340
+
341
+ type candidate struct {
342
+ operation string
343
+ factory *functionDecl
344
+ depth int
345
+ }
346
+ var candidates []candidate
347
+ for _, factory := range uniqueFunctions(factories) {
348
+ for key, depth := range depths {
349
+ operation := methodName(key)
350
+ if operation != "" && s.factorySupportsOperation(factory, operation) {
351
+ candidates = append(candidates, candidate{operation: operation, factory: factory, depth: depth})
352
+ }
353
+ }
354
+ }
355
+ if len(candidates) == 0 {
356
+ return "", nil
357
+ }
358
+ sort.Slice(candidates, func(i, j int) bool {
359
+ if candidates[i].depth != candidates[j].depth {
360
+ return candidates[i].depth < candidates[j].depth
361
+ }
362
+ if candidates[i].operation != candidates[j].operation {
363
+ return candidates[i].operation < candidates[j].operation
364
+ }
365
+ return candidates[i].factory.key < candidates[j].factory.key
366
+ })
367
+ best := candidates[0]
368
+ for _, candidate := range candidates[1:] {
369
+ if candidate.depth != best.depth {
370
+ break
371
+ }
372
+ if candidate.operation != best.operation || candidate.factory.key != best.factory.key {
373
+ // VTA is deliberately context-insensitive. If several operations are
374
+ // equally close through a shared dispatcher, refusing the join is
375
+ // safer than attaching every provider call to the wrong endpoint.
376
+ return "", nil
377
+ }
378
+ }
379
+ return best.operation, best.factory
380
+ }
381
+
382
+ func methodName(key string) string {
383
+ display := displayFunction(key)
384
+ if at := strings.LastIndex(display, "."); at >= 0 {
385
+ return display[at+1:]
386
+ }
387
+ return ""
388
+ }
389
+
390
+ func uniqueFunctions(in []*functionDecl) []*functionDecl {
391
+ seen := map[string]bool{}
392
+ var out []*functionDecl
393
+ for _, function := range in {
394
+ if function != nil && !seen[function.key] {
395
+ out = append(out, function)
396
+ seen[function.key] = true
397
+ }
398
+ }
399
+ return out
400
+ }
401
+
402
+ func (s *scanner) hasRouteAndProviderFactory() bool {
403
+ hasFactory := false
404
+ for _, declaration := range s.functions {
405
+ if len(s.factoryCases(declaration)) > 0 {
406
+ hasFactory = true
407
+ break
408
+ }
409
+ }
410
+ if !hasFactory {
411
+ return false
412
+ }
413
+ for _, owner := range s.functions {
414
+ found := false
415
+ ast.Inspect(owner.fn.Body, func(node ast.Node) bool {
416
+ call, ok := node.(*ast.CallExpr)
417
+ if ok {
418
+ _, _, _, found = s.routeCall(owner, call)
419
+ }
420
+ return !found
421
+ })
422
+ if found {
423
+ return true
424
+ }
425
+ }
426
+ return false
427
+ }
428
+
429
+ func (s *scanner) endpointArgumentType(owner *functionDecl, expr ast.Expr, locals map[string]endpointType) (endpointType, bool) {
430
+ switch value := expr.(type) {
431
+ case *ast.Ident:
432
+ typ, ok := locals[value.Name]
433
+ return typ, ok
434
+ case *ast.ParenExpr:
435
+ return s.endpointArgumentType(owner, value.X, locals)
436
+ case *ast.UnaryExpr:
437
+ return s.endpointArgumentType(owner, value.X, locals)
438
+ case *ast.CompositeLit:
439
+ return s.typeExpression(owner.file, value.Type)
440
+ }
441
+ return endpointType{}, false
442
+ }
443
+
444
+ func (s *scanner) directEndpointOperation(handler *functionDecl) (string, *functionDecl) {
445
+ factory := s.factoryCalledBy(handler)
446
+ if factory == nil {
447
+ return "", nil
448
+ }
449
+ factoryResult := map[string]bool{}
450
+ ast.Inspect(handler.fn.Body, func(node ast.Node) bool {
451
+ assignment, ok := node.(*ast.AssignStmt)
452
+ if !ok || len(assignment.Rhs) == 0 {
453
+ return true
454
+ }
455
+ if call, ok := assignment.Rhs[0].(*ast.CallExpr); ok && s.isFactoryCall(handler, call, factory) {
456
+ if name, ok := assignment.Lhs[0].(*ast.Ident); ok {
457
+ factoryResult[name.Name] = true
458
+ }
459
+ }
460
+ propagateAliases(assignment, factoryResult)
461
+ return true
462
+ })
463
+ var operation string
464
+ ast.Inspect(handler.fn.Body, func(node ast.Node) bool {
465
+ call, ok := node.(*ast.CallExpr)
466
+ if !ok {
467
+ return true
468
+ }
469
+ selector, ok := call.Fun.(*ast.SelectorExpr)
470
+ if !ok {
471
+ return true
472
+ }
473
+ if expressionUsesAlias(selector.X, factoryResult) {
474
+ operation = selector.Sel.Name
475
+ return false
476
+ }
477
+ return true
478
+ })
479
+ if operation == "" {
480
+ return "", nil
481
+ }
482
+ return operation, factory
483
+ }
484
+
485
+ func (s *scanner) isFactoryCall(owner *functionDecl, call *ast.CallExpr, factory *functionDecl) bool {
486
+ if s.localTarget(owner, call.Fun) == factory.key {
487
+ return true
488
+ }
489
+ selector, ok := call.Fun.(*ast.SelectorExpr)
490
+ if !ok {
491
+ return false
492
+ }
493
+ factoryName := displayFunction(factory.key)
494
+ if at := strings.LastIndex(factoryName, "."); at >= 0 {
495
+ factoryName = factoryName[at+1:]
496
+ }
497
+ return selector.Sel.Name == factoryName
498
+ }
499
+
500
+ func methodsCalledOn(fn *ast.FuncDecl, variable string) []string {
501
+ var methods []string
502
+ ast.Inspect(fn.Body, func(node ast.Node) bool {
503
+ call, ok := node.(*ast.CallExpr)
504
+ if !ok {
505
+ return true
506
+ }
507
+ selector, ok := call.Fun.(*ast.SelectorExpr)
508
+ if !ok {
509
+ return true
510
+ }
511
+ root, ok := selector.X.(*ast.Ident)
512
+ if ok && root.Name == variable {
513
+ methods = append(methods, selector.Sel.Name)
514
+ }
515
+ return true
516
+ })
517
+ return uniqueStrings(methods)
518
+ }
519
+
520
+ func interfaceOperations(fn *ast.FuncDecl) []string {
521
+ aliases := map[string]bool{}
522
+ if fn.Type.Params != nil {
523
+ for _, field := range fn.Type.Params.List {
524
+ for _, name := range field.Names {
525
+ aliases[name.Name] = true
526
+ }
527
+ }
528
+ }
529
+ var operations []string
530
+ ast.Inspect(fn.Body, func(node ast.Node) bool {
531
+ if assignment, ok := node.(*ast.AssignStmt); ok {
532
+ propagateAliases(assignment, aliases)
533
+ return true
534
+ }
535
+ call, ok := node.(*ast.CallExpr)
536
+ if !ok {
537
+ return true
538
+ }
539
+ selector, ok := call.Fun.(*ast.SelectorExpr)
540
+ if !ok {
541
+ return true
542
+ }
543
+ if expressionUsesAlias(selector.X, aliases) && selector.Sel.Name != "Error" {
544
+ operations = append(operations, selector.Sel.Name)
545
+ }
546
+ return true
547
+ })
548
+ return uniqueStrings(operations)
549
+ }
550
+
551
+ // propagateAliases follows the value-preserving assembly shapes used for
552
+ // capability narrowing: alias := value and capability, ok := value.(Port).
553
+ // It does not follow arbitrary calls or selectors, which could change identity.
554
+ func propagateAliases(assignment *ast.AssignStmt, aliases map[string]bool) {
555
+ for index, left := range assignment.Lhs {
556
+ name, ok := left.(*ast.Ident)
557
+ if !ok {
558
+ continue
559
+ }
560
+ rightAt := index
561
+ if len(assignment.Rhs) == 1 {
562
+ if index > 0 {
563
+ continue
564
+ }
565
+ rightAt = 0
566
+ }
567
+ if rightAt >= len(assignment.Rhs) || !expressionUsesAlias(assignment.Rhs[rightAt], aliases) {
568
+ continue
569
+ }
570
+ aliases[name.Name] = true
571
+ }
572
+ }
573
+
574
+ func expressionUsesAlias(expr ast.Expr, aliases map[string]bool) bool {
575
+ switch value := expr.(type) {
576
+ case *ast.Ident:
577
+ return aliases[value.Name]
578
+ case *ast.ParenExpr:
579
+ return expressionUsesAlias(value.X, aliases)
580
+ case *ast.UnaryExpr:
581
+ return expressionUsesAlias(value.X, aliases)
582
+ case *ast.TypeAssertExpr:
583
+ return expressionUsesAlias(value.X, aliases)
584
+ }
585
+ return false
586
+ }
587
+
588
+ func (s *scanner) factorySupportsOperation(factory *functionDecl, operation string) bool {
589
+ for _, branch := range s.factoryCases(factory) {
590
+ if s.providerOperation(branch.provider, operation, map[string]bool{}) != "" {
591
+ return true
592
+ }
593
+ }
594
+ return false
595
+ }
596
+
597
+ func (s *scanner) factoryCalledBy(coordinator *functionDecl) *functionDecl {
598
+ var candidates []string
599
+ ast.Inspect(coordinator.fn.Body, func(node ast.Node) bool {
600
+ call, ok := node.(*ast.CallExpr)
601
+ if !ok {
602
+ return true
603
+ }
604
+ if target := s.localTarget(coordinator, call.Fun); target != "" {
605
+ candidates = append(candidates, target)
606
+ }
607
+ if selector, ok := call.Fun.(*ast.SelectorExpr); ok {
608
+ candidates = append(candidates, s.methods[selector.Sel.Name]...)
609
+ }
610
+ return true
611
+ })
612
+ for _, key := range uniqueStrings(candidates) {
613
+ declaration := s.functions[key]
614
+ if declaration != nil && len(s.factoryCases(declaration)) > 0 {
615
+ return declaration
616
+ }
617
+ }
618
+ return nil
619
+ }
620
+
621
+ type factoryCase struct {
622
+ condition string
623
+ provider string
624
+ }
625
+
626
+ func (s *scanner) factoryCases(factory *functionDecl) []factoryCase {
627
+ var out []factoryCase
628
+ ast.Inspect(factory.fn.Body, func(node ast.Node) bool {
629
+ clause, ok := node.(*ast.CaseClause)
630
+ if !ok || len(clause.List) == 0 {
631
+ return true
632
+ }
633
+ provider := s.providerOfCase(factory.file, clause)
634
+ if provider == "" {
635
+ return true
636
+ }
637
+ for _, expression := range clause.List {
638
+ condition := s.value(factory.file, expression, map[string]string{}, map[string]bool{})
639
+ if condition != "" {
640
+ out = append(out, factoryCase{condition: condition, provider: provider})
641
+ }
642
+ }
643
+ return true
644
+ })
645
+ if len(out) > 0 {
646
+ return out
647
+ }
648
+ if mapped := s.factoryMapCases(factory); len(mapped) > 0 {
649
+ return mapped
650
+ }
651
+ providers := s.returnedProviders(factory)
652
+ if len(providers) != 1 {
653
+ return nil
654
+ }
655
+ return []factoryCase{{condition: filepath.Base(providers[0]), provider: providers[0]}}
656
+ }
657
+
658
+ func (s *scanner) factoryMapCases(factory *functionDecl) []factoryCase {
659
+ used := map[string]bool{}
660
+ ast.Inspect(factory.fn.Body, func(node ast.Node) bool {
661
+ ret, ok := node.(*ast.ReturnStmt)
662
+ if !ok {
663
+ return true
664
+ }
665
+ for _, result := range ret.Results {
666
+ if name := indexedMapName(result); name != "" {
667
+ used[name] = true
668
+ }
669
+ }
670
+ return true
671
+ })
672
+ if len(used) == 0 {
673
+ return nil
674
+ }
675
+
676
+ literals := map[string]*ast.CompositeLit{}
677
+ record := func(name string, expr ast.Expr) {
678
+ literal := compositeLiteral(expr)
679
+ if literal == nil {
680
+ return
681
+ }
682
+ if _, ok := literal.Type.(*ast.MapType); ok && used[name] {
683
+ literals[name] = literal
684
+ }
685
+ }
686
+ ast.Inspect(factory.fn.Body, func(node ast.Node) bool {
687
+ switch value := node.(type) {
688
+ case *ast.AssignStmt:
689
+ for index, left := range value.Lhs {
690
+ name, ok := left.(*ast.Ident)
691
+ if ok && index < len(value.Rhs) {
692
+ record(name.Name, value.Rhs[index])
693
+ }
694
+ }
695
+ case *ast.ValueSpec:
696
+ for index, name := range value.Names {
697
+ if index < len(value.Values) {
698
+ record(name.Name, value.Values[index])
699
+ }
700
+ }
701
+ }
702
+ return true
703
+ })
704
+ for _, declaration := range factory.file.node.Decls {
705
+ generic, ok := declaration.(*ast.GenDecl)
706
+ if !ok || generic.Tok != token.VAR {
707
+ continue
708
+ }
709
+ for _, raw := range generic.Specs {
710
+ spec, ok := raw.(*ast.ValueSpec)
711
+ if !ok {
712
+ continue
713
+ }
714
+ for index, name := range spec.Names {
715
+ if index < len(spec.Values) {
716
+ record(name.Name, spec.Values[index])
717
+ }
718
+ }
719
+ }
720
+ }
721
+
722
+ var out []factoryCase
723
+ seen := map[string]bool{}
724
+ for _, literal := range literals {
725
+ for _, element := range literal.Elts {
726
+ pair, ok := element.(*ast.KeyValueExpr)
727
+ if !ok {
728
+ continue
729
+ }
730
+ condition := s.value(factory.file, pair.Key, map[string]string{}, map[string]bool{})
731
+ provider := s.providerExpression(factory.file, pair.Value, map[string]string{})
732
+ key := condition + "\x00" + provider
733
+ if condition == "" || provider == "" || seen[key] {
734
+ continue
735
+ }
736
+ seen[key] = true
737
+ out = append(out, factoryCase{condition: condition, provider: provider})
738
+ }
739
+ }
740
+ sort.Slice(out, func(i, j int) bool { return out[i].condition < out[j].condition })
741
+ return out
742
+ }
743
+
744
+ func indexedMapName(expr ast.Expr) string {
745
+ switch value := expr.(type) {
746
+ case *ast.ParenExpr:
747
+ return indexedMapName(value.X)
748
+ case *ast.UnaryExpr:
749
+ return indexedMapName(value.X)
750
+ case *ast.CallExpr:
751
+ return indexedMapName(value.Fun)
752
+ case *ast.IndexExpr:
753
+ if name, ok := value.X.(*ast.Ident); ok {
754
+ return name.Name
755
+ }
756
+ }
757
+ return ""
758
+ }
759
+
760
+ // returnedProviders recognizes a fixed factory without manufacturing branches:
761
+ // a single local provider is returned, possibly through a temporary variable.
762
+ // Two distinct returns are ambiguous unless a switch supplied their conditions,
763
+ // so this conservative fallback rejects them.
764
+ func (s *scanner) returnedProviders(factory *functionDecl) []string {
765
+ locals := map[string]string{}
766
+ var providers []string
767
+ ast.Inspect(factory.fn.Body, func(node ast.Node) bool {
768
+ switch value := node.(type) {
769
+ case *ast.AssignStmt:
770
+ for index, left := range value.Lhs {
771
+ name, ok := left.(*ast.Ident)
772
+ if !ok {
773
+ continue
774
+ }
775
+ rightAt := index
776
+ if len(value.Rhs) == 1 {
777
+ rightAt = 0
778
+ }
779
+ if rightAt >= len(value.Rhs) {
780
+ continue
781
+ }
782
+ if provider := s.providerExpression(factory.file, value.Rhs[rightAt], locals); provider != "" {
783
+ locals[name.Name] = provider
784
+ }
785
+ }
786
+ case *ast.ReturnStmt:
787
+ for _, result := range value.Results {
788
+ if provider := s.providerExpression(factory.file, result, locals); provider != "" {
789
+ providers = append(providers, provider)
790
+ }
791
+ }
792
+ }
793
+ return true
794
+ })
795
+ return uniqueStrings(providers)
796
+ }
797
+
798
+ func (s *scanner) providerOfCase(file *parsedFile, clause *ast.CaseClause) string {
799
+ locals := map[string]string{}
800
+ for _, statement := range clause.Body {
801
+ ast.Inspect(statement, func(node ast.Node) bool {
802
+ assignment, ok := node.(*ast.AssignStmt)
803
+ if !ok {
804
+ return true
805
+ }
806
+ for index, left := range assignment.Lhs {
807
+ name, ok := left.(*ast.Ident)
808
+ if !ok || index >= len(assignment.Rhs) {
809
+ continue
810
+ }
811
+ if provider := s.providerExpression(file, assignment.Rhs[index], locals); provider != "" {
812
+ locals[name.Name] = provider
813
+ }
814
+ }
815
+ return true
816
+ })
817
+ }
818
+ var provider string
819
+ for _, statement := range clause.Body {
820
+ ast.Inspect(statement, func(node ast.Node) bool {
821
+ if provider != "" {
822
+ return false
823
+ }
824
+ ret, ok := node.(*ast.ReturnStmt)
825
+ if !ok {
826
+ return true
827
+ }
828
+ for _, result := range ret.Results {
829
+ if candidate := s.providerExpression(file, result, locals); candidate != "" {
830
+ provider = candidate
831
+ return false
832
+ }
833
+ }
834
+ return true
835
+ })
836
+ if provider != "" {
837
+ return provider
838
+ }
839
+ }
840
+ return ""
841
+ }
842
+
843
+ func (s *scanner) providerExpression(file *parsedFile, expr ast.Expr, locals map[string]string) string {
844
+ switch value := expr.(type) {
845
+ case *ast.Ident:
846
+ return locals[value.Name]
847
+ case *ast.UnaryExpr:
848
+ return s.providerExpression(file, value.X, locals)
849
+ case *ast.CompositeLit:
850
+ if selector, ok := value.Type.(*ast.SelectorExpr); ok {
851
+ if alias, ok := selector.X.(*ast.Ident); ok {
852
+ return s.importDirectory(file, alias.Name)
853
+ }
854
+ }
855
+ case *ast.CallExpr:
856
+ if selector, ok := value.Fun.(*ast.SelectorExpr); ok {
857
+ if alias, ok := selector.X.(*ast.Ident); ok {
858
+ return s.importDirectory(file, alias.Name)
859
+ }
860
+ }
861
+ case *ast.SelectorExpr:
862
+ if alias, ok := value.X.(*ast.Ident); ok {
863
+ return s.importDirectory(file, alias.Name)
864
+ }
865
+ case *ast.FuncLit:
866
+ var providers []string
867
+ ast.Inspect(value.Body, func(node ast.Node) bool {
868
+ ret, ok := node.(*ast.ReturnStmt)
869
+ if !ok {
870
+ return true
871
+ }
872
+ for _, result := range ret.Results {
873
+ if provider := s.providerExpression(file, result, locals); provider != "" {
874
+ providers = append(providers, provider)
875
+ }
876
+ }
877
+ return true
878
+ })
879
+ providers = uniqueStrings(providers)
880
+ if len(providers) == 1 {
881
+ return providers[0]
882
+ }
883
+ }
884
+ return ""
885
+ }
886
+
887
+ func (s *scanner) endpointBranches(factory *functionDecl, operation string, groups map[string]FlowGroup) []EndpointBranch {
888
+ var out []EndpointBranch
889
+ seen := map[string]bool{}
890
+ for _, branch := range s.factoryCases(factory) {
891
+ function := s.providerOperation(branch.provider, operation, map[string]bool{})
892
+ if function == "" {
893
+ continue
894
+ }
895
+ group := s.typedOutboundGroup(function, groups)
896
+ if len(group.Calls) == 0 {
897
+ if descendant, ok := providerOutboundGroup(branch.provider, operation, groups); ok {
898
+ group = descendant
899
+ function = descendant.Function
900
+ }
901
+ }
902
+ key := branch.condition + "\x00" + function
903
+ if seen[key] {
904
+ continue
905
+ }
906
+ seen[key] = true
907
+ out = append(out, EndpointBranch{
908
+ Condition: branch.condition, Provider: branch.provider,
909
+ Operation: operation, Function: function, Calls: group.Calls,
910
+ Source: s.source(s.functions[function].file, s.functions[function].fn.Pos()),
911
+ })
912
+ }
913
+ sort.Slice(out, func(i, j int) bool { return out[i].Condition < out[j].Condition })
914
+ return out
915
+ }
916
+
917
+ // typedOutboundGroup follows dynamic calls only after a concrete provider
918
+ // branch has been chosen. Keeping VTA out of the shared coordinator graph is
919
+ // important: VTA is context-insensitive, so a common ActionFlow may otherwise
920
+ // appear to call every Requester implementation from every endpoint.
921
+ func (s *scanner) typedOutboundGroup(root string, groups map[string]FlowGroup) FlowGroup {
922
+ base := groups[root]
923
+ if base.Function == "" {
924
+ base.Function = root
925
+ if declaration := s.functions[root]; declaration != nil {
926
+ base.Source = s.source(declaration.file, declaration.fn.Pos())
927
+ }
928
+ }
929
+ if len(s.typedEdges) == 0 {
930
+ return base
931
+ }
932
+
933
+ type visit struct {
934
+ key string
935
+ path []string
936
+ depth int
937
+ }
938
+ queue := []visit{{key: root}}
939
+ seenDepth := map[string]int{}
940
+ calls := append([]Call(nil), base.Calls...)
941
+ for len(queue) > 0 && len(seenDepth) < 256 {
942
+ current := queue[0]
943
+ queue = queue[1:]
944
+ if current.depth > 8 {
945
+ continue
946
+ }
947
+ if depth, seen := seenDepth[current.key]; seen && depth <= current.depth {
948
+ continue
949
+ }
950
+ seenDepth[current.key] = current.depth
951
+ path := appendCopy(current.path, displayFunction(current.key))
952
+ if current.key != root {
953
+ if descendant, ok := groups[current.key]; ok {
954
+ for _, call := range descendant.Calls {
955
+ copy := call
956
+ copy.Chain = joinCallChains(path, call.Chain)
957
+ calls = append(calls, copy)
958
+ }
959
+ }
960
+ }
961
+ for _, edge := range s.typedEdges[current.key] {
962
+ queue = append(queue, visit{key: edge.target, path: path, depth: current.depth + 1})
963
+ }
964
+ }
965
+ base.Calls = uniqueFlowCalls(calls)
966
+ return base
967
+ }
968
+
969
+ func joinCallChains(prefix, suffix []string) []string {
970
+ out := append([]string(nil), prefix...)
971
+ for _, item := range suffix {
972
+ if len(out) == 0 || out[len(out)-1] != item {
973
+ out = append(out, item)
974
+ }
975
+ }
976
+ return out
977
+ }
978
+
979
+ func providerOutboundGroup(provider, operation string, groups map[string]FlowGroup) (FlowGroup, bool) {
980
+ var candidates []FlowGroup
981
+ for key, group := range groups {
982
+ dir, display, qualified := strings.Cut(key, ":")
983
+ if !qualified || (dir != provider && !strings.HasPrefix(dir, provider+"/")) {
984
+ continue
985
+ }
986
+ if strings.HasSuffix(display, "."+operation) || display == operation {
987
+ candidates = append(candidates, group)
988
+ }
989
+ }
990
+ if len(candidates) == 0 {
991
+ return FlowGroup{}, false
992
+ }
993
+ sort.Slice(candidates, func(i, j int) bool {
994
+ leftDir, _, _ := strings.Cut(candidates[i].Function, ":")
995
+ rightDir, _, _ := strings.Cut(candidates[j].Function, ":")
996
+ if len(leftDir) != len(rightDir) {
997
+ return len(leftDir) < len(rightDir)
998
+ }
999
+ return candidates[i].Function < candidates[j].Function
1000
+ })
1001
+ return candidates[0], true
1002
+ }
1003
+
1004
+ func (s *scanner) providerOperation(provider, operation string, visiting map[string]bool) string {
1005
+ visitKey := provider + ":" + operation
1006
+ if visiting[visitKey] {
1007
+ return ""
1008
+ }
1009
+ visiting[visitKey] = true
1010
+ defer delete(visiting, visitKey)
1011
+
1012
+ var candidates []string
1013
+ for key := range s.functions {
1014
+ dir, display, qualified := strings.Cut(key, ":")
1015
+ if !qualified || dir != provider {
1016
+ continue
1017
+ }
1018
+ if strings.HasSuffix(display, "."+operation) || display == operation {
1019
+ candidates = append(candidates, key)
1020
+ }
1021
+ }
1022
+ if len(candidates) > 0 {
1023
+ sort.Strings(candidates)
1024
+ return candidates[0]
1025
+ }
1026
+
1027
+ // Go promotes methods from embedded fields. Following those fields lets a
1028
+ // factory branch such as aeroflot.New(*websky.Connector) inherit Search
1029
+ // without pretending that aeroflot declares its own implementation.
1030
+ for _, file := range s.files {
1031
+ if file.dir != provider {
1032
+ continue
1033
+ }
1034
+ for _, declaration := range file.node.Decls {
1035
+ generic, ok := declaration.(*ast.GenDecl)
1036
+ if !ok || generic.Tok != token.TYPE {
1037
+ continue
1038
+ }
1039
+ for _, raw := range generic.Specs {
1040
+ spec, ok := raw.(*ast.TypeSpec)
1041
+ if !ok {
1042
+ continue
1043
+ }
1044
+ structure, ok := spec.Type.(*ast.StructType)
1045
+ if !ok {
1046
+ continue
1047
+ }
1048
+ for _, field := range structure.Fields.List {
1049
+ if len(field.Names) != 0 {
1050
+ continue
1051
+ }
1052
+ typ, ok := s.typeExpression(file, field.Type)
1053
+ if !ok {
1054
+ continue
1055
+ }
1056
+ if key := s.methodKey(typ, operation); key != "" {
1057
+ return key
1058
+ }
1059
+ if key := s.providerOperation(typ.dir, operation, visiting); key != "" {
1060
+ return key
1061
+ }
1062
+ }
1063
+ }
1064
+ }
1065
+ }
1066
+ return ""
1067
+ }