@descryy/adapter-go 0.4.1 → 0.5.1
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/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +98 -10
- package/dist/adapter.js.map +1 -1
- package/dist/client-base.d.ts +83 -0
- package/dist/client-base.d.ts.map +1 -0
- package/dist/client-base.js +79 -0
- package/dist/client-base.js.map +1 -0
- package/dist/client.d.ts +26 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +208 -2
- package/dist/client.js.map +1 -1
- package/dist/extract.d.ts +53 -0
- package/dist/extract.d.ts.map +1 -1
- package/dist/extract.js +443 -25
- package/dist/extract.js.map +1 -1
- package/dist/go-types.d.ts +75 -0
- package/dist/go-types.d.ts.map +1 -0
- package/dist/go-types.js +198 -0
- package/dist/go-types.js.map +1 -0
- package/dist/gorm.d.ts +81 -0
- package/dist/gorm.d.ts.map +1 -0
- package/dist/gorm.js +140 -0
- package/dist/gorm.js.map +1 -0
- package/dist/grpc.d.ts +29 -0
- package/dist/grpc.d.ts.map +1 -1
- package/dist/grpc.js +21 -2
- package/dist/grpc.js.map +1 -1
- package/dist/parse.d.ts +41 -0
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +134 -16
- package/dist/parse.js.map +1 -1
- package/dist/routes.d.ts +18 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +156 -19
- package/dist/routes.js.map +1 -1
- package/goshapes/go.mod +17 -0
- package/goshapes/main.go +596 -0
- package/package.json +8 -7
package/goshapes/main.go
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
// Command goshapes is descry-adapters' R3 evidence source for Go: real
|
|
2
|
+
// type-checker output, not a heuristic. It loads a target Go module with
|
|
3
|
+
// golang.org/x/tools/go/packages (which shells out to `go list`/the Go
|
|
4
|
+
// toolchain already on the analysing machine), and for every function whose
|
|
5
|
+
// signature matches a recognised HTTP-framework handler, walks the
|
|
6
|
+
// function's own body — through go/types' resolved type information, not
|
|
7
|
+
// syntax alone — to find which named struct type it decodes an inbound
|
|
8
|
+
// request into and which it encodes as the response.
|
|
9
|
+
//
|
|
10
|
+
// That join (handler -> request/response struct) is exactly the capability
|
|
11
|
+
// gap `descry-adapters`' prior investigation named: Go carries no written
|
|
12
|
+
// annotation the way Python's Pydantic or Java's JSON libraries do, so
|
|
13
|
+
// finding it requires tracing what a variable holds through a Decode/Bind or
|
|
14
|
+
// Encode/JSON call — a data-flow fact only a type checker can state
|
|
15
|
+
// honestly. Nothing here is invented when the evidence is ambiguous: more
|
|
16
|
+
// than one candidate shape for one handler's request (or response) is
|
|
17
|
+
// reported as unresolved rather than guessed, matching this project's
|
|
18
|
+
// precision-over-recall rule.
|
|
19
|
+
//
|
|
20
|
+
// Output is one JSON object on stdout, always exit 0 (including on
|
|
21
|
+
// failure — failure is a value in the JSON, `"ok": false`, not a process
|
|
22
|
+
// exit code the caller has to distinguish from "the helper itself crashed").
|
|
23
|
+
package main
|
|
24
|
+
|
|
25
|
+
import (
|
|
26
|
+
"encoding/json"
|
|
27
|
+
"fmt"
|
|
28
|
+
"go/ast"
|
|
29
|
+
"go/token"
|
|
30
|
+
"go/types"
|
|
31
|
+
"os"
|
|
32
|
+
"reflect"
|
|
33
|
+
"strings"
|
|
34
|
+
|
|
35
|
+
"golang.org/x/tools/go/packages"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
// --- Output contract ---------------------------------------------------
|
|
39
|
+
//
|
|
40
|
+
// Mirrors what `packages/adapter-go/src/go-types.ts` decodes. Keep the two
|
|
41
|
+
// in lockstep by hand; there is no shared schema source between a Go program
|
|
42
|
+
// and a TypeScript one.
|
|
43
|
+
|
|
44
|
+
type shapeRef struct {
|
|
45
|
+
ImportPath string `json:"importPath"`
|
|
46
|
+
Name string `json:"name"`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type field struct {
|
|
50
|
+
Name string `json:"name"`
|
|
51
|
+
JSONName string `json:"jsonName,omitempty"`
|
|
52
|
+
Type string `json:"type"`
|
|
53
|
+
Embedded bool `json:"embedded,omitempty"`
|
|
54
|
+
Skip bool `json:"skip,omitempty"`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type structShape struct {
|
|
58
|
+
ImportPath string `json:"importPath"`
|
|
59
|
+
Name string `json:"name"`
|
|
60
|
+
Fields []field `json:"fields"`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type handler struct {
|
|
64
|
+
ImportPath string `json:"importPath"`
|
|
65
|
+
Receiver string `json:"receiver,omitempty"`
|
|
66
|
+
FuncName string `json:"funcName"`
|
|
67
|
+
File string `json:"file"`
|
|
68
|
+
Line int `json:"line"`
|
|
69
|
+
Framework string `json:"framework"`
|
|
70
|
+
Request *shapeRef `json:"request,omitempty"`
|
|
71
|
+
Response *shapeRef `json:"response,omitempty"`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type output struct {
|
|
75
|
+
OK bool `json:"ok"`
|
|
76
|
+
Error string `json:"error,omitempty"`
|
|
77
|
+
Structs []structShape `json:"structs"`
|
|
78
|
+
Handlers []handler `json:"handlers"`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func main() {
|
|
82
|
+
out := run()
|
|
83
|
+
enc := json.NewEncoder(os.Stdout)
|
|
84
|
+
enc.SetEscapeHTML(false)
|
|
85
|
+
if err := enc.Encode(out); err != nil {
|
|
86
|
+
// Nothing left to report through — the JSON contract itself
|
|
87
|
+
// could not be written. Fail loudly on stderr; the TypeScript
|
|
88
|
+
// side treats a stdout that fails to parse as a failure of its
|
|
89
|
+
// own, but this at least explains why to a human reading logs.
|
|
90
|
+
fmt.Fprintln(os.Stderr, "goshapes: failed to encode output:", err)
|
|
91
|
+
os.Exit(1)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// run never panics past this point — a bug in this file must degrade to a
|
|
96
|
+
// disclosed `ok: false`, never a hung or garbled process the caller cannot
|
|
97
|
+
// interpret (the project's "honest degradation" rule applies to this helper
|
|
98
|
+
// exactly as it applies to the TypeScript adapter that calls it).
|
|
99
|
+
func run() (result output) {
|
|
100
|
+
defer func() {
|
|
101
|
+
if r := recover(); r != nil {
|
|
102
|
+
result = output{OK: false, Error: fmt.Sprintf("panic: %v", r)}
|
|
103
|
+
}
|
|
104
|
+
}()
|
|
105
|
+
|
|
106
|
+
if len(os.Args) < 2 {
|
|
107
|
+
return output{OK: false, Error: "usage: goshapes <target-module-directory>"}
|
|
108
|
+
}
|
|
109
|
+
dir := os.Args[1]
|
|
110
|
+
|
|
111
|
+
cfg := &packages.Config{
|
|
112
|
+
Dir: dir,
|
|
113
|
+
Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles |
|
|
114
|
+
packages.NeedImports | packages.NeedTypes | packages.NeedTypesInfo |
|
|
115
|
+
packages.NeedSyntax | packages.NeedModule,
|
|
116
|
+
Tests: false,
|
|
117
|
+
}
|
|
118
|
+
pkgs, err := packages.Load(cfg, "./...")
|
|
119
|
+
if err != nil {
|
|
120
|
+
return output{OK: false, Error: "packages.Load: " + err.Error()}
|
|
121
|
+
}
|
|
122
|
+
if len(pkgs) == 0 {
|
|
123
|
+
return output{OK: false, Error: "no Go packages found under " + dir}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Load errors are per-package (a broken sibling package must not blind
|
|
127
|
+
// this run to every package that DID load — the same "one file's gap,
|
|
128
|
+
// not the run's" standard `adapter-go`'s own TypeScript holds for a
|
|
129
|
+
// tree-walk throw). Collected for disclosure only; loading continues.
|
|
130
|
+
var loadErrors []string
|
|
131
|
+
packages.Visit(pkgs, nil, func(p *packages.Package) {
|
|
132
|
+
for _, e := range p.Errors {
|
|
133
|
+
loadErrors = append(loadErrors, p.PkgPath+": "+e.Error())
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
extractor := newExtractor()
|
|
138
|
+
for _, pkg := range pkgs {
|
|
139
|
+
if pkg.TypesInfo == nil || pkg.Types == nil {
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
extractor.visitPackage(pkg)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
structs, handlers := extractor.finish()
|
|
146
|
+
out := output{OK: true, Structs: structs, Handlers: handlers}
|
|
147
|
+
if len(loadErrors) > 0 && len(structs) == 0 && len(handlers) == 0 {
|
|
148
|
+
// Every package that mattered failed to load and nothing else
|
|
149
|
+
// stepped in — an honest failure, not a quiet empty result.
|
|
150
|
+
out = output{OK: false, Error: "no package loaded cleanly: " + loadErrors[0]}
|
|
151
|
+
}
|
|
152
|
+
return out
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- Framework recognition ----------------------------------------------
|
|
156
|
+
//
|
|
157
|
+
// Deliberately the same closed set `packages/adapter-go/src/routes.ts`
|
|
158
|
+
// already recognises syntactically (its `FRAMEWORKS` map's header explains
|
|
159
|
+
// why a closed, auditable set is the right shape for this kind of table).
|
|
160
|
+
// chi and gorilla/mux both register ordinary `func(http.ResponseWriter,
|
|
161
|
+
// *http.Request)` handlers, so they need no entry of their own here — the
|
|
162
|
+
// "net/http" signature check already covers them, exactly as it does in
|
|
163
|
+
// `routes.ts`.
|
|
164
|
+
|
|
165
|
+
const (
|
|
166
|
+
frameworkNetHTTP = "net/http"
|
|
167
|
+
frameworkGin = "github.com/gin-gonic/gin"
|
|
168
|
+
frameworkEcho = "github.com/labstack/echo"
|
|
169
|
+
frameworkEchoV4 = "github.com/labstack/echo/v4"
|
|
170
|
+
frameworkFiberV1 = "github.com/gofiber/fiber"
|
|
171
|
+
frameworkFiberV2 = "github.com/gofiber/fiber/v2"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
// ctxParamOf reports the single-context-parameter frameworks' own parameter
|
|
175
|
+
// object, when `sig` is shaped like that framework's handler. Returns nil
|
|
176
|
+
// for anything else, including a signature with the right type in the wrong
|
|
177
|
+
// position or arity — an exact match only, never a guess (the same "a
|
|
178
|
+
// verb-shaped call is not a route" discipline `routes.ts` applies to
|
|
179
|
+
// syntax, applied here to a checked signature).
|
|
180
|
+
func ctxParamOf(sig *types.Signature, wantType string) *types.Var {
|
|
181
|
+
if sig.Params().Len() != 1 {
|
|
182
|
+
return nil
|
|
183
|
+
}
|
|
184
|
+
p := sig.Params().At(0)
|
|
185
|
+
if typeString(p.Type()) != wantType {
|
|
186
|
+
return nil
|
|
187
|
+
}
|
|
188
|
+
return p
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// handlerShapeOf classifies one function's signature against every
|
|
192
|
+
// recognised framework, returning the framework name and the request
|
|
193
|
+
// object(s) whose method calls are this handler's own (for gin/echo/fiber,
|
|
194
|
+
// the single context parameter; for net/http-shaped handlers, both the
|
|
195
|
+
// ResponseWriter and the *Request are candidate receivers, but neither
|
|
196
|
+
// carries bind/JSON methods this reader looks for beyond the request body
|
|
197
|
+
// itself, read directly).
|
|
198
|
+
func handlerShapeOf(sig *types.Signature) (framework string, ctx *types.Var, ok bool) {
|
|
199
|
+
if sig.Params().Len() == 2 {
|
|
200
|
+
w := sig.Params().At(0)
|
|
201
|
+
r := sig.Params().At(1)
|
|
202
|
+
if typeString(w.Type()) == "net/http.ResponseWriter" && typeString(r.Type()) == "*net/http.Request" {
|
|
203
|
+
return frameworkNetHTTP, nil, true
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if v := ctxParamOf(sig, "*github.com/gin-gonic/gin.Context"); v != nil {
|
|
207
|
+
return frameworkGin, v, true
|
|
208
|
+
}
|
|
209
|
+
if v := ctxParamOf(sig, "github.com/labstack/echo.Context"); v != nil {
|
|
210
|
+
return frameworkEcho, v, true
|
|
211
|
+
}
|
|
212
|
+
if v := ctxParamOf(sig, "github.com/labstack/echo/v4.Context"); v != nil {
|
|
213
|
+
return frameworkEchoV4, v, true
|
|
214
|
+
}
|
|
215
|
+
if v := ctxParamOf(sig, "*github.com/gofiber/fiber.Ctx"); v != nil {
|
|
216
|
+
return frameworkFiberV1, v, true
|
|
217
|
+
}
|
|
218
|
+
if v := ctxParamOf(sig, "*github.com/gofiber/fiber/v2.Ctx"); v != nil {
|
|
219
|
+
return frameworkFiberV2, v, true
|
|
220
|
+
}
|
|
221
|
+
return "", nil, false
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// typeString renders a type the same, stable way everywhere in this file —
|
|
225
|
+
// `types.TypeString` with no qualifier function, so every named type prints
|
|
226
|
+
// fully qualified by its import path. That is what makes it safe to compare
|
|
227
|
+
// against a literal like "*net/http.Request" instead of resolving the
|
|
228
|
+
// framework's own package object first.
|
|
229
|
+
func typeString(t types.Type) string {
|
|
230
|
+
return types.TypeString(t, nil)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- Struct shape extraction ---------------------------------------------
|
|
234
|
+
|
|
235
|
+
// namedStructOf reports the declared struct type's own field list, `nil`
|
|
236
|
+
// when `t` (after stripping at most one pointer) is not a named struct.
|
|
237
|
+
// Reading through `types.Struct` rather than the AST's field list is the
|
|
238
|
+
// entire reason this helper exists rather than a syntactic reader: it is
|
|
239
|
+
// go/types that flattens an anonymous embed and resolves a field's type
|
|
240
|
+
// across package boundaries, both of which `adapter-go`'s own tree-sitter
|
|
241
|
+
// reader (`GoType.fields`, `parse.ts`) discloses it cannot do.
|
|
242
|
+
func namedStructOf(t types.Type) (named *types.Named, st *types.Struct, ok bool) {
|
|
243
|
+
if ptr, isPtr := t.(*types.Pointer); isPtr {
|
|
244
|
+
t = ptr.Elem()
|
|
245
|
+
}
|
|
246
|
+
n, isNamed := t.(*types.Named)
|
|
247
|
+
if !isNamed {
|
|
248
|
+
return nil, nil, false
|
|
249
|
+
}
|
|
250
|
+
s, isStruct := n.Underlying().(*types.Struct)
|
|
251
|
+
if !isStruct {
|
|
252
|
+
return nil, nil, false
|
|
253
|
+
}
|
|
254
|
+
return n, s, true
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
func shapeRefOf(named *types.Named) shapeRef {
|
|
258
|
+
pkg := named.Obj().Pkg()
|
|
259
|
+
importPath := ""
|
|
260
|
+
if pkg != nil {
|
|
261
|
+
importPath = pkg.Path()
|
|
262
|
+
}
|
|
263
|
+
return shapeRef{ImportPath: importPath, Name: named.Obj().Name()}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
func fieldsOf(st *types.Struct) []field {
|
|
267
|
+
out := make([]field, 0, st.NumFields())
|
|
268
|
+
for i := 0; i < st.NumFields(); i++ {
|
|
269
|
+
v := st.Field(i)
|
|
270
|
+
f := field{Name: v.Name(), Type: typeString(v.Type()), Embedded: v.Embedded()}
|
|
271
|
+
tag := parseJSONTag(st.Tag(i))
|
|
272
|
+
if tag == "-" {
|
|
273
|
+
f.Skip = true
|
|
274
|
+
} else if tag != "" {
|
|
275
|
+
f.JSONName = tag
|
|
276
|
+
}
|
|
277
|
+
out = append(out, f)
|
|
278
|
+
}
|
|
279
|
+
return out
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// parseJSONTag reads only the `json:"..."` tag's name segment — omitempty
|
|
283
|
+
// and the other comma-separated options are encoding/json's own concern,
|
|
284
|
+
// not identity this adapter reports. `""` when there is no json tag at all,
|
|
285
|
+
// distinct from `"-"` (explicitly excluded from the wire shape).
|
|
286
|
+
// `reflect.StructTag` is the standard library's own tag parser — this file
|
|
287
|
+
// does not reimplement Go's tag grammar.
|
|
288
|
+
func parseJSONTag(tag string) string {
|
|
289
|
+
raw := reflect.StructTag(tag).Get("json")
|
|
290
|
+
if raw == "" {
|
|
291
|
+
return ""
|
|
292
|
+
}
|
|
293
|
+
if comma := strings.IndexByte(raw, ','); comma != -1 {
|
|
294
|
+
return raw[:comma]
|
|
295
|
+
}
|
|
296
|
+
return raw
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- Handler body walk -----------------------------------------------------
|
|
300
|
+
|
|
301
|
+
// candidate is one shape this reader found written into a handler's body,
|
|
302
|
+
// with enough left to decide ambiguity — more than one DISTINCT candidate
|
|
303
|
+
// for one role (request or response) is refused, not guessed.
|
|
304
|
+
type candidate struct {
|
|
305
|
+
named *types.Named
|
|
306
|
+
st *types.Struct
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
type extractor struct {
|
|
310
|
+
// pkgPath::Name -> the struct, recorded only when a handler actually
|
|
311
|
+
// names it — matches this project's "promote, don't dump" precision
|
|
312
|
+
// stance the way `adapter-python`'s DTO promotion only ever emits a
|
|
313
|
+
// class that some route's own contract names.
|
|
314
|
+
wanted map[string]structShape
|
|
315
|
+
handlers []handler
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
func newExtractor() *extractor {
|
|
319
|
+
return &extractor{wanted: map[string]structShape{}}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
func (e *extractor) finish() ([]structShape, []handler) {
|
|
323
|
+
structs := make([]structShape, 0, len(e.wanted))
|
|
324
|
+
for _, s := range e.wanted {
|
|
325
|
+
structs = append(structs, s)
|
|
326
|
+
}
|
|
327
|
+
return structs, e.handlers
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
func (e *extractor) remember(named *types.Named, st *types.Struct) shapeRef {
|
|
331
|
+
ref := shapeRefOf(named)
|
|
332
|
+
key := ref.ImportPath + "." + ref.Name
|
|
333
|
+
if _, ok := e.wanted[key]; !ok {
|
|
334
|
+
e.wanted[key] = structShape{ImportPath: ref.ImportPath, Name: ref.Name, Fields: fieldsOf(st)}
|
|
335
|
+
}
|
|
336
|
+
return ref
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
func (e *extractor) visitPackage(pkg *packages.Package) {
|
|
340
|
+
fset := pkg.Fset
|
|
341
|
+
info := pkg.TypesInfo
|
|
342
|
+
for _, file := range pkg.Syntax {
|
|
343
|
+
for _, decl := range file.Decls {
|
|
344
|
+
fn, isFunc := decl.(*ast.FuncDecl)
|
|
345
|
+
if !isFunc || fn.Body == nil {
|
|
346
|
+
continue
|
|
347
|
+
}
|
|
348
|
+
obj, isObj := info.Defs[fn.Name].(*types.Func)
|
|
349
|
+
if !isObj {
|
|
350
|
+
continue
|
|
351
|
+
}
|
|
352
|
+
sig, isSig := obj.Type().(*types.Signature)
|
|
353
|
+
if !isSig {
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
356
|
+
framework, ctx, matched := handlerShapeOf(sig)
|
|
357
|
+
if !matched {
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
receiver := ""
|
|
362
|
+
if fn.Recv != nil && len(fn.Recv.List) == 1 {
|
|
363
|
+
receiver = receiverTypeName(fn.Recv.List[0].Type)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
reqCandidates, respCandidates := e.bodyShapesOf(fn.Body, info, ctx, framework)
|
|
367
|
+
req := e.resolveOne(reqCandidates)
|
|
368
|
+
resp := e.resolveOne(respCandidates)
|
|
369
|
+
if req == nil && resp == nil {
|
|
370
|
+
// A confirmed handler signature with no discernible
|
|
371
|
+
// request/response shape is real but out of this
|
|
372
|
+
// helper's scope — see the file header. Nothing to
|
|
373
|
+
// report; the TypeScript side gains nothing from a
|
|
374
|
+
// handler row that names no shape.
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
pos := fset.Position(fn.Pos())
|
|
379
|
+
e.handlers = append(e.handlers, handler{
|
|
380
|
+
ImportPath: pkg.PkgPath,
|
|
381
|
+
Receiver: receiver,
|
|
382
|
+
FuncName: fn.Name.Name,
|
|
383
|
+
File: pos.Filename,
|
|
384
|
+
Line: pos.Line,
|
|
385
|
+
Framework: framework,
|
|
386
|
+
Request: req,
|
|
387
|
+
Response: resp,
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
func receiverTypeName(expr ast.Expr) string {
|
|
394
|
+
if star, isStar := expr.(*ast.StarExpr); isStar {
|
|
395
|
+
expr = star.X
|
|
396
|
+
}
|
|
397
|
+
if ident, isIdent := expr.(*ast.Ident); isIdent {
|
|
398
|
+
return ident.Name
|
|
399
|
+
}
|
|
400
|
+
return ""
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// resolveOne applies the ambiguity rule: zero candidates is `nil` (nothing
|
|
404
|
+
// found, disclosed by simple absence); exactly one DISTINCT named type
|
|
405
|
+
// across every candidate is that type; more than one distinct type is
|
|
406
|
+
// `nil` too — refused, the same as zero, because this reader cannot say
|
|
407
|
+
// which of them the caller meant and a wrong shape claim is worse than a
|
|
408
|
+
// missing one (rule 2).
|
|
409
|
+
func (e *extractor) resolveOne(candidates []candidate) *shapeRef {
|
|
410
|
+
if len(candidates) == 0 {
|
|
411
|
+
return nil
|
|
412
|
+
}
|
|
413
|
+
first := candidates[0]
|
|
414
|
+
firstRef := shapeRefOf(first.named)
|
|
415
|
+
for _, c := range candidates[1:] {
|
|
416
|
+
ref := shapeRefOf(c.named)
|
|
417
|
+
if ref != firstRef {
|
|
418
|
+
return nil
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
ref := e.remember(first.named, first.st)
|
|
422
|
+
return &ref
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// bodyShapesOf walks one handler's body for the framework's own
|
|
426
|
+
// decode/bind and encode/JSON idioms. `ctx` is the single context
|
|
427
|
+
// parameter for gin/echo/fiber, `nil` for net/http (whose two parameters
|
|
428
|
+
// are read straight off `info.Uses`, not through a shared receiver).
|
|
429
|
+
func (e *extractor) bodyShapesOf(
|
|
430
|
+
body *ast.BlockStmt,
|
|
431
|
+
info *types.Info,
|
|
432
|
+
ctx *types.Var,
|
|
433
|
+
framework string,
|
|
434
|
+
) (requests []candidate, responses []candidate) {
|
|
435
|
+
ast.Inspect(body, func(n ast.Node) bool {
|
|
436
|
+
call, isCall := n.(*ast.CallExpr)
|
|
437
|
+
if !isCall {
|
|
438
|
+
return true
|
|
439
|
+
}
|
|
440
|
+
sel, isSel := call.Fun.(*ast.SelectorExpr)
|
|
441
|
+
if !isSel {
|
|
442
|
+
return true
|
|
443
|
+
}
|
|
444
|
+
method := sel.Sel.Name
|
|
445
|
+
|
|
446
|
+
// `json.NewDecoder(r.Body).Decode(&x)` / `json.NewEncoder(w).Encode(x)`
|
|
447
|
+
// — the receiver is itself a call, so the method is read off the
|
|
448
|
+
// call's own resolved type rather than an identifier.
|
|
449
|
+
if method == "Decode" || method == "Encode" {
|
|
450
|
+
if isJSONDecoderOrEncoder(sel.X, info, method) {
|
|
451
|
+
if method == "Decode" && len(call.Args) == 1 {
|
|
452
|
+
if c, ok := candidateFromPointerArg(call.Args[0], info); ok {
|
|
453
|
+
requests = append(requests, c)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if method == "Encode" && len(call.Args) == 1 {
|
|
457
|
+
if c, ok := candidateFromValueArg(call.Args[0], info); ok {
|
|
458
|
+
responses = append(responses, c)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return true
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// `json.Unmarshal(data, &x)` — a package-level call, not a method
|
|
466
|
+
// on a receiver this reader tracks; `sel.X` is the `json`
|
|
467
|
+
// identifier itself.
|
|
468
|
+
if method == "Unmarshal" && isPackageIdent(sel.X, info, "encoding/json") && len(call.Args) == 2 {
|
|
469
|
+
if c, ok := candidateFromPointerArg(call.Args[1], info); ok {
|
|
470
|
+
requests = append(requests, c)
|
|
471
|
+
}
|
|
472
|
+
return true
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Every remaining idiom is a method called on the handler's own
|
|
476
|
+
// context parameter — confirm the receiver really is that
|
|
477
|
+
// parameter (`info.Uses[recv] == ctx`) before trusting the
|
|
478
|
+
// method name at all. A same-named method on an unrelated value
|
|
479
|
+
// is not this framework's bind/JSON call.
|
|
480
|
+
if ctx == nil {
|
|
481
|
+
return true
|
|
482
|
+
}
|
|
483
|
+
recvIdent, isIdent := sel.X.(*ast.Ident)
|
|
484
|
+
if !isIdent || info.Uses[recvIdent] != ctx {
|
|
485
|
+
return true
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
switch framework {
|
|
489
|
+
case frameworkGin:
|
|
490
|
+
switch method {
|
|
491
|
+
case "BindJSON", "ShouldBindJSON", "Bind", "ShouldBind":
|
|
492
|
+
if len(call.Args) == 1 {
|
|
493
|
+
if c, ok := candidateFromPointerArg(call.Args[0], info); ok {
|
|
494
|
+
requests = append(requests, c)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
case "JSON":
|
|
498
|
+
if len(call.Args) == 2 {
|
|
499
|
+
if c, ok := candidateFromValueArg(call.Args[1], info); ok {
|
|
500
|
+
responses = append(responses, c)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
case frameworkEcho, frameworkEchoV4:
|
|
505
|
+
switch method {
|
|
506
|
+
case "Bind":
|
|
507
|
+
if len(call.Args) == 1 {
|
|
508
|
+
if c, ok := candidateFromPointerArg(call.Args[0], info); ok {
|
|
509
|
+
requests = append(requests, c)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
case "JSON":
|
|
513
|
+
if len(call.Args) == 2 {
|
|
514
|
+
if c, ok := candidateFromValueArg(call.Args[1], info); ok {
|
|
515
|
+
responses = append(responses, c)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
case frameworkFiberV1, frameworkFiberV2:
|
|
520
|
+
switch method {
|
|
521
|
+
case "BodyParser":
|
|
522
|
+
if len(call.Args) == 1 {
|
|
523
|
+
if c, ok := candidateFromPointerArg(call.Args[0], info); ok {
|
|
524
|
+
requests = append(requests, c)
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
case "JSON":
|
|
528
|
+
if len(call.Args) == 1 {
|
|
529
|
+
if c, ok := candidateFromValueArg(call.Args[0], info); ok {
|
|
530
|
+
responses = append(responses, c)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return true
|
|
536
|
+
})
|
|
537
|
+
return requests, responses
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// isJSONDecoderOrEncoder reports whether `expr` (the receiver of a
|
|
541
|
+
// `.Decode`/`.Encode` call) is itself a call to `json.NewDecoder`/
|
|
542
|
+
// `json.NewEncoder` — resolved off the call's own result type
|
|
543
|
+
// (`*encoding/json.Decoder` / `*encoding/json.Encoder`), which is a
|
|
544
|
+
// stronger check than matching the source text and is exactly what a type
|
|
545
|
+
// checker is for.
|
|
546
|
+
func isJSONDecoderOrEncoder(expr ast.Expr, info *types.Info, method string) bool {
|
|
547
|
+
call, isCall := expr.(*ast.CallExpr)
|
|
548
|
+
if !isCall {
|
|
549
|
+
return false
|
|
550
|
+
}
|
|
551
|
+
t := info.TypeOf(call)
|
|
552
|
+
if t == nil {
|
|
553
|
+
return false
|
|
554
|
+
}
|
|
555
|
+
want := "*encoding/json.Decoder"
|
|
556
|
+
if method == "Encode" {
|
|
557
|
+
want = "*encoding/json.Encoder"
|
|
558
|
+
}
|
|
559
|
+
return typeString(t) == want
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
func isPackageIdent(expr ast.Expr, info *types.Info, importPath string) bool {
|
|
563
|
+
ident, isIdent := expr.(*ast.Ident)
|
|
564
|
+
if !isIdent {
|
|
565
|
+
return false
|
|
566
|
+
}
|
|
567
|
+
pkgName, isPkgName := info.Uses[ident].(*types.PkgName)
|
|
568
|
+
return isPkgName && pkgName.Imported().Path() == importPath
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// candidateFromPointerArg reads `&x` — the shape every Decode/Bind idiom
|
|
572
|
+
// this file recognises takes its target as. Anything else (a bare name
|
|
573
|
+
// with no `&`, an expression) names no addressable local this reader
|
|
574
|
+
// resolves a struct from, and is silently not a candidate rather than a
|
|
575
|
+
// refusal: the surrounding call may still be a real Decode of something
|
|
576
|
+
// this reader simply does not follow (a field of a struct, an interface
|
|
577
|
+
// value), which is a recall gap, never a precision one.
|
|
578
|
+
func candidateFromPointerArg(arg ast.Expr, info *types.Info) (candidate, bool) {
|
|
579
|
+
unary, isUnary := arg.(*ast.UnaryExpr)
|
|
580
|
+
if !isUnary || unary.Op != token.AND {
|
|
581
|
+
return candidate{}, false
|
|
582
|
+
}
|
|
583
|
+
return candidateFromValueArg(unary.X, info)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
func candidateFromValueArg(arg ast.Expr, info *types.Info) (candidate, bool) {
|
|
587
|
+
t := info.TypeOf(arg)
|
|
588
|
+
if t == nil {
|
|
589
|
+
return candidate{}, false
|
|
590
|
+
}
|
|
591
|
+
named, st, ok := namedStructOf(t)
|
|
592
|
+
if !ok {
|
|
593
|
+
return candidate{}, false
|
|
594
|
+
}
|
|
595
|
+
return candidate{named: named, st: st}, true
|
|
596
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descryy/adapter-go",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Go adapter, breadth tier. One grammar and one rule set over the shared tree-sitter harness — the second language to use it, and the measurement of whether that harness was worth building.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
|
-
"dist"
|
|
26
|
+
"dist",
|
|
27
|
+
"goshapes"
|
|
27
28
|
],
|
|
28
29
|
"publishConfig": {
|
|
29
30
|
"registry": "https://registry.npmjs.org",
|
|
@@ -34,11 +35,11 @@
|
|
|
34
35
|
"build:assets": "node -e \"require('fs').copyFileSync(require.resolve('tree-sitter-go/tree-sitter-go.wasm'),'dist/tree-sitter-go.wasm')\""
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
|
-
"@descryy/adapter-common": "0.3.
|
|
38
|
-
"@descryy/adapter-sql": "0.
|
|
39
|
-
"@descryy/adapter-treesitter": "0.2.
|
|
40
|
-
"@descryy/ir": "0.
|
|
38
|
+
"@descryy/adapter-common": "0.3.1",
|
|
39
|
+
"@descryy/adapter-sql": "0.5.1",
|
|
40
|
+
"@descryy/adapter-treesitter": "0.2.1",
|
|
41
|
+
"@descryy/ir": "0.10.0",
|
|
41
42
|
"tree-sitter-go": "0.25.0",
|
|
42
|
-
"@descryy/adapter-openapi": "0.
|
|
43
|
+
"@descryy/adapter-openapi": "0.5.1"
|
|
43
44
|
}
|
|
44
45
|
}
|