@ihelpee/prumo-plugin-go-gin 0.0.0-stage → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +110 -2
- package/dist/ast.d.ts +154 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +123 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/rules.d.ts +113 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +718 -0
- package/go-extractor/cmd/prumo-go-extractor/main.go +596 -0
- package/go-extractor/go.mod +3 -0
- package/package.json +35 -3
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
// prumo-go-extractor — fatos de AST Go em JSON canônico v1.
|
|
2
|
+
//
|
|
3
|
+
// Este programa é PARSER, não implementador da lei (spec
|
|
4
|
+
// docs/especificacao-prumo-go-gin.md §4.2): ele serializa fatos sintáticos
|
|
5
|
+
// e nunca decide "isto é violação?". A decisão vive 100% no core Node.
|
|
6
|
+
//
|
|
7
|
+
// R1.1: apenas stdlib Go. Qualquer import externo aqui é falha de build.
|
|
8
|
+
// R1.2: nunca executa o código analisado; apenas parser.ParseFile — parse
|
|
9
|
+
// puramente sintático, sem go build, sem go list, sem rede.
|
|
10
|
+
package main
|
|
11
|
+
|
|
12
|
+
import (
|
|
13
|
+
"encoding/json"
|
|
14
|
+
"flag"
|
|
15
|
+
"fmt"
|
|
16
|
+
"go/ast"
|
|
17
|
+
"go/format"
|
|
18
|
+
"go/parser"
|
|
19
|
+
"go/token"
|
|
20
|
+
"os"
|
|
21
|
+
"path/filepath"
|
|
22
|
+
"reflect"
|
|
23
|
+
"strconv"
|
|
24
|
+
"strings"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Contrato JSON v1 (spec §1.4) — congelado.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
type ImportInfo struct {
|
|
32
|
+
Path string `json:"path"`
|
|
33
|
+
Alias string `json:"alias"`
|
|
34
|
+
Line int `json:"line"`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type EngineRoot struct {
|
|
38
|
+
Name string `json:"name"`
|
|
39
|
+
Line int `json:"line"`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type RouteInfo struct {
|
|
43
|
+
Method string `json:"method"`
|
|
44
|
+
Path string `json:"path"`
|
|
45
|
+
Handler string `json:"handler"`
|
|
46
|
+
Receiver string `json:"receiver"`
|
|
47
|
+
InlineGuards []string `json:"inlineGuards"`
|
|
48
|
+
Line int `json:"line"`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type GroupDef struct {
|
|
52
|
+
Var string `json:"var"`
|
|
53
|
+
Parent string `json:"parent"`
|
|
54
|
+
Path string `json:"path"`
|
|
55
|
+
Guards []string `json:"guards"`
|
|
56
|
+
Line int `json:"line"`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type UseCall struct {
|
|
60
|
+
Receiver string `json:"receiver"`
|
|
61
|
+
Guards []string `json:"guards"`
|
|
62
|
+
Line int `json:"line"`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type BindingInfo struct {
|
|
66
|
+
Func string `json:"func"`
|
|
67
|
+
ArgExpr string `json:"argExpr"`
|
|
68
|
+
VarName string `json:"varName"`
|
|
69
|
+
Line int `json:"line"`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type FieldInfo struct {
|
|
73
|
+
Name string `json:"name"`
|
|
74
|
+
Type string `json:"type"`
|
|
75
|
+
BindingTag string `json:"bindingTag"`
|
|
76
|
+
HasBinding bool `json:"hasBinding"`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type StructInfo struct {
|
|
80
|
+
Name string `json:"name"`
|
|
81
|
+
Line int `json:"line"`
|
|
82
|
+
Fields []FieldInfo `json:"fields"`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type VarType struct {
|
|
86
|
+
Var string `json:"var"`
|
|
87
|
+
Type string `json:"type"`
|
|
88
|
+
Line int `json:"line"`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type ErrSite struct {
|
|
92
|
+
Expr string `json:"expr"`
|
|
93
|
+
Line int `json:"line"`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type ErrorHandling struct {
|
|
97
|
+
ExplicitDiscards []ErrSite `json:"explicitDiscards"`
|
|
98
|
+
UncheckedErrCalls []ErrSite `json:"uncheckedErrCalls"`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type FileModel struct {
|
|
102
|
+
Path string `json:"path"`
|
|
103
|
+
Package string `json:"package"`
|
|
104
|
+
IsTest bool `json:"isTest"`
|
|
105
|
+
IsInternal bool `json:"isInternal"`
|
|
106
|
+
Imports []ImportInfo `json:"imports"`
|
|
107
|
+
DynamicImports []string `json:"dynamicImports"`
|
|
108
|
+
EngineRoots []EngineRoot `json:"engineRoots"`
|
|
109
|
+
Routes []RouteInfo `json:"routes"`
|
|
110
|
+
GroupDefs []GroupDef `json:"groupDefs"`
|
|
111
|
+
UseCalls []UseCall `json:"useCalls"`
|
|
112
|
+
Bindings []BindingInfo `json:"bindings"`
|
|
113
|
+
Structs []StructInfo `json:"structs"`
|
|
114
|
+
VarTypes []VarType `json:"varTypes"`
|
|
115
|
+
UsesErr bool `json:"usesErr"`
|
|
116
|
+
// Fatos p/ heurística R3.8 (a decisão é do core): nomes das funções
|
|
117
|
+
// chamadas cujo resultado foi atribuído a uma variável de erro
|
|
118
|
+
// (`err := f()`, `x, err := f()`) — "esta chamada já aparece com err
|
|
119
|
+
// neste arquivo".
|
|
120
|
+
CheckedCallees []string `json:"checkedCallees"`
|
|
121
|
+
ErrorHandling ErrorHandling `json:"errorHandling"`
|
|
122
|
+
// R1.3: arquivo com erro de parse não derruba o run.
|
|
123
|
+
Error string `json:"error,omitempty"`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type Model struct {
|
|
127
|
+
Version int `json:"version"`
|
|
128
|
+
Module *string `json:"module"`
|
|
129
|
+
Files []FileModel `json:"files"`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Léxicos de reconhecimento (fatos sintáticos; a LEI dos símbolos é do core)
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
var routeMethods = map[string]string{
|
|
137
|
+
"GET": "GET",
|
|
138
|
+
"POST": "POST",
|
|
139
|
+
"PUT": "PUT",
|
|
140
|
+
"PATCH": "PATCH",
|
|
141
|
+
"DELETE": "DELETE",
|
|
142
|
+
"HEAD": "HEAD",
|
|
143
|
+
"OPTIONS": "OPTIONS",
|
|
144
|
+
"Any": "ANY",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
var bindFuncs = map[string]bool{
|
|
148
|
+
"ShouldBindJSON": true, "BindJSON": true,
|
|
149
|
+
"ShouldBind": true, "Bind": true,
|
|
150
|
+
"ShouldBindWith": true, "BindWith": true,
|
|
151
|
+
"ShouldBindQuery": true, "BindQuery": true,
|
|
152
|
+
"ShouldBindQueryWith": true, "BindQueryWith": true,
|
|
153
|
+
"ShouldBindUri": true, "BindUri": true,
|
|
154
|
+
"ShouldBindUriWith": true, "BindUriWith": true,
|
|
155
|
+
"ShouldBindHeader": true, "BindHeader": true,
|
|
156
|
+
"ShouldBindForm": true, "BindForm": true,
|
|
157
|
+
"ShouldBindYAML": true, "BindYAML": true,
|
|
158
|
+
"ShouldBindXML": true, "BindXML": true,
|
|
159
|
+
"ShouldBindMsgPack": true, "BindMsgPack": true,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Helpers
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
func exprText(fset *token.FileSet, n ast.Node) string {
|
|
167
|
+
var sb strings.Builder
|
|
168
|
+
if err := format.Node(&sb, fset, n); err != nil {
|
|
169
|
+
return ""
|
|
170
|
+
}
|
|
171
|
+
return sb.String()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// callName extrai o nome canônico de um guard/handler: Auth -> "Auth",
|
|
175
|
+
// gin.Recovery -> "gin.Recovery", Auth("realm") -> "Auth".
|
|
176
|
+
func callName(e ast.Expr) string {
|
|
177
|
+
switch v := e.(type) {
|
|
178
|
+
case *ast.Ident:
|
|
179
|
+
return v.Name
|
|
180
|
+
case *ast.SelectorExpr:
|
|
181
|
+
return callName(v.X) + "." + v.Sel.Name
|
|
182
|
+
case *ast.CallExpr:
|
|
183
|
+
return callName(v.Fun)
|
|
184
|
+
case *ast.IndexExpr:
|
|
185
|
+
return callName(v.X)
|
|
186
|
+
case *ast.IndexListExpr:
|
|
187
|
+
return callName(v.X)
|
|
188
|
+
case *ast.ParenExpr:
|
|
189
|
+
return callName(v.X)
|
|
190
|
+
}
|
|
191
|
+
return ""
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
func stringLit(e ast.Expr) (string, bool) {
|
|
195
|
+
lit, ok := e.(*ast.BasicLit)
|
|
196
|
+
if !ok || lit.Kind != token.STRING {
|
|
197
|
+
return "", false
|
|
198
|
+
}
|
|
199
|
+
s, err := strconv.Unquote(lit.Value)
|
|
200
|
+
if err != nil {
|
|
201
|
+
return "", false
|
|
202
|
+
}
|
|
203
|
+
return s, true
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
func isIdent(e ast.Expr, name string) bool {
|
|
207
|
+
id, ok := e.(*ast.Ident)
|
|
208
|
+
return ok && id.Name == name
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// selectorCall reconhece `X.Sel(args...)` e retorna (X, args).
|
|
212
|
+
func selectorCall(call *ast.CallExpr, sel string) (ast.Expr, []ast.Expr, bool) {
|
|
213
|
+
se, ok := call.Fun.(*ast.SelectorExpr)
|
|
214
|
+
if !ok || se.Sel.Name != sel {
|
|
215
|
+
return nil, nil, false
|
|
216
|
+
}
|
|
217
|
+
return se.X, call.Args, true
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// go.mod do alvo: apenas o module path (ler go.mod NÃO é resolver deps).
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
func modulePath(root string) *string {
|
|
225
|
+
data, err := os.ReadFile(filepath.Join(root, "go.mod"))
|
|
226
|
+
if err != nil {
|
|
227
|
+
return nil
|
|
228
|
+
}
|
|
229
|
+
for _, line := range strings.Split(string(data), "\n") {
|
|
230
|
+
trimmed := strings.TrimSpace(line)
|
|
231
|
+
if rest, ok := strings.CutPrefix(trimmed, "module "); ok {
|
|
232
|
+
mod := strings.TrimSpace(strings.SplitN(rest, "//", 2)[0])
|
|
233
|
+
mod = strings.Trim(mod, "\"")
|
|
234
|
+
if mod != "" {
|
|
235
|
+
return &mod
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return nil
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Extração por arquivo
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
func extractFile(root, rel string) FileModel {
|
|
247
|
+
fm := FileModel{
|
|
248
|
+
Path: rel,
|
|
249
|
+
IsTest: strings.HasSuffix(rel, "_test.go"),
|
|
250
|
+
IsInternal: strings.Contains("/"+filepath.ToSlash(rel), "/internal/"),
|
|
251
|
+
Imports: []ImportInfo{},
|
|
252
|
+
DynamicImports: []string{},
|
|
253
|
+
EngineRoots: []EngineRoot{},
|
|
254
|
+
Routes: []RouteInfo{},
|
|
255
|
+
GroupDefs: []GroupDef{},
|
|
256
|
+
UseCalls: []UseCall{},
|
|
257
|
+
Bindings: []BindingInfo{},
|
|
258
|
+
Structs: []StructInfo{},
|
|
259
|
+
VarTypes: []VarType{},
|
|
260
|
+
CheckedCallees: []string{},
|
|
261
|
+
ErrorHandling: ErrorHandling{ExplicitDiscards: []ErrSite{}, UncheckedErrCalls: []ErrSite{}},
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fset := token.NewFileSet()
|
|
265
|
+
af, err := parser.ParseFile(fset, filepath.Join(root, rel), nil, parser.SkipObjectResolution)
|
|
266
|
+
if err != nil {
|
|
267
|
+
fm.Error = "parse"
|
|
268
|
+
return fm
|
|
269
|
+
}
|
|
270
|
+
fm.Package = af.Name.Name
|
|
271
|
+
|
|
272
|
+
ast.Inspect(af, func(n ast.Node) bool {
|
|
273
|
+
switch node := n.(type) {
|
|
274
|
+
|
|
275
|
+
// imports (R2.1–R2.4: alias, dot e blank são todos arestas)
|
|
276
|
+
case *ast.GenDecl:
|
|
277
|
+
if node.Tok == token.IMPORT {
|
|
278
|
+
for _, spec := range node.Specs {
|
|
279
|
+
is, ok := spec.(*ast.ImportSpec)
|
|
280
|
+
if !ok {
|
|
281
|
+
continue
|
|
282
|
+
}
|
|
283
|
+
path, ok := stringLit(is.Path)
|
|
284
|
+
if !ok {
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
alias := ""
|
|
288
|
+
if is.Name != nil {
|
|
289
|
+
alias = is.Name.Name // inclui "." e "_"
|
|
290
|
+
}
|
|
291
|
+
fm.Imports = append(fm.Imports, ImportInfo{
|
|
292
|
+
Path: path, Alias: alias, Line: fset.Position(is.Pos()).Line,
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if node.Tok == token.TYPE {
|
|
297
|
+
for _, spec := range node.Specs {
|
|
298
|
+
ts, ok := spec.(*ast.TypeSpec)
|
|
299
|
+
if !ok {
|
|
300
|
+
continue
|
|
301
|
+
}
|
|
302
|
+
st, ok := ts.Type.(*ast.StructType)
|
|
303
|
+
if !ok {
|
|
304
|
+
continue
|
|
305
|
+
}
|
|
306
|
+
fm.Structs = append(fm.Structs, extractStruct(fset, ts, st))
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if node.Tok == token.VAR {
|
|
310
|
+
for _, spec := range node.Specs {
|
|
311
|
+
vs, ok := spec.(*ast.ValueSpec)
|
|
312
|
+
if !ok || vs.Type == nil {
|
|
313
|
+
continue
|
|
314
|
+
}
|
|
315
|
+
ty := exprText(fset, vs.Type)
|
|
316
|
+
for _, name := range vs.Names {
|
|
317
|
+
if name.Name == "_" {
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
fm.VarTypes = append(fm.VarTypes, VarType{
|
|
321
|
+
Var: name.Name, Type: ty, Line: fset.Position(name.Pos()).Line,
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// engineRoots: r := gin.Default() / r := gin.New()
|
|
328
|
+
case *ast.AssignStmt:
|
|
329
|
+
handleAssign(fset, node, &fm)
|
|
330
|
+
|
|
331
|
+
case *ast.CallExpr:
|
|
332
|
+
handleCall(fset, node, &fm)
|
|
333
|
+
|
|
334
|
+
case *ast.FuncDecl:
|
|
335
|
+
if node.Type.Params != nil {
|
|
336
|
+
for _, p := range node.Type.Params.List {
|
|
337
|
+
ty := exprText(fset, p.Type)
|
|
338
|
+
for _, name := range p.Names {
|
|
339
|
+
if name.Name == "_" {
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
fm.VarTypes = append(fm.VarTypes, VarType{
|
|
343
|
+
Var: name.Name, Type: ty, Line: fset.Position(name.Pos()).Line,
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
case *ast.Ident:
|
|
350
|
+
if node.Name == "err" {
|
|
351
|
+
fm.UsesErr = true
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return true
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
return fm
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
func extractStruct(fset *token.FileSet, ts *ast.TypeSpec, st *ast.StructType) StructInfo {
|
|
361
|
+
si := StructInfo{Name: ts.Name.Name, Line: fset.Position(ts.Pos()).Line, Fields: []FieldInfo{}}
|
|
362
|
+
if st.Fields == nil {
|
|
363
|
+
return si
|
|
364
|
+
}
|
|
365
|
+
for _, f := range st.Fields.List {
|
|
366
|
+
ty := exprText(fset, f.Type)
|
|
367
|
+
tag := ""
|
|
368
|
+
hasBinding := false
|
|
369
|
+
if f.Tag != nil {
|
|
370
|
+
if raw, err := strconv.Unquote(f.Tag.Value); err == nil {
|
|
371
|
+
tag = strings.TrimSpace(reflect.StructTag(raw).Get("binding"))
|
|
372
|
+
hasBinding = tag != ""
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if len(f.Names) == 0 { // campo embutido
|
|
376
|
+
name := ty
|
|
377
|
+
if idx := strings.LastIndexByte(name, '.'); idx >= 0 {
|
|
378
|
+
name = name[idx+1:]
|
|
379
|
+
}
|
|
380
|
+
name = strings.TrimPrefix(name, "*")
|
|
381
|
+
si.Fields = append(si.Fields, FieldInfo{Name: name, Type: ty, BindingTag: tag, HasBinding: hasBinding})
|
|
382
|
+
continue
|
|
383
|
+
}
|
|
384
|
+
for _, n := range f.Names {
|
|
385
|
+
si.Fields = append(si.Fields, FieldInfo{Name: n.Name, Type: ty, BindingTag: tag, HasBinding: hasBinding})
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return si
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
func handleAssign(fset *token.FileSet, as *ast.AssignStmt, fm *FileModel) {
|
|
392
|
+
line := fset.Position(as.Pos()).Line
|
|
393
|
+
|
|
394
|
+
// engineRoots / groupDefs / varTypes por :=
|
|
395
|
+
if as.Tok == token.DEFINE && len(as.Lhs) == 1 && len(as.Rhs) == 1 {
|
|
396
|
+
if id, ok := as.Lhs[0].(*ast.Ident); ok && id.Name != "_" {
|
|
397
|
+
if call, ok := as.Rhs[0].(*ast.CallExpr); ok {
|
|
398
|
+
// r := gin.Default() | gin.New()
|
|
399
|
+
if x, _, ok := selectorCall(call, "Default"); ok && isIdent(x, "gin") {
|
|
400
|
+
fm.EngineRoots = append(fm.EngineRoots, EngineRoot{Name: id.Name, Line: line})
|
|
401
|
+
}
|
|
402
|
+
if x, _, ok := selectorCall(call, "New"); ok && isIdent(x, "gin") {
|
|
403
|
+
fm.EngineRoots = append(fm.EngineRoots, EngineRoot{Name: id.Name, Line: line})
|
|
404
|
+
}
|
|
405
|
+
// api := r.Group("/api", Middleware())
|
|
406
|
+
if parent, args, ok := selectorCall(call, "Group"); ok && len(args) >= 1 {
|
|
407
|
+
path, _ := stringLit(args[0])
|
|
408
|
+
guards := []string{}
|
|
409
|
+
for _, a := range args[1:] {
|
|
410
|
+
if g := callName(a); g != "" {
|
|
411
|
+
guards = append(guards, g)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
fm.GroupDefs = append(fm.GroupDefs, GroupDef{
|
|
415
|
+
Var: id.Name, Parent: exprText(fset, parent), Path: path, Guards: guards, Line: line,
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// req := T{} / req := &T{}
|
|
420
|
+
switch rhs := as.Rhs[0].(type) {
|
|
421
|
+
case *ast.CompositeLit:
|
|
422
|
+
if rhs.Type != nil {
|
|
423
|
+
fm.VarTypes = append(fm.VarTypes, VarType{Var: id.Name, Type: exprText(fset, rhs.Type), Line: line})
|
|
424
|
+
}
|
|
425
|
+
case *ast.UnaryExpr:
|
|
426
|
+
if rhs.Op == token.AND {
|
|
427
|
+
if cl, ok := rhs.X.(*ast.CompositeLit); ok && cl.Type != nil {
|
|
428
|
+
fm.VarTypes = append(fm.VarTypes, VarType{Var: id.Name, Type: exprText(fset, cl.Type), Line: line})
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Fato R3.8: `err := f()` / `x, err := f()` — a chamada f aparece com
|
|
436
|
+
// err neste arquivo. O core usa isso para reduzir falso-positivo do
|
|
437
|
+
// sub-caso heurístico; a decisão de violar é do core.
|
|
438
|
+
for _, l := range as.Lhs {
|
|
439
|
+
if id, ok := l.(*ast.Ident); ok && (id.Name == "err" || strings.HasPrefix(id.Name, "err")) {
|
|
440
|
+
if call, ok := as.Rhs[0].(*ast.CallExpr); ok {
|
|
441
|
+
if n := callName(call.Fun); n != "" {
|
|
442
|
+
fm.CheckedCallees = append(fm.CheckedCallees, n)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
break
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// R3.7 (fatos): descarte explícito — `_ = err`, `_ = f()`, `x, _ := f()`.
|
|
450
|
+
hasBlank := false
|
|
451
|
+
for _, l := range as.Lhs {
|
|
452
|
+
if isIdent(l, "_") {
|
|
453
|
+
hasBlank = true
|
|
454
|
+
break
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if hasBlank {
|
|
458
|
+
discard := false
|
|
459
|
+
if len(as.Lhs) > 1 {
|
|
460
|
+
_, isCall := as.Rhs[0].(*ast.CallExpr)
|
|
461
|
+
discard = isCall
|
|
462
|
+
} else if len(as.Lhs) == 1 {
|
|
463
|
+
switch r := as.Rhs[0].(type) {
|
|
464
|
+
case *ast.CallExpr:
|
|
465
|
+
discard = true
|
|
466
|
+
case *ast.Ident:
|
|
467
|
+
discard = r.Name == "err" || strings.HasPrefix(r.Name, "err")
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if discard {
|
|
471
|
+
fm.ErrorHandling.ExplicitDiscards = append(fm.ErrorHandling.ExplicitDiscards, ErrSite{
|
|
472
|
+
Expr: exprText(fset, as), Line: line,
|
|
473
|
+
})
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
func handleCall(fset *token.FileSet, call *ast.CallExpr, fm *FileModel) {
|
|
479
|
+
line := fset.Position(call.Pos()).Line
|
|
480
|
+
|
|
481
|
+
// rotas Gin: r.POST("/x", Guard(), Handler)
|
|
482
|
+
if se, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
483
|
+
method, isRoute := routeMethods[se.Sel.Name]
|
|
484
|
+
if isRoute && len(call.Args) >= 2 {
|
|
485
|
+
path, okPath := stringLit(call.Args[0])
|
|
486
|
+
if okPath {
|
|
487
|
+
handler := ""
|
|
488
|
+
if len(call.Args) >= 1 {
|
|
489
|
+
handler = callName(call.Args[len(call.Args)-1])
|
|
490
|
+
}
|
|
491
|
+
guards := []string{}
|
|
492
|
+
for _, a := range call.Args[1 : len(call.Args)-1] {
|
|
493
|
+
if g := callName(a); g != "" {
|
|
494
|
+
guards = append(guards, g)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
fm.Routes = append(fm.Routes, RouteInfo{
|
|
498
|
+
Method: method, Path: path, Handler: handler,
|
|
499
|
+
Receiver: exprText(fset, se.X), InlineGuards: guards, Line: line,
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// r.Use(A(), B())
|
|
505
|
+
if recv, args, ok := selectorCall(call, "Use"); ok {
|
|
506
|
+
guards := []string{}
|
|
507
|
+
for _, a := range args {
|
|
508
|
+
if g := callName(a); g != "" {
|
|
509
|
+
guards = append(guards, g)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
fm.UseCalls = append(fm.UseCalls, UseCall{Receiver: exprText(fset, recv), Guards: guards, Line: line})
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// c.ShouldBindJSON(&req)
|
|
516
|
+
if bindFuncs[se.Sel.Name] && len(call.Args) >= 1 {
|
|
517
|
+
argExpr := exprText(fset, call.Args[0])
|
|
518
|
+
varName := strings.TrimPrefix(argExpr, "&")
|
|
519
|
+
fm.Bindings = append(fm.Bindings, BindingInfo{
|
|
520
|
+
Func: se.Sel.Name, ArgExpr: argExpr, VarName: varName, Line: line,
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
func main() {
|
|
528
|
+
root := flag.String("root", ".", "raiz do projeto-alvo")
|
|
529
|
+
filesFlag := flag.String("files", "", "caminhos relativos .go separados por vírgula")
|
|
530
|
+
flag.Parse()
|
|
531
|
+
|
|
532
|
+
// R1.1: stdlib-only. go.mod sem require é a prova mecânica.
|
|
533
|
+
model := Model{Version: 1, Module: modulePath(*root), Files: []FileModel{}}
|
|
534
|
+
|
|
535
|
+
var rels []string
|
|
536
|
+
for _, f := range strings.Split(*filesFlag, ",") {
|
|
537
|
+
f = strings.TrimSpace(f)
|
|
538
|
+
if f != "" {
|
|
539
|
+
rels = append(rels, filepath.FromSlash(f))
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
for _, rel := range rels {
|
|
544
|
+
model.Files = append(model.Files, extractFile(*root, rel))
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ExprStmt puro: precisa do contexto de statement, então uma passada
|
|
548
|
+
// dedicada por arquivo após o Inspect principal seria redundante — o
|
|
549
|
+
// Inspect acima não vê o wrapper ExprStmt. Refazemos com DeclFiles.
|
|
550
|
+
for i, rel := range rels {
|
|
551
|
+
if model.Files[i].Error != "" {
|
|
552
|
+
continue
|
|
553
|
+
}
|
|
554
|
+
fset := token.NewFileSet()
|
|
555
|
+
af, err := parser.ParseFile(fset, filepath.Join(*root, rel), nil, parser.SkipObjectResolution)
|
|
556
|
+
if err != nil {
|
|
557
|
+
continue
|
|
558
|
+
}
|
|
559
|
+
for _, decl := range af.Decls {
|
|
560
|
+
fd, ok := decl.(*ast.FuncDecl)
|
|
561
|
+
if !ok || fd.Body == nil {
|
|
562
|
+
continue
|
|
563
|
+
}
|
|
564
|
+
for _, stmt := range fd.Body.List {
|
|
565
|
+
es, ok := stmt.(*ast.ExprStmt)
|
|
566
|
+
if !ok {
|
|
567
|
+
continue
|
|
568
|
+
}
|
|
569
|
+
call, isCall := es.X.(*ast.CallExpr)
|
|
570
|
+
if !isCall {
|
|
571
|
+
continue
|
|
572
|
+
}
|
|
573
|
+
// Fato sintático: registro de rota/middleware Gin nunca é
|
|
574
|
+
// candidato a "retorno de error ignorado" (retorna *Router).
|
|
575
|
+
if se, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
576
|
+
if _, isRoute := routeMethods[se.Sel.Name]; isRoute {
|
|
577
|
+
continue
|
|
578
|
+
}
|
|
579
|
+
if se.Sel.Name == "Use" || se.Sel.Name == "Group" || se.Sel.Name == "Static" || se.Sel.Name == "StaticFile" || se.Sel.Name == "StaticFS" || se.Sel.Name == "NoRoute" || se.Sel.Name == "NoMethod" {
|
|
580
|
+
continue
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
model.Files[i].ErrorHandling.UncheckedErrCalls = append(
|
|
584
|
+
model.Files[i].ErrorHandling.UncheckedErrCalls,
|
|
585
|
+
ErrSite{Expr: exprText(fset, es.X), Line: fset.Position(es.Pos()).Line},
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
enc := json.NewEncoder(os.Stdout)
|
|
592
|
+
if err := enc.Encode(model); err != nil {
|
|
593
|
+
fmt.Fprintln(os.Stderr, "encode:", err)
|
|
594
|
+
os.Exit(1)
|
|
595
|
+
}
|
|
596
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ihelpee/prumo-plugin-go-gin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Go/Gin plugin for Prumo: mutation route guards (inline/Group.Use/Engine.Use), error-handling enforcement, payload binding validation, and Go-native oracle isolation.",
|
|
6
|
+
"author": "Alex Gotardi <alexgotardi@gmail.com>",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"go-extractor",
|
|
19
|
+
"package.json",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"license": "Apache-2.0",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/ihelpee/prumo.git"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20.11",
|
|
30
|
+
"go": ">=1.21"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@ihelpee/prumo": "0.9.0"
|
|
37
|
+
}
|
|
6
38
|
}
|