github.com/neilgarb/delve@v1.9.2-nobreaks/_scripts/rtype.go (about) 1 // This script checks that the Go runtime hasn't changed in ways that Delve 2 // doesn't understand. It accomplishes this task by parsing the pkg/proc 3 // package and extracting rules from all the comments starting with the 4 // magic string '+rtype'. 5 // 6 // COMMAND LINE 7 // 8 // go run _scripts/rtype.go (report [output-file]|check) 9 // 10 // Invoked with the command 'report' it will extract rules from pkg/proc and 11 // print them to stdout. 12 // Invoked with the command 'check' it will actually check that the runtime 13 // conforms to the rules in pkg/proc. 14 // 15 // RTYPE RULES 16 // 17 // // +rtype -var V T 18 // 19 // checks that variable runtime.V exists and has type T 20 // 21 // // +rtype -field S.F T 22 // 23 // checks that struct runtime.S has a field called F of type T 24 // 25 // const C1 = V // +rtype C2 26 // 27 // checks that constant runtime.C2 exists and has value V 28 // 29 // case "F": // +rtype -fieldof S T 30 // 31 // checks that struct runtime.S has a field called F of type T 32 // 33 // v := ... // +rtype T 34 // 35 // if v is declared as *proc.Variable it will assume that it has type 36 // runtime.T and it will then parse the enclosing function, searching for 37 // all calls to: 38 // v.loadFieldNamed 39 // v.fieldVariable 40 // v.structMember 41 // and check that type T has the specified fields. 42 // 43 // v.loadFieldNamed("F") // +rtype T 44 // v.loadFieldNamed("F") // +rtype -opt T 45 // 46 // checks that field F of the struct type declared for v has type T. Can 47 // also be used for fieldVariable, structMember and, inside parseG, 48 // loadInt64Maybe. 49 // The -opt flag specifies that the field can be missing (but if it exists 50 // it must have type T). 51 // 52 // 53 // Anywhere a type is required anytype can be used to specify that we don't 54 // care about its type. 55 56 package main 57 58 import ( 59 "bytes" 60 "fmt" 61 "go/ast" 62 "go/constant" 63 "go/printer" 64 "go/token" 65 "go/types" 66 "log" 67 "os" 68 "path/filepath" 69 "sort" 70 "strconv" 71 "strings" 72 73 "golang.org/x/tools/go/packages" 74 ) 75 76 const magicCommentPrefix = "+rtype" 77 78 var fset = &token.FileSet{} 79 var checkVarTypeRules = []*checkVarType{} 80 var checkFieldTypeRules = map[string][]*checkFieldType{} 81 var checkConstValRules = map[string][]*checkConstVal{} 82 var showRuleOrigin = false 83 84 // rtypeCmnt represents a +rtype comment 85 type rtypeCmnt struct { 86 slash token.Pos 87 txt string 88 node ast.Node // associated node 89 toplevel ast.Decl // toplevel declaration that contains the Slash of the comment 90 stmt ast.Stmt 91 } 92 93 type checkVarType struct { 94 V, T string // V must have type T 95 pos token.Pos 96 } 97 98 func (c *checkVarType) String() string { 99 if showRuleOrigin { 100 pos := fset.Position(c.pos) 101 return fmt.Sprintf("var %s %s // %s:%d", c.V, c.T, relative(pos.Filename), pos.Line) 102 } 103 return fmt.Sprintf("var %s %s", c.V, c.T) 104 } 105 106 type checkFieldType struct { 107 S, F, T string // S.F must have type T 108 opt bool 109 pos token.Pos 110 } 111 112 func (c *checkFieldType) String() string { 113 pos := fset.Position(c.pos) 114 return fmt.Sprintf("field %s.%s %s // %s:%d", c.S, c.F, c.T, relative(pos.Filename), pos.Line) 115 } 116 117 type checkConstVal struct { 118 C string // const C = V 119 V constant.Value 120 pos token.Pos 121 } 122 123 func (c *checkConstVal) String() string { 124 if showRuleOrigin { 125 pos := fset.Position(c.pos) 126 return fmt.Sprintf("const %s = %s // %s:%d", c.C, c.V, relative(pos.Filename), pos.Line) 127 } 128 return fmt.Sprintf("const %s = %s", c.C, c.V) 129 } 130 131 func main() { 132 if len(os.Args) < 2 { 133 fmt.Fprintf(os.Stderr, "Wrong number of arguments.\n\trtype (report [output-file]|check)\n") 134 os.Exit(1) 135 } 136 137 command := os.Args[1] 138 139 setup() 140 141 switch command { 142 case "report": 143 if len(os.Args) > 2 { 144 fh, err := os.Create(os.Args[2]) 145 if err != nil { 146 log.Fatalf("error creating output file: %v", err) 147 } 148 defer fh.Close() 149 os.Stdout = fh 150 } 151 report() 152 case "check": 153 check() 154 default: 155 fmt.Fprintf(os.Stderr, "Wrong argument %s\n", command) 156 os.Exit(1) 157 } 158 } 159 160 // setup parses the proc package, extracting all +rtype comments and 161 // converting them into rules. 162 func setup() { 163 pkgs, err := packages.Load(&packages.Config{Mode: packages.LoadSyntax, Fset: fset}, "github.com/go-delve/delve/pkg/proc") 164 if err != nil { 165 log.Fatalf("could not load proc package: %v", err) 166 } 167 168 for _, file := range pkgs[0].Syntax { 169 cmntmap := ast.NewCommentMap(fset, file, file.Comments) 170 rtypeCmnts := getRtypeCmnts(file, cmntmap) 171 for _, rtcmnt := range rtypeCmnts { 172 if rtcmnt == nil { 173 continue 174 } 175 process(pkgs[0], rtcmnt, cmntmap, rtypeCmnts) 176 } 177 } 178 } 179 180 // getRtypeCmnts returns all +rtype comments inside 'file'. It also 181 // decorates them with the toplevel declaration that contains them as well 182 // as the statement they are associated with (where applicable). 183 func getRtypeCmnts(file *ast.File, cmntmap ast.CommentMap) []*rtypeCmnt { 184 r := []*rtypeCmnt{} 185 186 for n, cmntgrps := range cmntmap { 187 for _, cmntgrp := range cmntgrps { 188 if len(cmntgrp.List) == 0 { 189 continue 190 } 191 192 for _, cmnt := range cmntgrp.List { 193 txt := cleanupCommentText(cmnt.Text) 194 if !strings.HasPrefix(txt, magicCommentPrefix) { 195 continue 196 } 197 198 r = append(r, &rtypeCmnt{slash: cmnt.Slash, txt: txt, node: n}) 199 } 200 201 } 202 } 203 204 sort.Slice(r, func(i, j int) bool { return r[i].slash < r[j].slash }) 205 206 // assign each comment to the toplevel declaration that contains it 207 for i, j := 0, 0; i < len(r) && j < len(file.Decls); { 208 decl := file.Decls[j] 209 if decl.Pos() <= r[i].slash && r[i].slash < decl.End() { 210 r[i].toplevel = decl 211 i++ 212 } else { 213 j++ 214 } 215 } 216 217 // for comments declared inside a function also find the statement that contains them. 218 for i := range r { 219 fndecl, ok := r[i].toplevel.(*ast.FuncDecl) 220 if !ok { 221 continue 222 } 223 224 var lastStmt ast.Stmt 225 ast.Inspect(fndecl, func(n ast.Node) bool { 226 if stmt, _ := n.(ast.Stmt); stmt != nil { 227 lastStmt = stmt 228 } 229 if n == r[i].node { 230 r[i].stmt = lastStmt 231 } 232 return true 233 }) 234 } 235 236 return r 237 } 238 239 func cleanupCommentText(txt string) string { 240 if strings.HasPrefix(txt, "/*") || strings.HasPrefix(txt, "//") { 241 txt = txt[2:] 242 } 243 return strings.TrimSpace(strings.TrimSuffix(txt, "*/")) 244 } 245 246 // process processes a single +rtype comment, turning it into a rule. 247 // If the +rtype comment is associated with a *proc.Variable declaration 248 // then it also checks the containing function for all uses of that 249 // variable. 250 func process(pkg *packages.Package, rtcmnt *rtypeCmnt, cmntmap ast.CommentMap, rtcmnts []*rtypeCmnt) { 251 tinfo := pkg.TypesInfo 252 fields := strings.Split(rtcmnt.txt, " ") 253 254 switch fields[1] { 255 case "-var": 256 // -var V T 257 // requests that variable V is of type T 258 addCheckVarType(fields[2], fields[3], rtcmnt.slash) 259 case "-field": 260 // -field S.F T 261 // requests that field F of type S is of type T 262 v := strings.Split(fields[2], ".") 263 addCheckFieldType(v[0], v[1], fields[3], false, rtcmnt.slash) 264 default: 265 ok := false 266 if ident := isProcVariableDecl(rtcmnt.stmt, tinfo); ident != nil { 267 if len(fields) == 2 { 268 processProcVariableUses(rtcmnt.toplevel, tinfo, ident, cmntmap, rtcmnts, fields[1]) 269 ok = true 270 } else if len(fields) == 3 && fields[1] == "-opt" { 271 processProcVariableUses(rtcmnt.toplevel, tinfo, ident, cmntmap, rtcmnts, fields[2]) 272 ok = true 273 } 274 } else if ident := isConstDecl(rtcmnt.toplevel, rtcmnt.node); len(fields) == 2 && ident != nil { 275 addCheckConstVal(fields[1], constValue(tinfo.Defs[ident]), rtcmnt.slash) 276 ok = true 277 } else if F := isStringCaseClause(rtcmnt.stmt); F != "" && len(fields) == 4 && fields[1] == "-fieldof" { 278 addCheckFieldType(fields[2], F, fields[3], false, rtcmnt.slash) 279 ok = true 280 } 281 if !ok { 282 pos := fset.Position(rtcmnt.slash) 283 log.Fatalf("%s:%d: unrecognized +rtype comment\n", pos.Filename, pos.Line) 284 } 285 } 286 } 287 288 // isProcVariableDecl returns true if stmt is a declaration of a 289 // *proc.Variable variable. 290 func isProcVariableDecl(stmt ast.Stmt, tinfo *types.Info) *ast.Ident { 291 ass, _ := stmt.(*ast.AssignStmt) 292 if ass == nil { 293 return nil 294 } 295 if len(ass.Lhs) == 0 { 296 return nil 297 } 298 ident, _ := ass.Lhs[0].(*ast.Ident) 299 if ident == nil { 300 return nil 301 } 302 var typ types.Type 303 if def := tinfo.Defs[ident]; def != nil { 304 typ = def.Type() 305 } 306 if tv, ok := tinfo.Types[ident]; ok { 307 typ = tv.Type 308 } 309 if typ == nil { 310 return nil 311 } 312 if typ == nil || typ.String() != "*github.com/go-delve/delve/pkg/proc.Variable" { 313 return nil 314 } 315 return ident 316 } 317 318 func isConstDecl(toplevel ast.Decl, node ast.Node) *ast.Ident { 319 gendecl, _ := toplevel.(*ast.GenDecl) 320 if gendecl == nil { 321 return nil 322 } 323 if gendecl.Tok != token.CONST { 324 return nil 325 } 326 valspec, _ := node.(*ast.ValueSpec) 327 if valspec == nil { 328 return nil 329 } 330 if len(valspec.Names) != 1 { 331 return nil 332 } 333 return valspec.Names[0] 334 } 335 336 func isStringCaseClause(stmt ast.Stmt) string { 337 c, _ := stmt.(*ast.CaseClause) 338 if c == nil { 339 return "" 340 } 341 if len(c.List) != 1 { 342 return "" 343 } 344 lit := c.List[0].(*ast.BasicLit) 345 if lit == nil { 346 return "" 347 } 348 if lit.Kind != token.STRING { 349 return "" 350 } 351 r, _ := strconv.Unquote(lit.Value) 352 return r 353 } 354 355 // processProcVariableUses scans the body of the function declaration 'decl' 356 // looking for uses of 'procVarIdent' which is assumed to be an identifier 357 // for a *proc.Variable variable. 358 func processProcVariableUses(decl ast.Node, tinfo *types.Info, procVarIdent *ast.Ident, cmntmap ast.CommentMap, rtcmnts []*rtypeCmnt, S string) { 359 if len(S) > 0 && S[0] == '*' { 360 S = S[1:] 361 } 362 isParseG := false 363 if fndecl, _ := decl.(*ast.FuncDecl); fndecl != nil { 364 if fndecl.Name.Name == "parseG" { 365 if procVarIdent.Name == "v" { 366 isParseG = true 367 } 368 } 369 } 370 var lastStmt ast.Stmt 371 ast.Inspect(decl, func(n ast.Node) bool { 372 if stmt, _ := n.(ast.Stmt); stmt != nil { 373 lastStmt = stmt 374 } 375 376 fncall, _ := n.(*ast.CallExpr) 377 if fncall == nil { 378 return true 379 } 380 var methodName string 381 if isParseG { 382 if xident, _ := fncall.Fun.(*ast.Ident); xident != nil && xident.Name == "loadInt64Maybe" { 383 methodName = "loadInt64Maybe" 384 } 385 } 386 if methodName == "" { 387 sel, _ := fncall.Fun.(*ast.SelectorExpr) 388 if sel == nil { 389 return true 390 } 391 methodName = sel.Sel.Name 392 xident, _ := sel.X.(*ast.Ident) 393 if xident == nil { 394 return true 395 } 396 if xident.Obj != procVarIdent.Obj { 397 return true 398 } 399 } 400 if len(fncall.Args) < 1 { 401 return true 402 } 403 arg0, _ := fncall.Args[0].(*ast.BasicLit) 404 if arg0 == nil { 405 return true 406 } 407 if arg0.Kind != token.STRING { 408 return true 409 } 410 411 switch methodName { 412 case "loadFieldNamed", "fieldVariable", "loadInt64Maybe", "structMember": 413 rtcmntIdx := -1 414 if cmntgrps := cmntmap[lastStmt]; len(cmntgrps) > 0 && len(cmntgrps[0].List) > 0 { 415 rtcmntIdx = findComment(cmntgrps[0].List[0].Slash, rtcmnts) 416 } 417 typ := "anytype" 418 opt := false 419 420 if rtcmntIdx >= 0 { 421 fields := strings.Split(rtcmnts[rtcmntIdx].txt, " ") 422 if len(fields) == 2 { 423 typ = fields[1] 424 } else if len(fields) == 3 && fields[1] == "-opt" { 425 opt = true 426 typ = fields[2] 427 } 428 if isProcVariableDecl(lastStmt, tinfo) == nil { 429 // remove it because we have already processed it 430 rtcmnts[rtcmntIdx] = nil 431 } 432 } 433 F, _ := strconv.Unquote(arg0.Value) 434 addCheckFieldType(S, F, typ, opt, fncall.Pos()) 435 //printNode(fset, fncall) 436 default: 437 pos := fset.Position(n.Pos()) 438 log.Fatalf("unknown node at %s:%d", pos.Filename, pos.Line) 439 } 440 return true 441 }) 442 } 443 444 func findComment(slash token.Pos, rtcmnts []*rtypeCmnt) int { 445 for i := range rtcmnts { 446 if rtcmnts[i] != nil && rtcmnts[i].slash == slash { 447 return i 448 } 449 } 450 return -1 451 } 452 453 func addCheckVarType(V, T string, pos token.Pos) { 454 checkVarTypeRules = append(checkVarTypeRules, &checkVarType{V, T, pos}) 455 } 456 457 func addCheckFieldType(S, F, T string, opt bool, pos token.Pos) { 458 checkFieldTypeRules[S] = append(checkFieldTypeRules[S], &checkFieldType{S, F, T, opt, pos}) 459 } 460 461 func addCheckConstVal(C string, V constant.Value, pos token.Pos) { 462 checkConstValRules[C] = append(checkConstValRules[C], &checkConstVal{C, V, pos}) 463 } 464 465 // report writes a report of all rules derived from the proc package to stdout. 466 func report() { 467 for _, rule := range checkVarTypeRules { 468 fmt.Printf("%s\n\n", rule.String()) 469 } 470 471 var Ss []string 472 for S := range checkFieldTypeRules { 473 Ss = append(Ss, S) 474 } 475 sort.Strings(Ss) 476 for _, S := range Ss { 477 rules := checkFieldTypeRules[S] 478 fmt.Printf("type %s struct {\n", S) 479 for _, rule := range rules { 480 fmt.Printf("\t%s %s", rule.F, rule.T) 481 if rule.opt { 482 fmt.Printf(" (optional)") 483 } 484 pos := fset.Position(rule.pos) 485 if showRuleOrigin { 486 fmt.Printf("\t// %s:%d", relative(pos.Filename), pos.Line) 487 } 488 fmt.Printf("\n") 489 } 490 fmt.Printf("}\n\n") 491 } 492 493 var Cs []string 494 for C := range checkConstValRules { 495 Cs = append(Cs, C) 496 } 497 sort.Strings(Cs) 498 for _, C := range Cs { 499 rules := checkConstValRules[C] 500 for i, rule := range rules { 501 if i == 0 { 502 fmt.Printf("%s\n", rule.String()) 503 } else { 504 fmt.Printf("or %s\n", rule.String()) 505 } 506 } 507 fmt.Printf("\n") 508 } 509 } 510 511 // check parses the runtime package and checks that all the rules retrieved 512 // from the 'proc' package pass. 513 func check() { 514 pkgs2, err := packages.Load(&packages.Config{Mode: packages.LoadSyntax, Fset: fset}, "runtime") 515 if err != nil { 516 log.Fatalf("could not load runtime package: %v", err) 517 } 518 519 allok := true 520 521 for _, rule := range checkVarTypeRules { 522 //TODO: implement 523 pos := fset.Position(rule.pos) 524 def := pkgs2[0].Types.Scope().Lookup(rule.V) 525 if def == nil { 526 fmt.Fprintf(os.Stderr, "%s:%d: could not find variable %s\n", pos.Filename, pos.Line, rule.V) 527 allok = false 528 continue 529 } 530 if !matchType(def.Type(), rule.T) { 531 fmt.Fprintf(os.Stderr, "%s:%d: wrong type for variable %s, expected %s got %s\n", pos.Filename, pos.Line, rule.V, rule.T, typeStr(def.Type())) 532 allok = false 533 continue 534 } 535 } 536 537 var Ss []string 538 for S := range checkFieldTypeRules { 539 Ss = append(Ss, S) 540 } 541 sort.Strings(Ss) 542 for _, S := range Ss { 543 rules := checkFieldTypeRules[S] 544 pos := fset.Position(rules[0].pos) 545 546 def := pkgs2[0].Types.Scope().Lookup(S) 547 if def == nil { 548 fmt.Fprintf(os.Stderr, "%s:%d: could not find struct %s\n", pos.Filename, pos.Line, S) 549 allok = false 550 continue 551 } 552 553 typ := def.Type() 554 if typ == nil { 555 fmt.Fprintf(os.Stderr, "%s:%d: could not find struct %s\n", pos.Filename, pos.Line, S) 556 allok = false 557 continue 558 } 559 styp, _ := typ.Underlying().(*types.Struct) 560 if styp == nil { 561 fmt.Fprintf(os.Stderr, "%s:%d: could not find struct %s\n", pos.Filename, pos.Line, S) 562 allok = false 563 continue 564 } 565 566 for _, rule := range rules { 567 pos := fset.Position(rule.pos) 568 fieldType := fieldTypeByName(styp, rule.F) 569 if fieldType == nil { 570 if rule.opt { 571 continue 572 } 573 fmt.Fprintf(os.Stderr, "%s:%d: could not find field %s.%s\n", pos.Filename, pos.Line, rule.S, rule.F) 574 allok = false 575 continue 576 } 577 if !matchType(fieldType, rule.T) { 578 fmt.Fprintf(os.Stderr, "%s:%d: wrong type for field %s.%s, expected %s got %s\n", pos.Filename, pos.Line, rule.S, rule.F, rule.T, typeStr(fieldType)) 579 allok = false 580 continue 581 } 582 } 583 } 584 585 var Cs []string 586 for C := range checkConstValRules { 587 Cs = append(Cs, C) 588 } 589 sort.Strings(Cs) 590 for _, C := range Cs { 591 rules := checkConstValRules[C] 592 pos := fset.Position(rules[0].pos) 593 def := pkgs2[0].Types.Scope().Lookup(C) 594 if def == nil { 595 fmt.Fprintf(os.Stderr, "%s:%d: could not find constant %s\n", pos.Filename, pos.Line, C) 596 allok = false 597 continue 598 } 599 600 val := constValue(def) 601 found := false 602 for _, rule := range rules { 603 if val == rule.V { 604 found = true 605 } 606 } 607 if !found { 608 fmt.Fprintf(os.Stderr, "%s:%d: wrong value for constant %s (%s)\n", pos.Filename, pos.Line, C, val.String()) 609 allok = false 610 continue 611 } 612 } 613 614 if !allok { 615 os.Exit(1) 616 } 617 } 618 619 func fieldTypeByName(typ *types.Struct, name string) types.Type { 620 for i := 0; i < typ.NumFields(); i++ { 621 field := typ.Field(i) 622 if field.Name() == name { 623 return field.Type() 624 } 625 } 626 return nil 627 } 628 629 func matchType(typ types.Type, T string) bool { 630 if T == "anytype" { 631 return true 632 } 633 return typeStr(typ) == T 634 } 635 636 func typeStr(typ types.Type) string { 637 return types.TypeString(typ, func(pkg *types.Package) string { 638 if pkg.Path() == "runtime" { 639 return "" 640 } 641 return pkg.Path() 642 }) 643 } 644 645 func constValue(obj types.Object) constant.Value { 646 return obj.(*types.Const).Val() 647 } 648 649 func printNode(fset *token.FileSet, n ast.Node) { 650 ast.Fprint(os.Stderr, fset, n, nil) 651 } 652 653 func exprToString(t ast.Expr) string { 654 var buf bytes.Buffer 655 printer.Fprint(&buf, token.NewFileSet(), t) 656 return buf.String() 657 } 658 659 func relative(s string) string { 660 wd, _ := os.Getwd() 661 r, err := filepath.Rel(wd, s) 662 if err != nil { 663 return s 664 } 665 return r 666 }