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  }