@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.
Files changed (151) hide show
  1. package/README.md +23 -0
  2. package/catalog/enum_test.go +46 -0
  3. package/catalog/evidence_test.go +35 -0
  4. package/catalog/model.go +1066 -0
  5. package/catalog/roundtrip_test.go +203 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/init.test.mjs +6 -1
  8. package/cli/portolan.mjs +8 -0
  9. package/cli/portolan.test.mjs +49 -0
  10. package/go.mod +14 -0
  11. package/go.sum +20 -0
  12. package/internal/gocall/README.md +19 -0
  13. package/internal/gocall/analyze.go +189 -0
  14. package/internal/gocall/analyze_test.go +107 -0
  15. package/internal/gohttp/analyze.go +2562 -0
  16. package/internal/gohttp/destination.go +373 -0
  17. package/internal/gohttp/endpoints.go +1067 -0
  18. package/internal/gohttp/roots.go +320 -0
  19. package/internal/gohttp/typed.go +96 -0
  20. package/internal/goscan/constants.go +85 -0
  21. package/internal/goscan/goscan_test.go +227 -0
  22. package/internal/goscan/index.go +629 -0
  23. package/internal/goscan/index_test.go +66 -0
  24. package/internal/goscan/names.go +52 -0
  25. package/internal/goscan/parse_test.go +11 -0
  26. package/internal/goscan/source.go +37 -0
  27. package/internal/goscan/tree.go +284 -0
  28. package/internal/goscan/types.go +99 -0
  29. package/internal/wsdl/ids.go +127 -0
  30. package/internal/wsdl/ids_test.go +21 -0
  31. package/internal/wsdl/model.go +70 -0
  32. package/internal/wsdl/parse.go +949 -0
  33. package/internal/wsdl/parse_test.go +170 -0
  34. package/package.json +22 -10
  35. package/plugin/describe.go +118 -0
  36. package/plugin/describe_test.go +114 -0
  37. package/plugin/protocol.go +141 -0
  38. package/plugin/schematest/schematest.go +126 -0
  39. package/plugins/README.md +109 -46
  40. package/plugins/cmd/portolan-http-clients/main.go +19 -0
  41. package/plugins/extract-celery/extract.py +0 -2
  42. package/plugins/extract-celery/extract_test.py +1 -1
  43. package/plugins/extract-django/README.md +39 -17
  44. package/plugins/extract-django/domain.py +28 -17
  45. package/plugins/extract-django/extract.py +21 -7
  46. package/plugins/extract-django/extract_test.py +55 -2
  47. package/plugins/extract-django/lifecycle.py +2 -0
  48. package/plugins/extract-django/operations.py +1 -1
  49. package/plugins/extract-django/routing_test.py +109 -1
  50. package/plugins/extract-django/store.py +1 -1
  51. package/plugins/extract-django/transport.py +101 -55
  52. package/plugins/extract-django/verbs.py +241 -0
  53. package/plugins/extract-go/README.md +47 -0
  54. package/plugins/extract-http-clients/describe.go +19 -0
  55. package/plugins/extract-http-clients/describe_test.go +11 -0
  56. package/plugins/extract-http-clients/extract.go +740 -0
  57. package/plugins/extract-http-clients/extract_test.go +1561 -0
  58. package/plugins/extract-http-clients/main.go +41 -0
  59. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  60. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  61. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  62. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  63. package/plugins/extract-python-kafka/extract.py +0 -2
  64. package/plugins/extract-python-kafka/extract_test.py +1 -1
  65. package/plugins/extract-ts/extract.test.ts +2 -2
  66. package/plugins/extract-ts/extract.ts +4 -5
  67. package/plugins/extract-ts/graphql.test.ts +1 -1
  68. package/plugins/openapi/ids.go +261 -0
  69. package/plugins/openapi/ids_test.go +98 -0
  70. package/plugins/portolan-go.wasm +0 -0
  71. package/plugins/pyplugin/protocol.py +1 -5
  72. package/portolan.json +3 -2
  73. package/schema/portolan.schema.json +34 -0
  74. package/scripts/README.md +18 -12
  75. package/scripts/catalog-sources.mjs +6 -0
  76. package/scripts/delivery-presets.mjs +21 -11
  77. package/scripts/diff.mjs +5 -1
  78. package/scripts/django-aggregates.test.mjs +58 -0
  79. package/scripts/gen-likec4.mjs +1 -1
  80. package/scripts/gen.mjs +118 -115
  81. package/scripts/go-discovery.test.mjs +30 -0
  82. package/scripts/history.mjs +186 -3
  83. package/scripts/history.test.mjs +1 -1
  84. package/scripts/host-plugins/fetch-git.mjs +77 -21
  85. package/scripts/host-plugins/fetch-git.test.mjs +62 -8
  86. package/scripts/local-api.mjs +71 -4
  87. package/scripts/local-api.test.mjs +63 -4
  88. package/scripts/local-discovery.mjs +82 -9
  89. package/scripts/manifest.mjs +5 -3
  90. package/scripts/manifest.test.mjs +24 -0
  91. package/scripts/output-diff.mjs +94 -0
  92. package/scripts/output-diff.test.mjs +36 -0
  93. package/scripts/package-smoke.mjs +62 -4
  94. package/scripts/plugin-host.mjs +22 -2
  95. package/scripts/plugin-host.test.mjs +9 -0
  96. package/scripts/plugin-wasm-worker.mjs +4 -1
  97. package/scripts/provenance.mjs +72 -0
  98. package/scripts/provenance.test.mjs +149 -0
  99. package/scripts/run-builtin.mjs +39 -5
  100. package/scripts/schema.mjs +29 -0
  101. package/scripts/warning-policy.mjs +161 -0
  102. package/scripts/warning-policy.test.mjs +56 -0
  103. package/src/app/Sidebar.tsx +3 -3
  104. package/src/catalog-docs.test.ts +64 -0
  105. package/src/catalog-docs.ts +35 -0
  106. package/src/catalog-error.test.ts +15 -0
  107. package/src/catalog-model.ts +48 -5
  108. package/src/catalog-validation.ts +9 -0
  109. package/src/chat/Starter.tsx +5 -11
  110. package/src/chat/tools.test.ts +27 -0
  111. package/src/chat/tools.ts +5 -9
  112. package/src/components/CatalogStamp.tsx +10 -8
  113. package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
  114. package/src/components/HTTPDestinationEvidence.tsx +31 -0
  115. package/src/components/Integrations.tsx +1 -1
  116. package/src/components/MachineDocs.tsx +6 -5
  117. package/src/components/MethodRows.tsx +9 -2
  118. package/src/components/RelationEvidence.test.tsx +14 -0
  119. package/src/components/RelationEvidence.tsx +53 -0
  120. package/src/data.ts +25 -7
  121. package/src/enrich.test.ts +332 -1
  122. package/src/enrich.ts +206 -3
  123. package/src/flow/StepDetail.tsx +6 -0
  124. package/src/flow/evidence.test.ts +16 -0
  125. package/src/flow/evidence.ts +34 -0
  126. package/src/index.css +44 -0
  127. package/src/landing/DraggableReveal.tsx +3 -2
  128. package/src/landing/EvidencePipeline.tsx +105 -0
  129. package/src/landing/LandingPage.tsx +2 -59
  130. package/src/lib/catalog-diff.ts +1 -1
  131. package/src/lib/django-aggregates.d.mts +9 -0
  132. package/src/lib/django-aggregates.mjs +36 -0
  133. package/src/lib/django-aggregates.test.ts +29 -0
  134. package/src/lib/django-aggregates.ts +5 -0
  135. package/src/lib/local-api.ts +20 -2
  136. package/src/lib/setup-info.test.ts +17 -0
  137. package/src/lib/setup-info.ts +58 -0
  138. package/src/lib/warnings.test.ts +54 -0
  139. package/src/lib/warnings.ts +260 -0
  140. package/src/map/ContextMapGraph.tsx +76 -32
  141. package/src/merge.ts +16 -9
  142. package/src/pages/AggregatePage.tsx +8 -7
  143. package/src/pages/ContextPage.tsx +6 -5
  144. package/src/pages/ServicePage.tsx +4 -3
  145. package/src/pages/Settings.tsx +189 -41
  146. package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
  147. package/src/selection/DetailPanel.tsx +15 -0
  148. package/src/virtual-provenance.d.ts +11 -0
  149. package/vite.config.ts +5 -0
  150. package/scripts/vendor-lock.mjs +0 -58
  151. package/scripts/vendor-lock.test.mjs +0 -69
