@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.
- package/README.md +23 -0
- package/catalog/enum_test.go +46 -0
- package/catalog/evidence_test.go +35 -0
- package/catalog/model.go +1066 -0
- package/catalog/roundtrip_test.go +203 -0
- package/catalog/via_test.go +38 -0
- package/cli/init.test.mjs +6 -1
- package/cli/portolan.mjs +8 -0
- package/cli/portolan.test.mjs +49 -0
- package/go.mod +14 -0
- package/go.sum +20 -0
- package/internal/gocall/README.md +19 -0
- package/internal/gocall/analyze.go +189 -0
- package/internal/gocall/analyze_test.go +107 -0
- package/internal/gohttp/analyze.go +2562 -0
- package/internal/gohttp/destination.go +373 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +96 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/index.go +629 -0
- package/internal/goscan/index_test.go +66 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +37 -0
- package/internal/goscan/tree.go +284 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +70 -0
- package/internal/wsdl/parse.go +949 -0
- package/internal/wsdl/parse_test.go +170 -0
- package/package.json +22 -10
- package/plugin/describe.go +118 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +141 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +114 -46
- package/plugins/cmd/portolan-http-clients/main.go +19 -0
- package/plugins/extract-celery/extract.py +0 -2
- package/plugins/extract-celery/extract_test.py +1 -1
- package/plugins/extract-django/README.md +39 -17
- package/plugins/extract-django/domain.py +28 -17
- package/plugins/extract-django/extract.py +21 -7
- package/plugins/extract-django/extract_test.py +55 -2
- package/plugins/extract-django/lifecycle.py +2 -0
- package/plugins/extract-django/operations.py +1 -1
- package/plugins/extract-django/routing_test.py +109 -1
- package/plugins/extract-django/store.py +1 -1
- package/plugins/extract-django/transport.py +101 -55
- package/plugins/extract-django/verbs.py +241 -0
- package/plugins/extract-go/README.md +47 -0
- package/plugins/extract-http-clients/describe.go +19 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +740 -0
- package/plugins/extract-http-clients/extract_test.go +1561 -0
- package/plugins/extract-http-clients/main.go +41 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-python-kafka/README.md +6 -0
- package/plugins/extract-python-kafka/extract.py +2 -4
- package/plugins/extract-python-kafka/extract_test.py +18 -2
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/extract.ts +4 -5
- package/plugins/extract-ts/graphql.test.ts +1 -1
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/catalog.py +12 -1
- package/plugins/pyplugin/kafka.py +74 -3
- package/plugins/pyplugin/protocol.py +1 -5
- package/portolan.json +3 -2
- package/schema/portolan.schema.json +34 -0
- package/scripts/README.md +18 -12
- package/scripts/catalog-sources.mjs +6 -0
- package/scripts/delivery-presets.mjs +21 -11
- package/scripts/diff.mjs +5 -1
- package/scripts/django-aggregates.test.mjs +58 -0
- package/scripts/gen-likec4.mjs +79 -21
- package/scripts/gen-likec4.test.mjs +25 -2
- package/scripts/gen.mjs +118 -115
- package/scripts/go-discovery.test.mjs +30 -0
- package/scripts/history.mjs +186 -3
- package/scripts/history.test.mjs +1 -1
- package/scripts/host-plugins/fetch-git.mjs +77 -21
- package/scripts/host-plugins/fetch-git.test.mjs +62 -8
- package/scripts/local-api.mjs +71 -4
- package/scripts/local-api.test.mjs +63 -4
- package/scripts/local-discovery.mjs +82 -9
- package/scripts/manifest.mjs +5 -3
- package/scripts/manifest.test.mjs +24 -0
- package/scripts/output-diff.mjs +94 -0
- package/scripts/output-diff.test.mjs +36 -0
- package/scripts/package-smoke.mjs +62 -4
- package/scripts/plugin-host.mjs +22 -2
- package/scripts/plugin-host.test.mjs +9 -0
- package/scripts/plugin-wasm-worker.mjs +4 -1
- package/scripts/provenance.mjs +72 -0
- package/scripts/provenance.test.mjs +149 -0
- package/scripts/run-builtin.mjs +39 -5
- package/scripts/schema.mjs +29 -0
- package/scripts/warning-policy.mjs +161 -0
- package/scripts/warning-policy.test.mjs +56 -0
- package/src/app/Breadcrumbs.test.ts +4 -0
- package/src/app/Breadcrumbs.tsx +1 -0
- package/src/app/Sidebar.tsx +3 -3
- package/src/catalog-docs.test.ts +64 -0
- package/src/catalog-docs.ts +35 -0
- package/src/catalog-error.test.ts +15 -0
- package/src/catalog-model.ts +70 -6
- package/src/catalog-stores.test.ts +17 -0
- package/src/catalog-validation.ts +52 -2
- package/src/catalog.test.ts +13 -2
- package/src/chat/Starter.tsx +5 -11
- package/src/chat/tools.test.ts +27 -0
- package/src/chat/tools.ts +5 -9
- package/src/components/CatalogStamp.tsx +10 -8
- package/src/components/ChannelRows.messagepack.test.tsx +28 -0
- package/src/components/ChannelRows.test.tsx +54 -0
- package/src/components/ChannelRows.tsx +57 -10
- package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
- package/src/components/HTTPDestinationEvidence.tsx +31 -0
- package/src/components/Integrations.tsx +1 -1
- package/src/components/LifecycleDiagram.tsx +28 -12
- package/src/components/MachineDocs.tsx +6 -5
- package/src/components/MethodRows.tsx +9 -2
- package/src/components/ProblemRow.tsx +4 -0
- package/src/components/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/components/WhatLinksHere.tsx +6 -4
- package/src/data.ts +25 -7
- package/src/enrich.test.ts +336 -6
- package/src/enrich.ts +206 -3
- package/src/flow/StepDetail.tsx +104 -54
- package/src/flow/answers.test.ts +18 -1
- package/src/flow/answers.ts +37 -8
- package/src/flow/evidence.test.ts +16 -0
- package/src/flow/evidence.ts +34 -0
- package/src/index.css +44 -0
- package/src/landing/DraggableReveal.tsx +3 -2
- package/src/landing/EvidencePipeline.tsx +105 -0
- package/src/landing/LandingPage.tsx +2 -59
- package/src/lib/backlinks.test.ts +16 -1
- package/src/lib/backlinks.ts +20 -0
- package/src/lib/catalog-diff.test.ts +18 -0
- package/src/lib/catalog-diff.ts +20 -2
- package/src/lib/derive.ts +1 -0
- package/src/lib/django-aggregates.d.mts +9 -0
- package/src/lib/django-aggregates.mjs +36 -0
- package/src/lib/django-aggregates.test.ts +29 -0
- package/src/lib/django-aggregates.ts +5 -0
- package/src/lib/kafka-ui.test.ts +87 -0
- package/src/lib/kafka-ui.ts +105 -0
- package/src/lib/local-api.ts +20 -2
- package/src/lib/setup-info.test.ts +17 -0
- package/src/lib/setup-info.ts +58 -0
- package/src/lib/warnings.test.ts +54 -0
- package/src/lib/warnings.ts +260 -0
- package/src/lib/wire-problems.test.ts +21 -0
- package/src/lib/wire-problems.ts +62 -1
- package/src/likec4/FlowView.tsx +2 -6
- package/src/likec4/flow-edges.test.ts +64 -1
- package/src/likec4/flow-edges.ts +43 -7
- package/src/likec4/view-index.ts +8 -2
- package/src/map/ContextMapGraph.tsx +76 -32
- package/src/merge.test.ts +23 -0
- package/src/merge.ts +33 -10
- package/src/pages/AggregatePage.tsx +8 -7
- package/src/pages/CatalogFailure.tsx +2 -2
- package/src/pages/ContextPage.tsx +6 -5
- package/src/pages/ServicePage.tsx +4 -3
- package/src/pages/Settings.tsx +200 -44
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- package/src/pages/settings/IntegrationsSettings.tsx +117 -0
- package/src/routes.test.ts +2 -0
- package/src/routes.ts +2 -1
- package/src/selection/DetailPanel.tsx +61 -1
- package/src/virtual-provenance.d.ts +11 -0
- package/vite.config.ts +5 -0
- package/scripts/vendor-lock.mjs +0 -58
- 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
|
+
}
|