@shortlink-org/portolan 0.2.4 → 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 +109 -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/extract.py +0 -2
- package/plugins/extract-python-kafka/extract_test.py +1 -1
- 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/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 +1 -1
- 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/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 +48 -5
- package/src/catalog-validation.ts +9 -0
- 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/HTTPDestinationEvidence.test.tsx +23 -0
- package/src/components/HTTPDestinationEvidence.tsx +31 -0
- package/src/components/Integrations.tsx +1 -1
- package/src/components/MachineDocs.tsx +6 -5
- package/src/components/MethodRows.tsx +9 -2
- package/src/components/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/data.ts +25 -7
- package/src/enrich.test.ts +332 -1
- package/src/enrich.ts +206 -3
- package/src/flow/StepDetail.tsx +6 -0
- 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/catalog-diff.ts +1 -1
- 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/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/map/ContextMapGraph.tsx +76 -32
- package/src/merge.ts +16 -9
- package/src/pages/AggregatePage.tsx +8 -7
- package/src/pages/ContextPage.tsx +6 -5
- package/src/pages/ServicePage.tsx +4 -3
- package/src/pages/Settings.tsx +189 -41
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- package/src/selection/DetailPanel.tsx +15 -0
- 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,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
|
+
}
|