@shortlink-org/portolan 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +23 -0
  2. package/catalog/enum_test.go +46 -0
  3. package/catalog/evidence_test.go +35 -0
  4. package/catalog/model.go +1066 -0
  5. package/catalog/roundtrip_test.go +203 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/init.test.mjs +6 -1
  8. package/cli/portolan.mjs +8 -0
  9. package/cli/portolan.test.mjs +49 -0
  10. package/go.mod +14 -0
  11. package/go.sum +20 -0
  12. package/internal/gocall/README.md +19 -0
  13. package/internal/gocall/analyze.go +189 -0
  14. package/internal/gocall/analyze_test.go +107 -0
  15. package/internal/gohttp/analyze.go +2562 -0
  16. package/internal/gohttp/destination.go +373 -0
  17. package/internal/gohttp/endpoints.go +1067 -0
  18. package/internal/gohttp/roots.go +320 -0
  19. package/internal/gohttp/typed.go +96 -0
  20. package/internal/goscan/constants.go +85 -0
  21. package/internal/goscan/goscan_test.go +227 -0
  22. package/internal/goscan/index.go +629 -0
  23. package/internal/goscan/index_test.go +66 -0
  24. package/internal/goscan/names.go +52 -0
  25. package/internal/goscan/parse_test.go +11 -0
  26. package/internal/goscan/source.go +37 -0
  27. package/internal/goscan/tree.go +284 -0
  28. package/internal/goscan/types.go +99 -0
  29. package/internal/wsdl/ids.go +127 -0
  30. package/internal/wsdl/ids_test.go +21 -0
  31. package/internal/wsdl/model.go +70 -0
  32. package/internal/wsdl/parse.go +949 -0
  33. package/internal/wsdl/parse_test.go +170 -0
  34. package/package.json +22 -10
  35. package/plugin/describe.go +118 -0
  36. package/plugin/describe_test.go +114 -0
  37. package/plugin/protocol.go +141 -0
  38. package/plugin/schematest/schematest.go +126 -0
  39. package/plugins/README.md +114 -46
  40. package/plugins/cmd/portolan-http-clients/main.go +19 -0
  41. package/plugins/extract-celery/extract.py +0 -2
  42. package/plugins/extract-celery/extract_test.py +1 -1
  43. package/plugins/extract-django/README.md +39 -17
  44. package/plugins/extract-django/domain.py +28 -17
  45. package/plugins/extract-django/extract.py +21 -7
  46. package/plugins/extract-django/extract_test.py +55 -2
  47. package/plugins/extract-django/lifecycle.py +2 -0
  48. package/plugins/extract-django/operations.py +1 -1
  49. package/plugins/extract-django/routing_test.py +109 -1
  50. package/plugins/extract-django/store.py +1 -1
  51. package/plugins/extract-django/transport.py +101 -55
  52. package/plugins/extract-django/verbs.py +241 -0
  53. package/plugins/extract-go/README.md +47 -0
  54. package/plugins/extract-http-clients/describe.go +19 -0
  55. package/plugins/extract-http-clients/describe_test.go +11 -0
  56. package/plugins/extract-http-clients/extract.go +740 -0
  57. package/plugins/extract-http-clients/extract_test.go +1561 -0
  58. package/plugins/extract-http-clients/main.go +41 -0
  59. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  60. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  61. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  62. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  63. package/plugins/extract-python-kafka/README.md +6 -0
  64. package/plugins/extract-python-kafka/extract.py +2 -4
  65. package/plugins/extract-python-kafka/extract_test.py +18 -2
  66. package/plugins/extract-ts/extract.test.ts +2 -2
  67. package/plugins/extract-ts/extract.ts +4 -5
  68. package/plugins/extract-ts/graphql.test.ts +1 -1
  69. package/plugins/openapi/ids.go +261 -0
  70. package/plugins/openapi/ids_test.go +98 -0
  71. package/plugins/portolan-go.wasm +0 -0
  72. package/plugins/pyplugin/catalog.py +12 -1
  73. package/plugins/pyplugin/kafka.py +74 -3
  74. package/plugins/pyplugin/protocol.py +1 -5
  75. package/portolan.json +3 -2
  76. package/schema/portolan.schema.json +34 -0
  77. package/scripts/README.md +18 -12
  78. package/scripts/catalog-sources.mjs +6 -0
  79. package/scripts/delivery-presets.mjs +21 -11
  80. package/scripts/diff.mjs +5 -1
  81. package/scripts/django-aggregates.test.mjs +58 -0
  82. package/scripts/gen-likec4.mjs +79 -21
  83. package/scripts/gen-likec4.test.mjs +25 -2
  84. package/scripts/gen.mjs +118 -115
  85. package/scripts/go-discovery.test.mjs +30 -0
  86. package/scripts/history.mjs +186 -3
  87. package/scripts/history.test.mjs +1 -1
  88. package/scripts/host-plugins/fetch-git.mjs +77 -21
  89. package/scripts/host-plugins/fetch-git.test.mjs +62 -8
  90. package/scripts/local-api.mjs +71 -4
  91. package/scripts/local-api.test.mjs +63 -4
  92. package/scripts/local-discovery.mjs +82 -9
  93. package/scripts/manifest.mjs +5 -3
  94. package/scripts/manifest.test.mjs +24 -0
  95. package/scripts/output-diff.mjs +94 -0
  96. package/scripts/output-diff.test.mjs +36 -0
  97. package/scripts/package-smoke.mjs +62 -4
  98. package/scripts/plugin-host.mjs +22 -2
  99. package/scripts/plugin-host.test.mjs +9 -0
  100. package/scripts/plugin-wasm-worker.mjs +4 -1
  101. package/scripts/provenance.mjs +72 -0
  102. package/scripts/provenance.test.mjs +149 -0
  103. package/scripts/run-builtin.mjs +39 -5
  104. package/scripts/schema.mjs +29 -0
  105. package/scripts/warning-policy.mjs +161 -0
  106. package/scripts/warning-policy.test.mjs +56 -0
  107. package/src/app/Breadcrumbs.test.ts +4 -0
  108. package/src/app/Breadcrumbs.tsx +1 -0
  109. package/src/app/Sidebar.tsx +3 -3
  110. package/src/catalog-docs.test.ts +64 -0
  111. package/src/catalog-docs.ts +35 -0
  112. package/src/catalog-error.test.ts +15 -0
  113. package/src/catalog-model.ts +70 -6
  114. package/src/catalog-stores.test.ts +17 -0
  115. package/src/catalog-validation.ts +52 -2
  116. package/src/catalog.test.ts +13 -2
  117. package/src/chat/Starter.tsx +5 -11
  118. package/src/chat/tools.test.ts +27 -0
  119. package/src/chat/tools.ts +5 -9
  120. package/src/components/CatalogStamp.tsx +10 -8
  121. package/src/components/ChannelRows.messagepack.test.tsx +28 -0
  122. package/src/components/ChannelRows.test.tsx +54 -0
  123. package/src/components/ChannelRows.tsx +57 -10
  124. package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
  125. package/src/components/HTTPDestinationEvidence.tsx +31 -0
  126. package/src/components/Integrations.tsx +1 -1
  127. package/src/components/LifecycleDiagram.tsx +28 -12
  128. package/src/components/MachineDocs.tsx +6 -5
  129. package/src/components/MethodRows.tsx +9 -2
  130. package/src/components/ProblemRow.tsx +4 -0
  131. package/src/components/RelationEvidence.test.tsx +14 -0
  132. package/src/components/RelationEvidence.tsx +53 -0
  133. package/src/components/WhatLinksHere.tsx +6 -4
  134. package/src/data.ts +25 -7
  135. package/src/enrich.test.ts +336 -6
  136. package/src/enrich.ts +206 -3
  137. package/src/flow/StepDetail.tsx +104 -54
  138. package/src/flow/answers.test.ts +18 -1
  139. package/src/flow/answers.ts +37 -8
  140. package/src/flow/evidence.test.ts +16 -0
  141. package/src/flow/evidence.ts +34 -0
  142. package/src/index.css +44 -0
  143. package/src/landing/DraggableReveal.tsx +3 -2
  144. package/src/landing/EvidencePipeline.tsx +105 -0
  145. package/src/landing/LandingPage.tsx +2 -59
  146. package/src/lib/backlinks.test.ts +16 -1
  147. package/src/lib/backlinks.ts +20 -0
  148. package/src/lib/catalog-diff.test.ts +18 -0
  149. package/src/lib/catalog-diff.ts +20 -2
  150. package/src/lib/derive.ts +1 -0
  151. package/src/lib/django-aggregates.d.mts +9 -0
  152. package/src/lib/django-aggregates.mjs +36 -0
  153. package/src/lib/django-aggregates.test.ts +29 -0
  154. package/src/lib/django-aggregates.ts +5 -0
  155. package/src/lib/kafka-ui.test.ts +87 -0
  156. package/src/lib/kafka-ui.ts +105 -0
  157. package/src/lib/local-api.ts +20 -2
  158. package/src/lib/setup-info.test.ts +17 -0
  159. package/src/lib/setup-info.ts +58 -0
  160. package/src/lib/warnings.test.ts +54 -0
  161. package/src/lib/warnings.ts +260 -0
  162. package/src/lib/wire-problems.test.ts +21 -0
  163. package/src/lib/wire-problems.ts +62 -1
  164. package/src/likec4/FlowView.tsx +2 -6
  165. package/src/likec4/flow-edges.test.ts +64 -1
  166. package/src/likec4/flow-edges.ts +43 -7
  167. package/src/likec4/view-index.ts +8 -2
  168. package/src/map/ContextMapGraph.tsx +76 -32
  169. package/src/merge.test.ts +23 -0
  170. package/src/merge.ts +33 -10
  171. package/src/pages/AggregatePage.tsx +8 -7
  172. package/src/pages/CatalogFailure.tsx +2 -2
  173. package/src/pages/ContextPage.tsx +6 -5
  174. package/src/pages/ServicePage.tsx +4 -3
  175. package/src/pages/Settings.tsx +200 -44
  176. package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
  177. package/src/pages/settings/IntegrationsSettings.tsx +117 -0
  178. package/src/routes.test.ts +2 -0
  179. package/src/routes.ts +2 -1
  180. package/src/selection/DetailPanel.tsx +61 -1
  181. package/src/virtual-provenance.d.ts +11 -0
  182. package/vite.config.ts +5 -0
  183. package/scripts/vendor-lock.mjs +0 -58
  184. package/scripts/vendor-lock.test.mjs +0 -69
