package main import ( "bytes" "fmt" "go/ast" "go/build" "go/doc" "go/parser" "go/printer" "go/token" "os" "path/filepath" "strconv" "strings" ) // parseSourceFlag parses the "-source" flag value. It must have "import/path".VariableName format. // It returns an error if the parsed import path is relative. func parseSourceFlag(sourceFlag string) (importPath, variableName string, err error) { // Parse sourceFlag as a Go expression, albeit a strange one: // // "import/path".VariableName // e, err := parser.ParseExpr(sourceFlag) if err != nil { return "", "", fmt.Errorf("invalid format, failed to parse %q as a Go expression", sourceFlag) } se, ok := e.(*ast.SelectorExpr) if !ok { return "", "", fmt.Errorf("invalid format, expression %v is not a selector expression but %T", sourceFlag, e) } importPath, err = stringValue(se.X) if err != nil { return "", "", fmt.Errorf("invalid format, expression %v is not a properly quoted Go string: %v", stringifyAST(se.X), err) } if build.IsLocalImport(importPath) { // Generated code is executed in a temporary directory, // and can't use relative import paths. So disallow them. return "", "", fmt.Errorf("relative import paths are not supported") } variableName = se.Sel.Name return importPath, variableName, nil } // stringValue returns the string value of string literal e. func stringValue(e ast.Expr) (string, error) { lit, ok := e.(*ast.BasicLit) if !ok { return "", fmt.Errorf("not a string, but %T", e) } if lit.Kind != token.STRING { return "", fmt.Errorf("not a string, but %v", lit.Kind) } return strconv.Unquote(lit.Value) } // parseTagFlag parses the "-tag" flag value. It must be a single build tag. func parseTagFlag(tagFlag string) (tag string, err error) { tags := strings.Fields(tagFlag) if len(tags) != 1 { return "", fmt.Errorf("%q is not a valid single build tag, but %q", tagFlag, tags) } return tags[0], nil } // lookupNameAndComment imports package using provided build context, and // returns the package name and variable comment. func lookupNameAndComment(bctx build.Context, importPath, variableName string) (packageName, variableComment string, err error) { wd, err := os.Getwd() if err != nil { return "", "", err } bpkg, err := bctx.Import(importPath, wd, 0) if err != nil { return "", "", fmt.Errorf("can't import package %q: %v", importPath, err) } dpkg, err := computeDoc(bpkg) if err != nil { return "", "", fmt.Errorf("can't get godoc of package %q: %v", importPath, err) } for _, v := range dpkg.Vars { if len(v.Names) == 1 && v.Names[0] == variableName { variableComment = strings.TrimSuffix(v.Doc, "\n") break } } return bpkg.Name, variableComment, nil } func stringifyAST(node interface{}) string { var buf bytes.Buffer err := printer.Fprint(&buf, token.NewFileSet(), node) if err != nil { return "printer.Fprint error: " + err.Error() } return buf.String() } // computeDoc computes the package documentation for the given package. func computeDoc(bpkg *build.Package) (*doc.Package, error) { fset := token.NewFileSet() files := make(map[string]*ast.File) for _, file := range append(bpkg.GoFiles, bpkg.CgoFiles...) { f, err := parser.ParseFile(fset, filepath.Join(bpkg.Dir, file), nil, parser.ParseComments) if err != nil { return nil, err } files[file] = f } apkg := &ast.Package{ Name: bpkg.Name, Files: files, } return doc.New(apkg, bpkg.ImportPath, 0), nil }