@@ -0,0 +1,629 @@
1
+ package goscan
2
+
3
+ import (
4
+ "go/ast"
5
+ "go/token"
6
+ "reflect"
7
+ "sort"
8
+ "strconv"
9
+ )
10
+
11
+ // StructType is a struct as a reader needs it: what type each field has, so
12
+ // that `b.conn` is known to be a connection, and what a field defaults to
13
+ // when its tag says, so that a subject or a topic read off config resolves.
14
+ type StructType struct {
15
+ Key string
16
+ Fields map[string]string
17
+ Defaults map[string]string
18
+ // Env is the environment variable a field is configured by, when its
19
+ // `envconfig` or `env` tag says; documentation for the default's origin.
20
+ Env map[string]string
21
+ }
22
+
23
+ // Function is one declared function or method: its parameters by name and
24
+ // type, what it returns, and what its body assigns to which name.
25
+ type Function struct {
26
+ Key string
27
+ Name string
28
+ Receiver string
29
+ File *File
30
+ Decl *ast.FuncDecl
31
+ Params []string
32
+ Types map[string]string
33
+ Results []string
34
+ assigned map[string]Assignment
35
+ }
36
+
37
+ // Assignment is the last thing a local name was given: the expression, and
38
+ // which of its values when the expression yields several.
39
+ type Assignment struct {
40
+ Expr ast.Expr
41
+ Index int
42
+ }
43
+
44
+ // Resolved is one string an expression was found to be, where it was
45
+ // written, and - when it came through a wrapper that also names the message -
46
+ // what the message is called.
47
+ type Resolved struct {
48
+ Value string
49
+ At Source
50
+ Name string
51
+ }
52
+
53
+ // CallArg is one expression a caller passes for a parameter, with the
54
+ // function it is written in, so that it can be read in that function's scope.
55
+ type CallArg struct {
56
+ Expr ast.Expr
57
+ Fn *Function
58
+ Call *ast.CallExpr
59
+ }
60
+
61
+ // Index is the tree, and the declarations in it a call is read against:
62
+ // the structs with their field types and defaults, the interfaces by their
63
+ // method names, and every function with what it takes and returns. It is
64
+ // what lets an extractor follow a value through a local, a field, a
65
+ // constructor and a parameter up to the caller that passed it - the same
66
+ // walk for a NATS subject, a Watermill topic and a River queue.
67
+ type Index struct {
68
+ *Tree
69
+ Structs map[string]*StructType
70
+ Interfaces map[string][]string
71
+ // Named is every type the tree declares, by key, whatever its shape.
72
+ Named map[string]bool
73
+ Functions map[string]*Function
74
+ Methods map[string][]*Function
75
+ ByName map[string][]*Function
76
+ // KnownResults is what constructors outside the tree hand back, by
77
+ // "<import path>.<Func>" or "<type key>.<Method>": nats.Connect gives a
78
+ // *nats.Conn, and nothing in the tree says so. Set by the extractor.
79
+ KnownResults map[string][]string
80
+ // Companion, when set, is which other parameter of fn names the message
81
+ // carried beside the value at index - the shape of a bus port,
82
+ // Subscribe(subject, name, handler). -1 when there is none.
83
+ Companion func(fn *Function, index int) int
84
+ // Hops is how far up the callers a parameter is followed. One hop is the
85
+ // port: the adapter takes the value as a parameter and the assembly
86
+ // passes a constant. Two is an assembly that itself was handed it.
87
+ // Further than that is not a declaration any more.
88
+ Hops int
89
+ // typing is the locals whose type is being worked out right now, so a
90
+ // name shadowed by an expression of itself - `js := js.Sub()` in an
91
+ // inner block - ends rather than recurses.
92
+ typing map[string]bool
93
+ }
94
+
95
+ // NewIndex reads every type and function declaration of the tree.
96
+ func NewIndex(tree *Tree) *Index {
97
+ s := &Index{
98
+ Tree: tree,
99
+ Structs: map[string]*StructType{},
100
+ Interfaces: map[string][]string{},
101
+ Named: map[string]bool{},
102
+ Functions: map[string]*Function{},
103
+ Methods: map[string][]*Function{},
104
+ ByName: map[string][]*Function{},
105
+ Hops: 2,
106
+ typing: map[string]bool{},
107
+ }
108
+ for _, file := range tree.Files {
109
+ for _, decl := range file.Node.Decls {
110
+ switch d := decl.(type) {
111
+ case *ast.GenDecl:
112
+ if d.Tok != token.TYPE {
113
+ continue
114
+ }
115
+ for _, raw := range d.Specs {
116
+ s.indexType(file, raw.(*ast.TypeSpec))
117
+ }
118
+ case *ast.FuncDecl:
119
+ s.indexFunction(file, d)
120
+ }
121
+ }
122
+ }
123
+ return s
124
+ }
125
+
126
+ func (s *Index) indexType(file *File, spec *ast.TypeSpec) {
127
+ key := file.Pkg + "." + spec.Name.Name
128
+ s.Named[key] = true
129
+ switch body := spec.Type.(type) {
130
+ case *ast.StructType:
131
+ st := &StructType{Key: key, Fields: map[string]string{}, Defaults: map[string]string{}, Env: map[string]string{}}
132
+ for _, field := range body.Fields.List {
133
+ typeKey := s.TypeKey(field.Type, file)
134
+ def, env := "", ""
135
+ if field.Tag != nil {
136
+ text, _ := strconv.Unquote(field.Tag.Value)
137
+ tag := reflect.StructTag(text)
138
+ def = tag.Get("default")
139
+ env = FirstNonEmpty(tag.Get("envconfig"), tag.Get("env"))
140
+ }
141
+ for _, name := range field.Names {
142
+ st.Fields[name.Name] = typeKey
143
+ if def != "" {
144
+ st.Defaults[name.Name] = def
145
+ }
146
+ if env != "" {
147
+ st.Env[name.Name] = env
148
+ }
149
+ }
150
+ }
151
+ s.Structs[key] = st
152
+ case *ast.InterfaceType:
153
+ // Embedded interfaces are not followed: the port a service writes for
154
+ // its bus names its methods itself.
155
+ var names []string
156
+ for _, method := range body.Methods.List {
157
+ for _, name := range method.Names {
158
+ names = append(names, name.Name)
159
+ }
160
+ }
161
+ sort.Strings(names)
162
+ s.Interfaces[key] = names
163
+ }
164
+ }
165
+
166
+ func (s *Index) indexFunction(file *File, decl *ast.FuncDecl) {
167
+ key := file.Pkg + "." + decl.Name.Name
168
+ receiver := ""
169
+ if decl.Recv != nil && len(decl.Recv.List) > 0 {
170
+ receiver = s.TypeKey(decl.Recv.List[0].Type, file)
171
+ key = receiver + "." + decl.Name.Name
172
+ }
173
+ fn := &Function{Key: key, Name: decl.Name.Name, Receiver: receiver, File: file, Decl: decl, Types: map[string]string{}}
174
+ if decl.Recv != nil {
175
+ for _, field := range decl.Recv.List {
176
+ for _, name := range field.Names {
177
+ fn.Types[name.Name] = receiver
178
+ }
179
+ }
180
+ }
181
+ if decl.Type.Params != nil {
182
+ for _, field := range decl.Type.Params.List {
183
+ typeKey := s.TypeKey(field.Type, file)
184
+ for _, name := range field.Names {
185
+ fn.Params = append(fn.Params, name.Name)
186
+ fn.Types[name.Name] = typeKey
187
+ }
188
+ }
189
+ }
190
+ if decl.Type.Results != nil {
191
+ for _, field := range decl.Type.Results.List {
192
+ typeKey := s.TypeKey(field.Type, file)
193
+ if len(field.Names) == 0 {
194
+ fn.Results = append(fn.Results, typeKey)
195
+ }
196
+ for range field.Names {
197
+ fn.Results = append(fn.Results, typeKey)
198
+ }
199
+ }
200
+ }
201
+ s.Functions[key] = fn
202
+ if receiver != "" {
203
+ s.Methods[receiver] = append(s.Methods[receiver], fn)
204
+ s.ByName[fn.Name] = append(s.ByName[fn.Name], fn)
205
+ }
206
+ }
207
+
208
+ // Implements is whether a receiver type has every method an interface asks
209
+ // for, by name. Names are what a port is: a type with the right names is
210
+ // what the assembly hands over, and the compiler holds the signatures.
211
+ func (s *Index) Implements(receiver, iface string) bool {
212
+ want := s.Interfaces[iface]
213
+ if len(want) == 0 {
214
+ return false
215
+ }
216
+ have := map[string]bool{}
217
+ for _, fn := range s.Methods[receiver] {
218
+ have[fn.Name] = true
219
+ }
220
+ for _, name := range want {
221
+ if !have[name] {
222
+ return false
223
+ }
224
+ }
225
+ return true
226
+ }
227
+
228
+ // AssignedTo is the last expression a local name was given in the function,
229
+ // read once per function.
230
+ func (s *Index) AssignedTo(fn *Function, name string) (Assignment, bool) {
231
+ if fn.assigned == nil {
232
+ fn.assigned = map[string]Assignment{}
233
+ if fn.Decl.Body != nil {
234
+ ast.Inspect(fn.Decl.Body, func(node ast.Node) bool {
235
+ switch stmt := node.(type) {
236
+ case *ast.AssignStmt:
237
+ record(fn.assigned, stmt.Lhs, stmt.Rhs)
238
+ case *ast.ValueSpec:
239
+ var lhs []ast.Expr
240
+ for _, ident := range stmt.Names {
241
+ lhs = append(lhs, ident)
242
+ }
243
+ record(fn.assigned, lhs, stmt.Values)
244
+ }
245
+ return true
246
+ })
247
+ }
248
+ }
249
+ found, ok := fn.assigned[name]
250
+ return found, ok
251
+ }
252
+
253
+ func record(into map[string]Assignment, lhs, rhs []ast.Expr) {
254
+ if len(rhs) == 0 {
255
+ return
256
+ }
257
+ for i, target := range lhs {
258
+ ident, ok := target.(*ast.Ident)
259
+ if !ok || ident.Name == "_" {
260
+ continue
261
+ }
262
+ if len(rhs) == 1 && len(lhs) > 1 {
263
+ into[ident.Name] = Assignment{Expr: rhs[0], Index: i}
264
+ } else if i < len(rhs) {
265
+ into[ident.Name] = Assignment{Expr: rhs[i]}
266
+ }
267
+ }
268
+ }
269
+
270
+ // TypeOf is the type key of an expression, as far as declarations say: a
271
+ // parameter or receiver by its declared type, a local by what it was
272
+ // assigned, a field by its struct, a call by what the callee returns.
273
+ func (s *Index) TypeOf(expr ast.Expr, fn *Function) string {
274
+ switch value := Unwrap(expr).(type) {
275
+ case *ast.Ident:
276
+ if kind := fn.Types[value.Name]; kind != "" {
277
+ return kind
278
+ }
279
+ key := fn.Key + ":" + value.Name
280
+ if s.typing[key] {
281
+ return ""
282
+ }
283
+ s.typing[key] = true
284
+ defer delete(s.typing, key)
285
+ if given, ok := s.AssignedTo(fn, value.Name); ok {
286
+ if call, isCall := Unwrap(given.Expr).(*ast.CallExpr); isCall {
287
+ results := s.ResultsOf(call, fn)
288
+ if given.Index < len(results) {
289
+ return results[given.Index]
290
+ }
291
+ return ""
292
+ }
293
+ if given.Index == 0 {
294
+ return s.TypeOf(given.Expr, fn)
295
+ }
296
+ }
297
+ case *ast.CompositeLit:
298
+ return s.TypeKey(value.Type, fn.File)
299
+ case *ast.SelectorExpr:
300
+ if st := s.Structs[s.TypeOf(value.X, fn)]; st != nil {
301
+ return st.Fields[value.Sel.Name]
302
+ }
303
+ case *ast.CallExpr:
304
+ if results := s.ResultsOf(value, fn); len(results) > 0 {
305
+ return results[0]
306
+ }
307
+ }
308
+ return ""
309
+ }
310
+
311
+ // ResultsOf is what a call returns: the callee's declared results when it is
312
+ // in the tree, KnownResults when it is a library's.
313
+ func (s *Index) ResultsOf(call *ast.CallExpr, fn *Function) []string {
314
+ for _, target := range s.Callees(call, fn) {
315
+ return target.Results
316
+ }
317
+ if key := s.ExternalKey(call, fn); key != "" {
318
+ return s.KnownResults[key]
319
+ }
320
+ return nil
321
+ }
322
+
323
+ // ExternalKey names a call on something declared outside the tree: a
324
+ // package function by import path, a method by the type of what it is
325
+ // called on.
326
+ func (s *Index) ExternalKey(call *ast.CallExpr, fn *Function) string {
327
+ sel, ok := call.Fun.(*ast.SelectorExpr)
328
+ if !ok {
329
+ return ""
330
+ }
331
+ if base, ok := sel.X.(*ast.Ident); ok {
332
+ if imported := fn.File.Imports[base.Name]; imported != "" && fn.Types[base.Name] == "" {
333
+ if _, local := s.AssignedTo(fn, base.Name); !local {
334
+ return imported + "." + sel.Sel.Name
335
+ }
336
+ }
337
+ }
338
+ if recv := s.TypeOf(sel.X, fn); recv != "" {
339
+ return recv + "." + sel.Sel.Name
340
+ }
341
+ return ""
342
+ }
343
+
344
+ // Callees is every function in the tree a call may reach: the one named,
345
+ // when it is a plain function or a method on a known type; every
346
+ // implementation, when it is a method on an interface the tree declares.
347
+ func (s *Index) Callees(call *ast.CallExpr, fn *Function) []*Function {
348
+ return s.Targets(call.Fun, fn)
349
+ }
350
+
351
+ // Targets is every function an expression names - the same as Callees, for
352
+ // a function value that is passed rather than called: `h.Handle` handed to a
353
+ // router, `consume` handed to a subscriber.
354
+ func (s *Index) Targets(expr ast.Expr, fn *Function) []*Function {
355
+ switch callee := Unwrap(expr).(type) {
356
+ case *ast.Ident:
357
+ if target := s.Functions[fn.File.Pkg+"."+callee.Name]; target != nil {
358
+ return []*Function{target}
359
+ }
360
+ case *ast.SelectorExpr:
361
+ if base, ok := callee.X.(*ast.Ident); ok && fn.Types[base.Name] == "" {
362
+ if imported := fn.File.Imports[base.Name]; imported != "" {
363
+ if _, local := s.AssignedTo(fn, base.Name); !local {
364
+ if target := s.Functions[imported+"."+callee.Sel.Name]; target != nil {
365
+ return []*Function{target}
366
+ }
367
+ return nil
368
+ }
369
+ }
370
+ }
371
+ recv := s.TypeOf(callee.X, fn)
372
+ if recv == "" {
373
+ return nil
374
+ }
375
+ if target := s.Functions[recv+"."+callee.Sel.Name]; target != nil {
376
+ return []*Function{target}
377
+ }
378
+ if s.Interfaces[recv] != nil {
379
+ var out []*Function
380
+ for _, candidate := range s.ByName[callee.Sel.Name] {
381
+ if s.Implements(candidate.Receiver, recv) {
382
+ out = append(out, candidate)
383
+ }
384
+ }
385
+ sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key })
386
+ return out
387
+ }
388
+ }
389
+ return nil
390
+ }
391
+
392
+ // ParamIndex is which parameter a name is, or -1.
393
+ func ParamIndex(fn *Function, name string) int {
394
+ for i, param := range fn.Params {
395
+ if param == name {
396
+ return i
397
+ }
398
+ }
399
+ return -1
400
+ }
401
+
402
+ // SortedFunctions is every function with a body, in a fixed order.
403
+ func (s *Index) SortedFunctions() []*Function {
404
+ out := make([]*Function, 0, len(s.Functions))
405
+ for _, fn := range s.Functions {
406
+ if fn.Decl.Body != nil {
407
+ out = append(out, fn)
408
+ }
409
+ }
410
+ sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key })
411
+ return out
412
+ }
413
+
414
+ // SingleReturn is the expression a function is, when its whole body is
415
+ // `return x`: the Name() of an event, the Topic() of a message.
416
+ func SingleReturn(fn *Function) ast.Expr {
417
+ if fn.Decl.Body == nil || len(fn.Decl.Body.List) != 1 {
418
+ return nil
419
+ }
420
+ ret, ok := fn.Decl.Body.List[0].(*ast.ReturnStmt)
421
+ if !ok || len(ret.Results) != 1 {
422
+ return nil
423
+ }
424
+ return ret.Results[0]
425
+ }
426
+
427
+ // Resolve is the strings an expression can be worth: a literal; a constant,
428
+ // the tree's own or an imported one; a config field's default; a call to a
429
+ // function whose body is one return; a `string(x)` conversion of any of
430
+ // those; a concatenation of them; a parameter, by what every caller passes
431
+ // for it. Empty when it is none of those - never a guess.
432
+ func (s *Index) Resolve(expr ast.Expr, fn *Function, depth int, visiting map[string]bool) []Resolved {
433
+ at := s.At(expr.Pos())
434
+ switch value := Unwrap(expr).(type) {
435
+ case *ast.BasicLit:
436
+ if value.Kind == token.STRING {
437
+ text, _ := strconv.Unquote(value.Value)
438
+ return []Resolved{{Value: text, At: at}}
439
+ }
440
+ case *ast.Ident:
441
+ if index := ParamIndex(fn, value.Name); index >= 0 {
442
+ return s.FromCallers(fn, index, depth, visiting)
443
+ }
444
+ key := fn.Key + ":" + value.Name
445
+ if visiting[key] {
446
+ return nil
447
+ }
448
+ visiting[key] = true
449
+ defer delete(visiting, key)
450
+ if given, ok := s.AssignedTo(fn, value.Name); ok {
451
+ if given.Index == 0 {
452
+ return s.Resolve(given.Expr, fn, depth, visiting)
453
+ }
454
+ return nil
455
+ }
456
+ if text := s.StringOf(value, fn.File, nil); text != "" {
457
+ return []Resolved{{Value: text, At: at}}
458
+ }
459
+ case *ast.SelectorExpr:
460
+ if text := s.StringOf(value, fn.File, nil); text != "" {
461
+ return []Resolved{{Value: text, At: at}}
462
+ }
463
+ if st := s.Structs[s.TypeOf(value.X, fn)]; st != nil {
464
+ if def := st.Defaults[value.Sel.Name]; def != "" {
465
+ return []Resolved{{Value: def, At: at}}
466
+ }
467
+ }
468
+ case *ast.BinaryExpr:
469
+ if value.Op != token.ADD {
470
+ return nil
471
+ }
472
+ return s.concat(value.X, value.Y, fn, depth, visiting)
473
+ case *ast.CallExpr:
474
+ if ident, ok := value.Fun.(*ast.Ident); ok && ident.Name == "string" && len(value.Args) == 1 && s.Functions[fn.File.Pkg+".string"] == nil {
475
+ return s.Resolve(value.Args[0], fn, depth, visiting)
476
+ }
477
+ var out []Resolved
478
+ for _, target := range s.Callees(value, fn) {
479
+ if ret := SingleReturn(target); ret != nil {
480
+ key := "call:" + target.Key
481
+ if visiting[key] {
482
+ continue
483
+ }
484
+ visiting[key] = true
485
+ out = append(out, s.Resolve(ret, target, depth, visiting)...)
486
+ delete(visiting, key)
487
+ }
488
+ }
489
+ return out
490
+ }
491
+ return nil
492
+ }
493
+
494
+ // concat is every string `x + y` can be, when both halves resolve; a half
495
+ // that does not leaves the whole unresolved rather than half-guessed.
496
+ func (s *Index) concat(x, y ast.Expr, fn *Function, depth int, visiting map[string]bool) []Resolved {
497
+ left := s.Resolve(x, fn, depth, visiting)
498
+ if len(left) == 0 {
499
+ return nil
500
+ }
501
+ right := s.Resolve(y, fn, depth, visiting)
502
+ if len(right) == 0 {
503
+ return nil
504
+ }
505
+ var out []Resolved
506
+ for _, a := range left {
507
+ for _, b := range right {
508
+ out = append(out, Resolved{Value: a.Value + b.Value, At: a.At, Name: FirstNonEmpty(a.Name, b.Name)})
509
+ }
510
+ }
511
+ return out
512
+ }
513
+
514
+ // FromCallers is what a parameter is worth: whatever each call site passes
515
+ // for it, resolved there. When Companion names another parameter, a call
516
+ // site that passes a literal or constant for it is taken to name the
517
+ // message.
518
+ func (s *Index) FromCallers(fn *Function, index, depth int, visiting map[string]bool) []Resolved {
519
+ if depth >= s.Hops {
520
+ return nil
521
+ }
522
+ key := fn.Key + "#" + strconv.Itoa(index)
523
+ if visiting[key] {
524
+ return nil
525
+ }
526
+ visiting[key] = true
527
+ defer delete(visiting, key)
528
+
529
+ nameIndex := -1
530
+ if s.Companion != nil {
531
+ nameIndex = s.Companion(fn, index)
532
+ }
533
+ var out []Resolved
534
+ for _, arg := range s.ArgsFromCallers(fn, index) {
535
+ found := s.Resolve(arg.Expr, arg.Fn, depth+1, visiting)
536
+ if nameIndex >= 0 && nameIndex < len(arg.Call.Args) {
537
+ names := s.Resolve(arg.Call.Args[nameIndex], arg.Fn, depth+1, visiting)
538
+ if len(names) == 1 {
539
+ for i := range found {
540
+ found[i].Name = names[0].Value
541
+ }
542
+ }
543
+ }
544
+ out = append(out, found...)
545
+ }
546
+ return out
547
+ }
548
+
549
+ // ArgsFromCallers is every expression a caller passes for fn's parameter at
550
+ // index, each with the function it was written in. In a fixed order.
551
+ func (s *Index) ArgsFromCallers(fn *Function, index int) []CallArg {
552
+ var out []CallArg
553
+ for _, site := range s.CallSites(fn) {
554
+ if index < len(site.Call.Args) {
555
+ out = append(out, CallArg{Expr: site.Call.Args[index], Fn: site.Fn, Call: site.Call})
556
+ }
557
+ }
558
+ return out
559
+ }
560
+
561
+ // CallSites is every call in the tree that lands on fn, with the function
562
+ // it is written in. In a fixed order. Expr is nil: the site is the call.
563
+ func (s *Index) CallSites(fn *Function) []CallArg {
564
+ var out []CallArg
565
+ for _, caller := range s.SortedFunctions() {
566
+ ast.Inspect(caller.Decl.Body, func(node ast.Node) bool {
567
+ call, ok := node.(*ast.CallExpr)
568
+ if !ok || !s.Reaches(call, caller, fn) {
569
+ return true
570
+ }
571
+ out = append(out, CallArg{Fn: caller, Call: call})
572
+ return true
573
+ })
574
+ }
575
+ return out
576
+ }
577
+
578
+ // Reaches is whether a call lands on fn: directly, or through an interface
579
+ // fn's receiver satisfies.
580
+ func (s *Index) Reaches(call *ast.CallExpr, caller, fn *Function) bool {
581
+ for _, target := range s.Callees(call, caller) {
582
+ if target == fn {
583
+ return true
584
+ }
585
+ }
586
+ return false
587
+ }
588
+
589
+ // TypesOf is every concrete type an expression can have: TypeOf, or when the
590
+ // expression is a parameter of an interface or unnamed type, the types the
591
+ // callers pass for it, followed Hops levels up. Empty when nothing says.
592
+ func (s *Index) TypesOf(expr ast.Expr, fn *Function, depth int, visiting map[string]bool) []string {
593
+ if ident, ok := Unwrap(expr).(*ast.Ident); ok {
594
+ if index := ParamIndex(fn, ident.Name); index >= 0 && !s.isConcrete(fn.Types[ident.Name]) {
595
+ if depth >= s.Hops {
596
+ return nil
597
+ }
598
+ key := fn.Key + "#type:" + strconv.Itoa(index)
599
+ if visiting[key] {
600
+ return nil
601
+ }
602
+ visiting[key] = true
603
+ defer delete(visiting, key)
604
+ var out []string
605
+ seen := map[string]bool{}
606
+ for _, arg := range s.ArgsFromCallers(fn, index) {
607
+ for _, found := range s.TypesOf(arg.Expr, arg.Fn, depth+1, visiting) {
608
+ if !seen[found] {
609
+ seen[found] = true
610
+ out = append(out, found)
611
+ }
612
+ }
613
+ }
614
+ return out
615
+ }
616
+ }
617
+ if found := s.TypeOf(expr, fn); s.isConcrete(found) {
618
+ return []string{found}
619
+ }
620
+ return nil
621
+ }
622
+
623
+ // isConcrete is whether a type key names a non-interface type the tree
624
+ // declares. A library's interface - river.JobArgs - is not declared here and
625
+ // so is not concrete either: a parameter of that type says nothing about
626
+ // which job it carries, and the callers are asked.
627
+ func (s *Index) isConcrete(key string) bool {
628
+ return s.Named[key] && s.Interfaces[key] == nil
629
+ }
@@ -0,0 +1,66 @@
1
+ package goscan
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ func TestIndexSharesASTAndResolvesRealLocalPackageNames(t *testing.T) {
9
+ root := t.TempDir()
10
+ write(t, root, "go.mod", "module example.com/index\n")
11
+ write(t, root, "main.go", `package main
12
+ import "example.com/index/adapter/v2"
13
+ const Topic = wire.Topic
14
+ `)
15
+ write(t, root, "adapter/v2/messages.go", "package wire\nconst Topic = \"orders\"\n")
16
+ index, err := ReadWithOptions(root, ReadOptions{IncludeGenerated: true})
17
+ if err != nil {
18
+ t.Fatal(err)
19
+ }
20
+ files := index.PackageFiles(".")
21
+ if len(files) != 1 || files[0].Imports["wire"] != "example.com/index/adapter/v2" {
22
+ t.Fatalf("imports = %+v", files)
23
+ }
24
+ if index.ConstantString("example.com/index.Topic", nil) != "orders" {
25
+ t.Fatal("real import name did not resolve")
26
+ }
27
+ again, err := PackageIndex(root, ".", index)
28
+ if err != nil || again.PackageFiles(".")[0].Node != files[0].Node || again.Fset != index.Fset {
29
+ t.Fatal("AST was reparsed")
30
+ }
31
+ }
32
+
33
+ func TestBuildSelectionGeneratedCodeAndModuleBoundaries(t *testing.T) {
34
+ root := t.TempDir()
35
+ write(t, root, "go.mod", "module example.com/index\n")
36
+ write(t, root, "app_linux.go", "package app\n")
37
+ write(t, root, "app_windows.go", "package app\n")
38
+ write(t, root, "feature.go", "//go:build custom\n\npackage app\n")
39
+ write(t, root, "client.go", "// Code generated by fixture. DO NOT EDIT.\npackage app\n")
40
+ write(t, root, "child/go.mod", "module example.com/child\n")
41
+ write(t, root, "child/app.go", "package child\n")
42
+ write(t, root, "testdata/invalid.go", "not Go source")
43
+ index, err := ReadWithOptions(root, ReadOptions{GOOS: "linux", GOARCH: "amd64", IncludeGenerated: true, Tags: []string{"custom"}})
44
+ if err != nil {
45
+ t.Fatal(err)
46
+ }
47
+ if len(index.Files) != 3 {
48
+ t.Fatalf("files = %+v", index.Files)
49
+ }
50
+ generated := 0
51
+ for _, file := range index.Files {
52
+ if file.Generated {
53
+ generated++
54
+ }
55
+ if strings.Contains(file.Name, "windows") || strings.HasPrefix(file.Name, "child/") {
56
+ t.Fatalf("foreign source selected: %s", file.Name)
57
+ }
58
+ }
59
+ if generated != 1 {
60
+ t.Fatalf("generated = %d", generated)
61
+ }
62
+ syntax, err := ReadWithOptions(root, ReadOptions{GOOS: "linux", GOARCH: "amd64"})
63
+ if err != nil || len(syntax.Files) != 1 || syntax.Files[0].Name != "app_linux.go" {
64
+ t.Fatalf("syntax source selection: %+v / %v", syntax, err)
65
+ }
66
+ }