@@ -0,0 +1,52 @@
1
+ package goscan
2
+
3
+ import "strings"
4
+
5
+ // LastSegment is what follows the last dot: the name off a type key, the
6
+ // event off a wire name.
7
+ func LastSegment(value string) string {
8
+ if at := strings.LastIndex(value, "."); at >= 0 {
9
+ return value[at+1:]
10
+ }
11
+ return value
12
+ }
13
+
14
+ // Slug is a value as an identifier in a URL or a catalog id: lower case,
15
+ // runs of anything else collapsed to one dash, none at the ends.
16
+ func Slug(value string) string {
17
+ value = strings.ToLower(strings.TrimSpace(value))
18
+ var out strings.Builder
19
+ dash := false
20
+ for _, r := range value {
21
+ if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' {
22
+ out.WriteRune(r)
23
+ dash = false
24
+ } else if out.Len() > 0 && !dash {
25
+ out.WriteByte('-')
26
+ dash = true
27
+ }
28
+ }
29
+ return strings.TrimSuffix(out.String(), "-")
30
+ }
31
+
32
+ // Title is a slug or an identifier as words: split on dash, underscore and
33
+ // dot, each capitalised.
34
+ func Title(value string) string {
35
+ parts := strings.FieldsFunc(value, func(r rune) bool { return r == '-' || r == '_' || r == '.' })
36
+ for i := range parts {
37
+ if parts[i] != "" {
38
+ parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:]
39
+ }
40
+ }
41
+ return strings.Join(parts, " ")
42
+ }
43
+
44
+ // FirstNonEmpty is the first value that is more than whitespace.
45
+ func FirstNonEmpty(values ...string) string {
46
+ for _, value := range values {
47
+ if strings.TrimSpace(value) != "" {
48
+ return value
49
+ }
50
+ }
51
+ return ""
52
+ }
@@ -0,0 +1,11 @@
1
+ package goscan
2
+
3
+ import (
4
+ "go/ast"
5
+ "go/parser"
6
+ )
7
+
8
+ // parseExpr is go/parser's, named here so a test reads as a table.
9
+ func parseExpr(src string) (ast.Expr, error) {
10
+ return parser.ParseExpr(src)
11
+ }
@@ -0,0 +1,37 @@
1
+ package goscan
2
+
3
+ import (
4
+ "fmt"
5
+ "go/ast"
6
+ )
7
+
8
+ // Source is a place in the tree, the way the catalog cites one: the file
9
+ // relative to the root, and the line when there is one.
10
+ type Source struct {
11
+ File string
12
+ Line int
13
+ }
14
+
15
+ func (s Source) String() string {
16
+ if s.Line == 0 {
17
+ return s.File
18
+ }
19
+ return fmt.Sprintf("%s:%d", s.File, s.Line)
20
+ }
21
+
22
+ // File is one parsed file: its path under the root, the import path of its
23
+ // package, its imports by local name, and the syntax.
24
+ type File struct {
25
+ Generated bool
26
+ Name string
27
+ Pkg string
28
+ Imports map[string]string
29
+ Node *ast.File
30
+ }
31
+
32
+ // ConstExpr is a constant as it was declared: the expression, and the file it
33
+ // is in, since the expression's names mean what that file's imports say.
34
+ type ConstExpr struct {
35
+ Expr ast.Expr
36
+ File *File
37
+ }
@@ -0,0 +1,284 @@
1
+ // Package goscan reads a Go source tree the way the extractors need it: every
2
+ // non-test, non-generated file parsed once, each with the import path of its
3
+ // package and its imports by local name, and the string constants of the tree
4
+ // indexed so that a topic, a queue or a subject named by a constant resolves to
5
+ // the literal behind it.
6
+ //
7
+ // It is the part of a Go extractor that has nothing to do with what the
8
+ // extractor is looking for. River, Watermill and go-nats read the tree this
9
+ // way, and the next Go extractor should not have to copy it a fourth time.
10
+ //
11
+ // Index, over a Tree, is the second shared part: the structs with their field
12
+ // types and tag defaults, the interfaces by method name, and every function
13
+ // with its parameters and results, so that a value - a subject, a topic, a
14
+ // queue, a job type - is followed through locals, fields, constructors and
15
+ // parameters up to the callers that pass it, the same way in each extractor.
16
+ package goscan
17
+
18
+ import (
19
+ "fmt"
20
+ "go/ast"
21
+ "go/build"
22
+ "go/parser"
23
+ "go/token"
24
+ "io/fs"
25
+ "os"
26
+ "path"
27
+ "path/filepath"
28
+ "runtime"
29
+ "sort"
30
+ "strconv"
31
+ "strings"
32
+ )
33
+
34
+ // ReadOptions controls a source snapshot. Generated declarations can be used
35
+ // for type/contract resolution without treating them as handwritten behavior.
36
+ type ReadOptions struct {
37
+ IncludeGenerated bool
38
+ AllowPartial bool
39
+ GOOS string
40
+ GOARCH string
41
+ Tags []string
42
+ // Directories restricts parsing to these module-relative directories.
43
+ Directories []string
44
+ }
45
+
46
+ // Tree is one Go module's source, read once.
47
+ type Tree struct {
48
+ Diagnostics []string
49
+ ByDir map[string][]*File
50
+ Options ReadOptions
51
+
52
+ Root string
53
+ Module string
54
+ Fset *token.FileSet
55
+ // Files in path order, so that whatever is read off them comes out in the
56
+ // same order every run.
57
+ Files []*File
58
+ // Constants is every constant of the tree by "<import path>.<Name>", as
59
+ // the expression that declared it, so a constant defined through another
60
+ // is followed rather than copied.
61
+ Constants map[string]ConstExpr
62
+ // Foreign is what an identifier from a package outside the tree is worth,
63
+ // when the extractor knows: river.QueueDefault is "default", and nothing
64
+ // in the tree says so. Nil when there is nothing to say.
65
+ Foreign func(importPath, name string) (string, bool)
66
+ }
67
+
68
+ // Read parses the tree under root. Test files, generated files, vendored
69
+ // code and dot-directories are left out: they are not what the service
70
+ // says about itself.
71
+ func Read(root string) (*Tree, error) { return ReadWithOptions(root, ReadOptions{}) }
72
+
73
+ func ReadWithOptions(root string, options ReadOptions) (*Tree, error) {
74
+ root = filepath.Clean(root)
75
+ t := &Tree{Root: root, Module: ModulePath(root), Fset: token.NewFileSet(), Constants: map[string]ConstExpr{}, ByDir: map[string][]*File{}, Options: options}
76
+ buildContext := build.Default
77
+ if options.GOOS != "" {
78
+ buildContext.GOOS = options.GOOS
79
+ } else if target := os.Getenv("GOOS"); target != "" {
80
+ buildContext.GOOS = target
81
+ }
82
+ if options.GOARCH != "" {
83
+ buildContext.GOARCH = options.GOARCH
84
+ } else if target := os.Getenv("GOARCH"); target != "" {
85
+ buildContext.GOARCH = target
86
+ }
87
+ buildContext.BuildTags = append([]string(nil), options.Tags...)
88
+ // The WASI host supplies the target platform. Never silently analyze the
89
+ // wasip1 runtime's file set when running the syntax extractor as a plugin.
90
+ matchBuild := runtime.GOOS != "wasip1" || options.GOOS != "" || os.Getenv("GOOS") != ""
91
+ allowed := map[string]bool{}
92
+ for _, dir := range options.Directories {
93
+ allowed[filepath.ToSlash(filepath.Clean(dir))] = true
94
+ }
95
+
96
+ var names []string
97
+ err := filepath.WalkDir(root, func(name string, entry fs.DirEntry, err error) error {
98
+ if err != nil {
99
+ return err
100
+ }
101
+ if entry.IsDir() {
102
+ if name != root {
103
+ if _, err := os.Stat(filepath.Join(name, "go.mod")); err == nil {
104
+ return filepath.SkipDir
105
+ }
106
+ }
107
+ if name != root && strings.HasPrefix(entry.Name(), ".") {
108
+ return filepath.SkipDir
109
+ }
110
+ switch entry.Name() {
111
+ case ".git", ".portolan", "node_modules", "vendor", "testdata":
112
+ if name != root {
113
+ return filepath.SkipDir
114
+ }
115
+ }
116
+ return nil
117
+ }
118
+ base := entry.Name()
119
+ if !strings.HasSuffix(base, ".go") || strings.HasSuffix(base, "_test.go") {
120
+ return nil
121
+ }
122
+ rel, _ := filepath.Rel(root, filepath.Dir(name))
123
+ if len(allowed) > 0 && !allowed[filepath.ToSlash(rel)] {
124
+ return nil
125
+ }
126
+ if matchBuild {
127
+ match, err := buildContext.MatchFile(filepath.Dir(name), base)
128
+ if err != nil {
129
+ return err
130
+ }
131
+ if !match {
132
+ return nil
133
+ }
134
+ }
135
+ if !options.IncludeGenerated && generatedName(base) {
136
+ return nil
137
+ }
138
+ names = append(names, name)
139
+ return nil
140
+ })
141
+ if err != nil {
142
+ return nil, err
143
+ }
144
+ sort.Strings(names)
145
+
146
+ for _, name := range names {
147
+ node, err := parser.ParseFile(t.Fset, name, nil, parser.ParseComments)
148
+ if err != nil {
149
+ if !options.AllowPartial {
150
+ return nil, fmt.Errorf("parse %s: %w", name, err)
151
+ }
152
+ t.Diagnostics = append(t.Diagnostics, fmt.Sprintf("parse %s: %v", name, err))
153
+ continue
154
+ }
155
+ generated := IsGenerated(name, node)
156
+ if generated && !options.IncludeGenerated {
157
+ continue
158
+ }
159
+ rel, _ := filepath.Rel(root, name)
160
+ t.Files = append(t.Files, &File{
161
+ Name: filepath.ToSlash(rel),
162
+ Generated: generated,
163
+ Pkg: t.PackagePath(filepath.Dir(name)),
164
+ Imports: ImportsOf(node),
165
+ Node: node,
166
+ })
167
+ }
168
+ // Index actual local package names after all declarations are known.
169
+ namesByImport := map[string]string{}
170
+ for _, file := range t.Files {
171
+ namesByImport[file.Pkg] = file.Node.Name.Name
172
+ dir := filepath.ToSlash(filepath.Dir(file.Name))
173
+ t.ByDir[dir] = append(t.ByDir[dir], file)
174
+ }
175
+ for _, file := range t.Files {
176
+ for _, spec := range file.Node.Imports {
177
+ if spec.Name != nil {
178
+ continue
179
+ }
180
+ imported, _ := strconv.Unquote(spec.Path.Value)
181
+ if name := namesByImport[imported]; name != "" {
182
+ delete(file.Imports, path.Base(imported))
183
+ file.Imports[name] = imported
184
+ }
185
+ }
186
+ }
187
+ t.indexConstants()
188
+
189
+ return t, nil
190
+ }
191
+
192
+ // PackagePath is the import path of the package in dir: the module path
193
+ // with the directory under it.
194
+ func (t *Tree) PackagePath(dir string) string {
195
+ rel, err := filepath.Rel(t.Root, dir)
196
+ if err != nil || rel == "." {
197
+ return t.Module
198
+ }
199
+ return strings.TrimSuffix(t.Module, "/") + "/" + filepath.ToSlash(rel)
200
+ }
201
+
202
+ // At is where a node is, relative to the root.
203
+ func (t *Tree) At(pos token.Pos) Source {
204
+ position := t.Fset.Position(pos)
205
+ rel, err := filepath.Rel(t.Root, position.Filename)
206
+ if err != nil {
207
+ rel = position.Filename
208
+ }
209
+ return Source{File: filepath.ToSlash(rel), Line: position.Line}
210
+ }
211
+
212
+ // ModulePath is what go.mod under root calls the module; the cleaned root
213
+ // when there is no go.mod to ask.
214
+ func ModulePath(root string) string {
215
+ data, err := os.ReadFile(filepath.Join(root, "go.mod"))
216
+ if err == nil {
217
+ for _, line := range strings.Split(string(data), "\n") {
218
+ if value, ok := strings.CutPrefix(strings.TrimSpace(line), "module "); ok {
219
+ return strings.TrimSpace(value)
220
+ }
221
+ }
222
+ }
223
+ return filepath.ToSlash(filepath.Clean(root))
224
+ }
225
+
226
+ // ImportsOf is a file's imports by the name the file uses for them: the alias
227
+ // when there is one, the last path segment when there is not. Blank and dot
228
+ // imports keep the segment, since nothing in the file refers to them by name.
229
+ //
230
+ // A segment with a dot in it - `nats.go`, `yaml.v3` - is not a Go identifier,
231
+ // and the package behind it is called by what comes before the dot; that name
232
+ // is recorded too, since it is the one the file uses.
233
+ func ImportsOf(node *ast.File) map[string]string {
234
+ out := map[string]string{}
235
+ for _, spec := range node.Imports {
236
+ value, err := strconv.Unquote(spec.Path.Value)
237
+ if err != nil {
238
+ continue
239
+ }
240
+ name := path.Base(value)
241
+ if spec.Name != nil && spec.Name.Name != "_" && spec.Name.Name != "." {
242
+ name = spec.Name.Name
243
+ }
244
+ out[name] = value
245
+ if short, _, dotted := strings.Cut(name, "."); dotted && short != "" {
246
+ out[short] = value
247
+ }
248
+ }
249
+ return out
250
+ }
251
+
252
+ func generatedName(name string) bool {
253
+ return strings.HasSuffix(name, ".gen.go") || strings.HasSuffix(name, "_generated.go") || strings.HasSuffix(name, ".generated.go")
254
+ }
255
+
256
+ func IsGenerated(name string, file *ast.File) bool {
257
+ return generatedName(name) || ast.IsGenerated(file)
258
+ }
259
+
260
+ // PackageFiles and PackageDirs use the same immutable source selection in
261
+ // every consumer; repeated lookups reuse the AST and token positions.
262
+ func (t *Tree) PackageFiles(dir string) []*File {
263
+ return t.ByDir[filepath.ToSlash(filepath.Clean(dir))]
264
+ }
265
+ func (t *Tree) PackageDirs(prefix string) []string {
266
+ prefix = filepath.ToSlash(filepath.Clean(prefix))
267
+ var dirs []string
268
+ for dir := range t.ByDir {
269
+ if prefix == "." || dir == prefix || strings.HasPrefix(dir, prefix+"/") {
270
+ dirs = append(dirs, dir)
271
+ }
272
+ }
273
+ sort.Strings(dirs)
274
+ return dirs
275
+ }
276
+
277
+ // PackageIndex also serves small standalone readers and tests. Extractors
278
+ // should pass their request-scoped index to reuse parsing across passes.
279
+ func PackageIndex(root, dir string, indexes ...*Tree) (*Tree, error) {
280
+ if len(indexes) > 0 && indexes[0] != nil {
281
+ return indexes[0], nil
282
+ }
283
+ return ReadWithOptions(root, ReadOptions{IncludeGenerated: true, AllowPartial: true})
284
+ }
@@ -0,0 +1,99 @@
1
+ package goscan
2
+
3
+ import (
4
+ "bytes"
5
+ "go/ast"
6
+ "go/printer"
7
+ "reflect"
8
+ "strconv"
9
+ "strings"
10
+ )
11
+
12
+ // Unwrap takes the parentheses, the address-of and the pointer off an
13
+ // expression, down to the thing itself.
14
+ func Unwrap(expr ast.Expr) ast.Expr {
15
+ for {
16
+ switch value := expr.(type) {
17
+ case *ast.ParenExpr:
18
+ expr = value.X
19
+ case *ast.UnaryExpr:
20
+ expr = value.X
21
+ case *ast.StarExpr:
22
+ expr = value.X
23
+ default:
24
+ return expr
25
+ }
26
+ }
27
+ }
28
+
29
+ // TypeKey names a type the way the tree indexes types: "<import path>.<Name>"
30
+ // for a named type, whether written bare or through an import; the builtin's
31
+ // own name; "[]T" and "map[K]V" over their parts; "interface" for any
32
+ // interface literal. Empty for a type this reader does not name.
33
+ func (t *Tree) TypeKey(expr ast.Expr, file *File) string {
34
+ if expr == nil {
35
+ return ""
36
+ }
37
+ switch value := Unwrap(expr).(type) {
38
+ case *ast.Ident:
39
+ if IsBuiltin(value.Name) {
40
+ return value.Name
41
+ }
42
+ return file.Pkg + "." + value.Name
43
+ case *ast.SelectorExpr:
44
+ if base, ok := value.X.(*ast.Ident); ok {
45
+ return file.Imports[base.Name] + "." + value.Sel.Name
46
+ }
47
+ case *ast.ArrayType:
48
+ return "[]" + t.TypeKey(value.Elt, file)
49
+ case *ast.MapType:
50
+ return "map[" + t.TypeKey(value.Key, file) + "]" + t.TypeKey(value.Value, file)
51
+ case *ast.InterfaceType:
52
+ return "interface"
53
+ }
54
+ return ""
55
+ }
56
+
57
+ // IsBuiltin is whether a bare identifier is one of the language's own types.
58
+ func IsBuiltin(name string) bool {
59
+ switch name {
60
+ case "string", "bool", "byte", "rune", "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "float32", "float64", "error", "any":
61
+ return true
62
+ }
63
+ return false
64
+ }
65
+
66
+ // FieldsOf lists a struct's named fields as "name type", the name being what
67
+ // the json tag says when it says anything, and a field tagged "-" left out:
68
+ // the fields as they travel, not as the code calls them.
69
+ func (t *Tree) FieldsOf(body *ast.StructType) []string {
70
+ var out []string
71
+ for _, field := range body.Fields.List {
72
+ if len(field.Names) == 0 {
73
+ continue
74
+ }
75
+ typeName := t.PrintNode(field.Type)
76
+ for _, ident := range field.Names {
77
+ name := ident.Name
78
+ if field.Tag != nil {
79
+ tagText, _ := strconv.Unquote(field.Tag.Value)
80
+ jsonName := strings.Split(reflect.StructTag(tagText).Get("json"), ",")[0]
81
+ if jsonName == "-" {
82
+ continue
83
+ }
84
+ if jsonName != "" {
85
+ name = jsonName
86
+ }
87
+ }
88
+ out = append(out, name+" "+typeName)
89
+ }
90
+ }
91
+ return out
92
+ }
93
+
94
+ // PrintNode is a node as source text.
95
+ func (t *Tree) PrintNode(node ast.Node) string {
96
+ var out bytes.Buffer
97
+ _ = printer.Fprint(&out, t.Fset, node)
98
+ return out.String()
99
+ }
@@ -0,0 +1,127 @@
1
+ package wsdl
2
+
3
+ import (
4
+ "path/filepath"
5
+ "regexp"
6
+ "strconv"
7
+ "strings"
8
+ "unicode"
9
+ )
10
+
11
+ var versionInSource = regexp.MustCompile(`(?i)(?:^|[_-])(\d+(?:\.\d+)+)(?:[_-]|\.wsdl$)`)
12
+
13
+ // APIID is the stable prefix shared by the contract extractor and call-site
14
+ // analyzer. WSDL has no standard version field, so the protocol is the suffix.
15
+ func APIID(contract Contract) string {
16
+ name := Slug(contract.Name)
17
+ if name == "" {
18
+ name = "soap"
19
+ }
20
+ return name + ".soap"
21
+ }
22
+
23
+ // APIIDs resolves the case WSDL itself cannot: two checked-in versions may
24
+ // repeat the same service, port and targetNamespace. Only colliding interface
25
+ // ids receive a source-backed version suffix, so an unrelated file rename does
26
+ // not churn stable ids.
27
+ func APIIDs(contracts []Contract) map[string]string {
28
+ bases := map[string]int{}
29
+ for _, contract := range contracts {
30
+ base := APIID(contract)
31
+ for _, iface := range contract.Interfaces {
32
+ bases[InterfaceID(base, iface)]++
33
+ }
34
+ }
35
+ out := map[string]string{}
36
+ for index, contract := range contracts {
37
+ base := APIID(contract)
38
+ collides := false
39
+ for _, iface := range contract.Interfaces {
40
+ if bases[InterfaceID(base, iface)] > 1 {
41
+ collides = true
42
+ break
43
+ }
44
+ }
45
+ if collides {
46
+ version := sourceVersion(contract.Source)
47
+ if version == "" {
48
+ version = Slug(strings.TrimSuffix(filepath.Base(contract.Source), filepath.Ext(contract.Source)))
49
+ }
50
+ if version == "" {
51
+ version = "copy-" + strconv.Itoa(index+1)
52
+ }
53
+ base += ".v" + strings.ReplaceAll(strings.TrimPrefix(version, "v"), ".", "-")
54
+ }
55
+ out[ContractKey(contract)] = base
56
+ }
57
+ return out
58
+ }
59
+
60
+ func ContractKey(contract Contract) string {
61
+ return contract.Source + "\x00" + contract.Name
62
+ }
63
+
64
+ func sourceVersion(source string) string {
65
+ match := versionInSource.FindStringSubmatch(filepath.Base(source))
66
+ if len(match) > 1 {
67
+ return match[1]
68
+ }
69
+ return ""
70
+ }
71
+
72
+ // InterfaceID keeps distinct WSDL ports distinct. A document commonly exposes
73
+ // SOAP 1.1 and 1.2 ports with the same operations but different bindings.
74
+ func InterfaceID(api string, iface Interface) string {
75
+ name := title(iface.Name)
76
+ if name == "" {
77
+ name = title(iface.PortType)
78
+ }
79
+ if name == "" {
80
+ return api
81
+ }
82
+ return api + "." + name
83
+ }
84
+
85
+ func CallID(api string, iface Interface, operation Operation) string {
86
+ return InterfaceID(api, iface) + "/" + operation.Name
87
+ }
88
+
89
+ func ExternalID(contract Contract) string {
90
+ id := Slug(contract.Name)
91
+ id = strings.TrimSuffix(id, "-service")
92
+ id = strings.TrimSuffix(id, "service")
93
+ id = strings.TrimSuffix(id, "-soap")
94
+ if id == "" {
95
+ return "soap-peer"
96
+ }
97
+ return id
98
+ }
99
+
100
+ func Slug(value string) string {
101
+ var out strings.Builder
102
+ dash := false
103
+ for _, r := range strings.ToLower(strings.TrimSpace(value)) {
104
+ if unicode.IsLetter(r) || unicode.IsDigit(r) {
105
+ out.WriteRune(r)
106
+ dash = false
107
+ } else if out.Len() > 0 && !dash {
108
+ out.WriteByte('-')
109
+ dash = true
110
+ }
111
+ }
112
+ return strings.Trim(out.String(), "-")
113
+ }
114
+
115
+ func title(value string) string {
116
+ var out strings.Builder
117
+ for _, word := range strings.FieldsFunc(value, func(r rune) bool {
118
+ return r == '-' || r == '_' || unicode.IsSpace(r)
119
+ }) {
120
+ runes := []rune(word)
121
+ if len(runes) > 0 {
122
+ runes[0] = unicode.ToUpper(runes[0])
123
+ out.WriteString(string(runes))
124
+ }
125
+ }
126
+ return out.String()
127
+ }
@@ -0,0 +1,21 @@
1
+ package wsdl
2
+
3
+ import "testing"
4
+
5
+ func TestAPIIDsOnlyVersionCollidingContracts(t *testing.T) {
6
+ contracts := []Contract{
7
+ {Name: "BookingService", Source: "booking_1.0_4.0.wsdl", Interfaces: []Interface{{Name: "BookingPort"}}},
8
+ {Name: "BookingService", Source: "booking_1.2_4.0.wsdl", Interfaces: []Interface{{Name: "BookingPort"}}},
9
+ {Name: "StatusService", Source: "status.wsdl", Interfaces: []Interface{{Name: "StatusPort"}}},
10
+ }
11
+ ids := APIIDs(contracts)
12
+ if got := ids[ContractKey(contracts[0])]; got != "bookingservice.soap.v1-0" {
13
+ t.Fatalf("v1 id = %q", got)
14
+ }
15
+ if got := ids[ContractKey(contracts[1])]; got != "bookingservice.soap.v1-2" {
16
+ t.Fatalf("v1.2 id = %q", got)
17
+ }
18
+ if got := ids[ContractKey(contracts[2])]; got != "statusservice.soap" {
19
+ t.Fatalf("unrelated id = %q", got)
20
+ }
21
+ }
@@ -0,0 +1,70 @@
1
+ // Package wsdl reads local WSDL 1.1 contracts and the XML Schemas they reach.
2
+ //
3
+ // It deliberately performs no network access. A remote import is evidence that
4
+ // part of a contract is unavailable in the checkout, and is returned as a
5
+ // warning rather than fetched behind the caller's back.
6
+ package wsdl
7
+
8
+ // Contract is one WSDL service, or one standalone port type when the document
9
+ // does not declare a service.
10
+ type Contract struct {
11
+ Name string
12
+ Namespace string
13
+ Summary string
14
+ Source string
15
+ Interfaces []Interface
16
+ }
17
+
18
+ // Interface is a WSDL port bound to one SOAP binding. Keeping the port rather
19
+ // than flattening the whole document preserves distinct endpoints and SOAP
20
+ // versions when a service publishes more than one.
21
+ type Interface struct {
22
+ Name string
23
+ Binding string
24
+ PortType string
25
+ Endpoint string
26
+ Version string
27
+ Style string
28
+ Operations []Operation
29
+ Messages []Message
30
+ }
31
+
32
+ // Operation joins the abstract portType operation to its concrete SOAP
33
+ // binding. Request, response and faults name entries in Interface.Messages.
34
+ type Operation struct {
35
+ Name string
36
+ Action string
37
+ Doc string
38
+ Request string
39
+ Response string
40
+ Faults []string
41
+ Headers []string
42
+ }
43
+
44
+ // Message is a WSDL message or an XSD complex type reachable from one.
45
+ type Message struct {
46
+ Name string
47
+ Fields []Field
48
+ }
49
+
50
+ // Field is the compact XML shape the catalog can display.
51
+ type Field struct {
52
+ Name string
53
+ Type string
54
+ Doc string
55
+ }
56
+
57
+ // Result is deterministic: contracts, interfaces, operations and messages are
58
+ // sorted by the parser before they leave the package.
59
+ type Result struct {
60
+ Contracts []Contract
61
+ // Warnings are what stopped a document from being read in full: a remote
62
+ // import, a port whose binding is not here. They matter to anyone reading
63
+ // the contracts, including a client extractor resolving calls against them.
64
+ Warnings []string
65
+ // SchemaWarnings are about the quality of the schemas themselves, such as
66
+ // one name declared twice in a namespace. Nothing failed to resolve; the
67
+ // loader kept the first and says so. They belong to whoever publishes the
68
+ // contracts, not to every extractor that happens to read them.
69
+ SchemaWarnings []string
70
+ }