@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,320 @@
|
|
|
1
|
+
package gohttp
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"go/ast"
|
|
5
|
+
"sort"
|
|
6
|
+
"strings"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// rootFlows discovers execution roots that do not need the provider-factory
|
|
10
|
+
// join used by endpointFlows. Every root still needs an outbound FlowGroup;
|
|
11
|
+
// registering a handler alone is not enough to manufacture a flow.
|
|
12
|
+
func (s *scanner) rootFlows(groups []FlowGroup, providerEndpoints []EndpointFlow) []RootFlow {
|
|
13
|
+
byFunction := map[string]FlowGroup{}
|
|
14
|
+
for _, group := range groups {
|
|
15
|
+
byFunction[group.Function] = group
|
|
16
|
+
}
|
|
17
|
+
providerRoutes := map[string]bool{}
|
|
18
|
+
for _, endpoint := range providerEndpoints {
|
|
19
|
+
providerRoutes[routeKey(endpoint.Method, endpoint.Path)] = true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var out []RootFlow
|
|
23
|
+
rootedHandlers := map[string]bool{}
|
|
24
|
+
rootedRoutes := map[string]bool{}
|
|
25
|
+
registrations := s.handlerRegistrations()
|
|
26
|
+
for _, owner := range s.sortedFunctions() {
|
|
27
|
+
ast.Inspect(owner.fn.Body, func(node ast.Node) bool {
|
|
28
|
+
call, ok := node.(*ast.CallExpr)
|
|
29
|
+
if !ok {
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
method, path, handler, ok := s.routeCall(owner, call)
|
|
33
|
+
if !ok || providerRoutes[routeKey(method, path)] || rootedRoutes[routeKey(method, path)] {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
root := RootFlow{
|
|
37
|
+
Kind: RootHTTP, Method: method, Path: path,
|
|
38
|
+
Label: strings.TrimSpace(method + " " + path), Confidence: "high",
|
|
39
|
+
Source: s.source(owner.file, call.Pos()),
|
|
40
|
+
}
|
|
41
|
+
for _, candidate := range s.routeHandlers(owner, handler) {
|
|
42
|
+
group, found := byFunction[candidate]
|
|
43
|
+
if !found || len(group.Calls) == 0 {
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if root.Handler == "" {
|
|
47
|
+
root.Handler = candidate
|
|
48
|
+
}
|
|
49
|
+
root.Calls = append(root.Calls, group.Calls...)
|
|
50
|
+
root.Covered = append(root.Covered, candidate)
|
|
51
|
+
rootedHandlers[candidate] = true
|
|
52
|
+
}
|
|
53
|
+
root.Calls = uniqueFlowCalls(root.Calls)
|
|
54
|
+
root.Covered = uniqueStrings(root.Covered)
|
|
55
|
+
if len(root.Calls) == 0 {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
rootedRoutes[routeKey(method, path)] = true
|
|
59
|
+
out = append(out, root)
|
|
60
|
+
return true
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for _, owner := range s.sortedFunctions() {
|
|
65
|
+
ast.Inspect(owner.fn.Body, func(node ast.Node) bool {
|
|
66
|
+
call, ok := node.(*ast.CallExpr)
|
|
67
|
+
if !ok {
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
schedule, handler, ok := s.scheduledRegistration(owner, call)
|
|
71
|
+
if !ok {
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
for _, candidate := range s.routeHandlers(owner, handler) {
|
|
75
|
+
group, found := byFunction[candidate]
|
|
76
|
+
if !found || len(group.Calls) == 0 {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
label := "Schedule " + schedule + " → " + displayFunction(candidate)
|
|
80
|
+
out = append(out, RootFlow{
|
|
81
|
+
Kind: RootScheduled, Handler: candidate, Label: label, Confidence: "high",
|
|
82
|
+
Source: s.source(owner.file, call.Pos()), Calls: group.Calls,
|
|
83
|
+
Covered: []string{candidate},
|
|
84
|
+
})
|
|
85
|
+
rootedHandlers[candidate] = true
|
|
86
|
+
}
|
|
87
|
+
return true
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Swagger-style route annotations are useful for handler factories and
|
|
92
|
+
// custom registries whose final path is assembled by framework code. They
|
|
93
|
+
// are medium-confidence evidence: a concrete registration above wins when
|
|
94
|
+
// both point to the same source function.
|
|
95
|
+
for _, declaration := range s.sortedFunctions() {
|
|
96
|
+
if rootedHandlers[declaration.key] {
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
group, found := byFunction[declaration.key]
|
|
100
|
+
if !found || len(group.Calls) == 0 {
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
method, path, ok := annotatedRoute(declaration.fn)
|
|
104
|
+
if !ok || providerRoutes[routeKey(method, path)] || rootedRoutes[routeKey(method, path)] {
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
kind := RootHTTP
|
|
108
|
+
if looksLikeCallback(path, declaration.key) {
|
|
109
|
+
kind = RootCallback
|
|
110
|
+
}
|
|
111
|
+
confidence := "medium"
|
|
112
|
+
source := group.Source
|
|
113
|
+
if registeredAt, registered := registrations[declaration.key]; registered {
|
|
114
|
+
confidence = "high"
|
|
115
|
+
source = registeredAt
|
|
116
|
+
}
|
|
117
|
+
out = append(out, RootFlow{
|
|
118
|
+
Kind: kind, Method: method, Path: path, Handler: declaration.key,
|
|
119
|
+
Label: strings.TrimSpace(method + " " + path), Confidence: confidence,
|
|
120
|
+
Source: source, Calls: group.Calls, Covered: []string{declaration.key},
|
|
121
|
+
})
|
|
122
|
+
rootedHandlers[declaration.key] = true
|
|
123
|
+
rootedRoutes[routeKey(method, path)] = true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// main and package-main init are lifecycle roots. Calls inside registered
|
|
127
|
+
// closures are deliberately skipped: those execute when their own HTTP or
|
|
128
|
+
// async root fires, not while the process is starting.
|
|
129
|
+
for _, owner := range s.sortedFunctions() {
|
|
130
|
+
if !lifecycleFunction(owner) {
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
out = append(out, s.lifecycleRoots(owner, byFunction, map[string]bool{}, 0)...)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
out = uniqueRootFlows(out)
|
|
137
|
+
sort.Slice(out, func(i, j int) bool {
|
|
138
|
+
if out[i].Kind != out[j].Kind {
|
|
139
|
+
return out[i].Kind < out[j].Kind
|
|
140
|
+
}
|
|
141
|
+
if out[i].Label != out[j].Label {
|
|
142
|
+
return out[i].Label < out[j].Label
|
|
143
|
+
}
|
|
144
|
+
return out[i].Handler < out[j].Handler
|
|
145
|
+
})
|
|
146
|
+
return out
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// handlerRegistrations upgrades a route annotation from documentation-only
|
|
150
|
+
// evidence when source also passes that handler to a conventional registry.
|
|
151
|
+
// The annotation still supplies the path; the registration proves it is live.
|
|
152
|
+
func (s *scanner) handlerRegistrations() map[string]Source {
|
|
153
|
+
out := map[string]Source{}
|
|
154
|
+
for _, owner := range s.sortedFunctions() {
|
|
155
|
+
ast.Inspect(owner.fn.Body, func(node ast.Node) bool {
|
|
156
|
+
call, ok := node.(*ast.CallExpr)
|
|
157
|
+
if !ok {
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
selector, ok := call.Fun.(*ast.SelectorExpr)
|
|
161
|
+
if !ok || (selector.Sel.Name != "Register" && selector.Sel.Name != "Handle") {
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
for _, argument := range call.Args {
|
|
165
|
+
target := s.handlerExpressionTarget(owner, argument)
|
|
166
|
+
if target != "" {
|
|
167
|
+
out[target] = s.source(owner.file, call.Pos())
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return true
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
return out
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
func (s *scanner) handlerExpressionTarget(owner *functionDecl, expr ast.Expr) string {
|
|
177
|
+
if call, ok := expr.(*ast.CallExpr); ok {
|
|
178
|
+
return s.localTarget(owner, call.Fun)
|
|
179
|
+
}
|
|
180
|
+
if target := s.localTarget(owner, expr); target != "" {
|
|
181
|
+
return target
|
|
182
|
+
}
|
|
183
|
+
return s.localVariableMethod(owner, expr)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
func (s *scanner) scheduledRegistration(owner *functionDecl, call *ast.CallExpr) (string, ast.Expr, bool) {
|
|
187
|
+
selector, ok := call.Fun.(*ast.SelectorExpr)
|
|
188
|
+
if !ok || len(call.Args) < 2 {
|
|
189
|
+
return "", nil, false
|
|
190
|
+
}
|
|
191
|
+
switch selector.Sel.Name {
|
|
192
|
+
case "AddFunc", "AfterFunc", "Schedule":
|
|
193
|
+
default:
|
|
194
|
+
return "", nil, false
|
|
195
|
+
}
|
|
196
|
+
locals := s.localStrings(owner.file, owner.fn)
|
|
197
|
+
schedule := s.value(owner.file, call.Args[0], locals, map[string]bool{})
|
|
198
|
+
if schedule == "" {
|
|
199
|
+
schedule = expression(call.Args[0])
|
|
200
|
+
}
|
|
201
|
+
if schedule == "" {
|
|
202
|
+
schedule = "registered"
|
|
203
|
+
}
|
|
204
|
+
return schedule, call.Args[len(call.Args)-1], true
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
func (s *scanner) lifecycleRoots(owner *functionDecl, groups map[string]FlowGroup, visiting map[string]bool, depth int) []RootFlow {
|
|
208
|
+
if owner == nil || depth > 4 || visiting[owner.key] {
|
|
209
|
+
return nil
|
|
210
|
+
}
|
|
211
|
+
visiting[owner.key] = true
|
|
212
|
+
defer delete(visiting, owner.key)
|
|
213
|
+
var out []RootFlow
|
|
214
|
+
ast.Inspect(owner.fn.Body, func(node ast.Node) bool {
|
|
215
|
+
if _, nested := node.(*ast.FuncLit); nested {
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
call, ok := node.(*ast.CallExpr)
|
|
219
|
+
if !ok {
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
target := s.localTarget(owner, call.Fun)
|
|
223
|
+
if target == "" {
|
|
224
|
+
target = s.localVariableMethod(owner, call.Fun)
|
|
225
|
+
}
|
|
226
|
+
if group, found := groups[target]; found && len(group.Calls) > 0 {
|
|
227
|
+
out = append(out, RootFlow{
|
|
228
|
+
Kind: RootStartup, Handler: target,
|
|
229
|
+
Label: "Startup → " + displayFunction(target), Confidence: "high",
|
|
230
|
+
Source: s.source(owner.file, call.Pos()), Calls: group.Calls,
|
|
231
|
+
Covered: []string{target},
|
|
232
|
+
})
|
|
233
|
+
return true
|
|
234
|
+
}
|
|
235
|
+
if next := s.functions[target]; assemblyEntrypoint(next) {
|
|
236
|
+
out = append(out, s.lifecycleRoots(next, groups, visiting, depth+1)...)
|
|
237
|
+
}
|
|
238
|
+
return true
|
|
239
|
+
})
|
|
240
|
+
return out
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
func (s *scanner) sortedFunctions() []*functionDecl {
|
|
244
|
+
keys := make([]string, 0, len(s.functions))
|
|
245
|
+
for key := range s.functions {
|
|
246
|
+
keys = append(keys, key)
|
|
247
|
+
}
|
|
248
|
+
sort.Strings(keys)
|
|
249
|
+
out := make([]*functionDecl, 0, len(keys))
|
|
250
|
+
for _, key := range keys {
|
|
251
|
+
out = append(out, s.functions[key])
|
|
252
|
+
}
|
|
253
|
+
return out
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
func annotatedRoute(fn *ast.FuncDecl) (string, string, bool) {
|
|
257
|
+
if fn.Doc == nil {
|
|
258
|
+
return "", "", false
|
|
259
|
+
}
|
|
260
|
+
for _, line := range strings.Split(fn.Doc.Text(), "\n") {
|
|
261
|
+
at := strings.Index(line, "@Router")
|
|
262
|
+
if at < 0 {
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
fields := strings.Fields(line[at+len("@Router"):])
|
|
266
|
+
if len(fields) < 2 || !strings.HasPrefix(fields[0], "/") {
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
method := strings.ToUpper(strings.Trim(fields[1], "[]"))
|
|
270
|
+
if method == "" {
|
|
271
|
+
method = "HTTP"
|
|
272
|
+
}
|
|
273
|
+
return method, fields[0], true
|
|
274
|
+
}
|
|
275
|
+
return "", "", false
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
func lifecycleFunction(declaration *functionDecl) bool {
|
|
279
|
+
name := displayFunction(declaration.key)
|
|
280
|
+
return declaration.file.pkg == "main" && (name == "init" || name == "main")
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
func assemblyEntrypoint(declaration *functionDecl) bool {
|
|
284
|
+
if declaration == nil {
|
|
285
|
+
return false
|
|
286
|
+
}
|
|
287
|
+
name := displayFunction(declaration.key)
|
|
288
|
+
if at := strings.LastIndex(name, "."); at >= 0 {
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
291
|
+
switch name {
|
|
292
|
+
case "Run", "Start", "Bootstrap":
|
|
293
|
+
dir := "/" + strings.Trim(declaration.file.dir, "/") + "/"
|
|
294
|
+
return strings.Contains(dir, "/app/") || strings.Contains(dir, "/cmd/")
|
|
295
|
+
}
|
|
296
|
+
return false
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
func looksLikeCallback(path, function string) bool {
|
|
300
|
+
value := strings.ToLower(path + " " + function)
|
|
301
|
+
return strings.Contains(value, "callback") || strings.Contains(value, "webhook")
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func routeKey(method, path string) string {
|
|
305
|
+
return strings.ToUpper(method) + "\x00" + path
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
func uniqueRootFlows(in []RootFlow) []RootFlow {
|
|
309
|
+
seen := map[string]bool{}
|
|
310
|
+
out := make([]RootFlow, 0, len(in))
|
|
311
|
+
for _, root := range in {
|
|
312
|
+
key := string(root.Kind) + "\x00" + root.Label + "\x00" + root.Handler
|
|
313
|
+
if seen[key] {
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
seen[key] = true
|
|
317
|
+
out = append(out, root)
|
|
318
|
+
}
|
|
319
|
+
return out
|
|
320
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
package gohttp
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"sort"
|
|
7
|
+
"strconv"
|
|
8
|
+
"strings"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
"github.com/shortlink-org/portolan/internal/gocall"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const typedAnalysisTimeout = 45 * time.Second
|
|
15
|
+
|
|
16
|
+
// indexTypedCallEdges adapts shared typed facts to the HTTP syntax index.
|
|
17
|
+
// Dynamic edges are possible callees, not proof of runtime execution.
|
|
18
|
+
func (s *scanner) indexTypedCallEdges() bool {
|
|
19
|
+
ctx, cancel := context.WithTimeout(context.Background(), typedAnalysisTimeout)
|
|
20
|
+
defer cancel()
|
|
21
|
+
result, err := gocall.Analyze(ctx, gocall.Options{Root: s.root})
|
|
22
|
+
if err != nil {
|
|
23
|
+
s.typedCallGraphError = err.Error()
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
for _, diagnostic := range result.Diagnostics {
|
|
27
|
+
s.warnings = append(s.warnings, "typed call graph is partial: "+diagnostic.String())
|
|
28
|
+
}
|
|
29
|
+
positions := s.functionKeysByPosition()
|
|
30
|
+
seen := map[string]bool{}
|
|
31
|
+
for _, edge := range result.Edges {
|
|
32
|
+
caller := s.typedFunctionKey(edge.Caller, positions)
|
|
33
|
+
callee := s.typedFunctionKey(edge.Callee, positions)
|
|
34
|
+
if caller == "" || callee == "" || caller == callee {
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
key := caller + "\x00" + strconv.Itoa(edge.Site.Line) + "\x00" + callee
|
|
38
|
+
if !seen[key] {
|
|
39
|
+
seen[key] = true
|
|
40
|
+
s.typedEdges[caller] = append(s.typedEdges[caller], localEdge{target: callee, line: edge.Site.Line})
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for caller := range s.typedEdges {
|
|
44
|
+
sort.Slice(s.typedEdges[caller], func(i, j int) bool {
|
|
45
|
+
a, b := s.typedEdges[caller][i], s.typedEdges[caller][j]
|
|
46
|
+
if a.line != b.line {
|
|
47
|
+
return a.line < b.line
|
|
48
|
+
}
|
|
49
|
+
return a.target < b.target
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
return len(s.typedEdges) > 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func (s *scanner) functionKeysByPosition() map[string][]string {
|
|
56
|
+
out := map[string][]string{}
|
|
57
|
+
for key, declaration := range s.functions {
|
|
58
|
+
position := s.fset.Position(declaration.fn.Name.Pos())
|
|
59
|
+
location := s.relativePosition(position.Filename, position.Line)
|
|
60
|
+
if location != "" {
|
|
61
|
+
out[location] = append(out[location], key)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for location := range out {
|
|
65
|
+
sort.Strings(out[location])
|
|
66
|
+
}
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func (s *scanner) typedFunctionKey(function gocall.Function, positions map[string][]string) string {
|
|
71
|
+
for _, definition := range function.Definitions {
|
|
72
|
+
pos := definition.Position
|
|
73
|
+
keys := positions[pos.File+":"+strconv.Itoa(pos.Line)]
|
|
74
|
+
if len(keys) == 1 {
|
|
75
|
+
return keys[0]
|
|
76
|
+
}
|
|
77
|
+
for _, key := range keys {
|
|
78
|
+
display := displayFunction(key)
|
|
79
|
+
if display == definition.Name || strings.HasSuffix(display, "."+definition.Name) {
|
|
80
|
+
return key
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return ""
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func (s *scanner) relativePosition(filename string, line int) string {
|
|
88
|
+
if filename == "" || line == 0 {
|
|
89
|
+
return ""
|
|
90
|
+
}
|
|
91
|
+
relative, err := filepath.Rel(s.root, filename)
|
|
92
|
+
if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
|
|
93
|
+
return ""
|
|
94
|
+
}
|
|
95
|
+
return filepath.ToSlash(relative) + ":" + strconv.Itoa(line)
|
|
96
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package goscan
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"go/ast"
|
|
5
|
+
"go/token"
|
|
6
|
+
"strconv"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// indexConstants records every constant in the tree. A name in a const group
|
|
10
|
+
// with no value of its own takes the values of the line before it, the way
|
|
11
|
+
// the language does.
|
|
12
|
+
func (t *Tree) indexConstants() {
|
|
13
|
+
for _, file := range t.Files {
|
|
14
|
+
for _, decl := range file.Node.Decls {
|
|
15
|
+
gen, ok := decl.(*ast.GenDecl)
|
|
16
|
+
if !ok || gen.Tok != token.CONST {
|
|
17
|
+
continue
|
|
18
|
+
}
|
|
19
|
+
var inherited []ast.Expr
|
|
20
|
+
for _, raw := range gen.Specs {
|
|
21
|
+
spec := raw.(*ast.ValueSpec)
|
|
22
|
+
values := spec.Values
|
|
23
|
+
if len(values) == 0 {
|
|
24
|
+
values = inherited
|
|
25
|
+
} else {
|
|
26
|
+
inherited = values
|
|
27
|
+
}
|
|
28
|
+
for i, name := range spec.Names {
|
|
29
|
+
if len(values) > 0 {
|
|
30
|
+
t.Constants[file.Pkg+"."+name.Name] = ConstExpr{Expr: values[min(i, len(values)-1)], File: file}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// StringOf is the string an expression is worth: a literal; a constant of the
|
|
39
|
+
// tree, by its own name or through an import; or what Foreign says about a
|
|
40
|
+
// name from outside. Empty when it is none of those. visiting is the chain of
|
|
41
|
+
// constants being followed, so one defined in terms of itself ends rather
|
|
42
|
+
// than recurses; nil to start.
|
|
43
|
+
func (t *Tree) StringOf(expr ast.Expr, file *File, visiting map[string]bool) string {
|
|
44
|
+
switch value := Unwrap(expr).(type) {
|
|
45
|
+
case *ast.BasicLit:
|
|
46
|
+
if value.Kind == token.STRING {
|
|
47
|
+
text, _ := strconv.Unquote(value.Value)
|
|
48
|
+
return text
|
|
49
|
+
}
|
|
50
|
+
case *ast.Ident:
|
|
51
|
+
return t.ConstantString(file.Pkg+"."+value.Name, visiting)
|
|
52
|
+
case *ast.SelectorExpr:
|
|
53
|
+
pkg, ok := value.X.(*ast.Ident)
|
|
54
|
+
if !ok {
|
|
55
|
+
return ""
|
|
56
|
+
}
|
|
57
|
+
imported := file.Imports[pkg.Name]
|
|
58
|
+
if t.Foreign != nil {
|
|
59
|
+
if known, ok := t.Foreign(imported, value.Sel.Name); ok {
|
|
60
|
+
return known
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return t.ConstantString(imported+"."+value.Sel.Name, visiting)
|
|
64
|
+
}
|
|
65
|
+
return ""
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ConstantString is the string behind a constant, by "<import path>.<Name>",
|
|
69
|
+
// followed through whatever it was declared as.
|
|
70
|
+
func (t *Tree) ConstantString(key string, visiting map[string]bool) string {
|
|
71
|
+
if visiting == nil {
|
|
72
|
+
visiting = map[string]bool{}
|
|
73
|
+
}
|
|
74
|
+
if visiting[key] {
|
|
75
|
+
return ""
|
|
76
|
+
}
|
|
77
|
+
found, ok := t.Constants[key]
|
|
78
|
+
if !ok {
|
|
79
|
+
return ""
|
|
80
|
+
}
|
|
81
|
+
visiting[key] = true
|
|
82
|
+
value := t.StringOf(found.Expr, found.File, visiting)
|
|
83
|
+
delete(visiting, key)
|
|
84
|
+
return value
|
|
85
|
+
}
|