| // Copyright 2009 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // The doc package extracts source code documentation from a Go AST. |
| package doc |
| |
| import ( |
| "container/vector"; |
| "go/ast"; |
| "go/token"; |
| "regexp"; |
| "sort"; |
| ) |
| |
| |
| // ---------------------------------------------------------------------------- |
| |
| type typeDoc struct { |
| // len(decl.Specs) == 1, and the element type is *ast.TypeSpec |
| // if the type declaration hasn't been seen yet, decl is nil |
| decl *ast.GenDecl; |
| // values, factory functions, and methods associated with the type |
| values *vector.Vector; // list of *ast.GenDecl (consts and vars) |
| factories map[string]*ast.FuncDecl; |
| methods map[string]*ast.FuncDecl; |
| } |
| |
| |
| // docReader accumulates documentation for a single package. |
| // It modifies the AST: Comments (declaration documentation) |
| // that have been collected by the DocReader are set to nil |
| // in the respective AST nodes so that they are not printed |
| // twice (once when printing the documentation and once when |
| // printing the corresponding AST node). |
| // |
| type docReader struct { |
| doc *ast.CommentGroup; // package documentation, if any |
| pkgName string; |
| values *vector.Vector; // list of *ast.GenDecl (consts and vars) |
| types map[string]*typeDoc; |
| funcs map[string]*ast.FuncDecl; |
| bugs *vector.Vector; // list of *ast.CommentGroup |
| } |
| |
| |
| func (doc *docReader) init(pkgName string) { |
| doc.pkgName = pkgName; |
| doc.values = vector.New(0); |
| doc.types = make(map[string]*typeDoc); |
| doc.funcs = make(map[string]*ast.FuncDecl); |
| doc.bugs = vector.New(0); |
| } |
| |
| |
| func (doc *docReader) addType(decl *ast.GenDecl) { |
| spec := decl.Specs[0].(*ast.TypeSpec); |
| typ := doc.lookupTypeDoc(spec.Name.Value); |
| // typ should always be != nil since declared types |
| // are always named - be conservative and check |
| if typ != nil { |
| // a type should be added at most once, so typ.decl |
| // should be nil - if it isn't, simply overwrite it |
| typ.decl = decl |
| } |
| } |
| |
| |
| func (doc *docReader) lookupTypeDoc(name string) *typeDoc { |
| if name == "" { |
| return nil // no type docs for anonymous types |
| } |
| if tdoc, found := doc.types[name]; found { |
| return tdoc |
| } |
| // type wasn't found - add one without declaration |
| tdoc := &typeDoc{nil, vector.New(0), make(map[string]*ast.FuncDecl), make(map[string]*ast.FuncDecl)}; |
| doc.types[name] = tdoc; |
| return tdoc; |
| } |
| |
| |
| func baseTypeName(typ ast.Expr) string { |
| switch t := typ.(type) { |
| case *ast.Ident: |
| // if the type is not exported, the effect to |
| // a client is as if there were no type name |
| if t.IsExported() { |
| return string(t.Value) |
| } |
| case *ast.StarExpr: |
| return baseTypeName(t.X) |
| } |
| return ""; |
| } |
| |
| |
| func (doc *docReader) addValue(decl *ast.GenDecl) { |
| // determine if decl should be associated with a type |
| // Heuristic: For each typed entry, determine the type name, if any. |
| // If there is exactly one type name that is sufficiently |
| // frequent, associate the decl with the respective type. |
| domName := ""; |
| domFreq := 0; |
| prev := ""; |
| for _, s := range decl.Specs { |
| if v, ok := s.(*ast.ValueSpec); ok { |
| name := ""; |
| switch { |
| case v.Type != nil: |
| // a type is present; determine it's name |
| name = baseTypeName(v.Type) |
| case decl.Tok == token.CONST: |
| // no type is present but we have a constant declaration; |
| // use the previous type name (w/o more type information |
| // we cannot handle the case of unnamed variables with |
| // initializer expressions except for some trivial cases) |
| name = prev |
| } |
| if name != "" { |
| // entry has a named type |
| if domName != "" && domName != name { |
| // more than one type name - do not associate |
| // with any type |
| domName = ""; |
| break; |
| } |
| domName = name; |
| domFreq++; |
| } |
| prev = name; |
| } |
| } |
| |
| // determine values list |
| const threshold = 0.75; |
| values := doc.values; |
| if domName != "" && domFreq >= int(float(len(decl.Specs))*threshold) { |
| // typed entries are sufficiently frequent |
| typ := doc.lookupTypeDoc(domName); |
| if typ != nil { |
| values = typ.values // associate with that type |
| } |
| } |
| |
| values.Push(decl); |
| } |
| |
| |
| func (doc *docReader) addFunc(fun *ast.FuncDecl) { |
| name := fun.Name.Value; |
| |
| // determine if it should be associated with a type |
| if fun.Recv != nil { |
| // method |
| typ := doc.lookupTypeDoc(baseTypeName(fun.Recv.Type)); |
| if typ != nil { |
| // exported receiver type |
| typ.methods[name] = fun |
| } |
| // otherwise don't show the method |
| // TODO(gri): There may be exported methods of non-exported types |
| // that can be called because of exported values (consts, vars, or |
| // function results) of that type. Could determine if that is the |
| // case and then show those methods in an appropriate section. |
| return; |
| } |
| |
| // perhaps a factory function |
| // determine result type, if any |
| if len(fun.Type.Results) >= 1 { |
| res := fun.Type.Results[0]; |
| if len(res.Names) <= 1 { |
| // exactly one (named or anonymous) result associated |
| // with the first type in result signature (there may |
| // be more than one result) |
| tname := baseTypeName(res.Type); |
| typ := doc.lookupTypeDoc(tname); |
| if typ != nil { |
| // named and exported result type |
| |
| // Work-around for failure of heuristic: In package os |
| // too many functions are considered factory functions |
| // for the Error type. Eliminate manually for now as |
| // this appears to be the only important case in the |
| // current library where the heuristic fails. |
| if doc.pkgName == "os" && tname == "Error" && |
| name != "NewError" && name != "NewSyscallError" { |
| // not a factory function for os.Error |
| doc.funcs[name] = fun; // treat as ordinary function |
| return; |
| } |
| |
| typ.factories[name] = fun; |
| return; |
| } |
| } |
| } |
| |
| // ordinary function |
| doc.funcs[name] = fun; |
| } |
| |
| |
| func (doc *docReader) addDecl(decl ast.Decl) { |
| switch d := decl.(type) { |
| case *ast.GenDecl: |
| if len(d.Specs) > 0 { |
| switch d.Tok { |
| case token.CONST, token.VAR: |
| // constants and variables are always handled as a group |
| doc.addValue(d) |
| case token.TYPE: |
| // types are handled individually |
| var noPos token.Position; |
| for _, spec := range d.Specs { |
| // make a (fake) GenDecl node for this TypeSpec |
| // (we need to do this here - as opposed to just |
| // for printing - so we don't lose the GenDecl |
| // documentation) |
| // |
| // TODO(gri): Consider just collecting the TypeSpec |
| // node (and copy in the GenDecl.doc if there is no |
| // doc in the TypeSpec - this is currently done in |
| // makeTypeDocs below). Simpler data structures, but |
| // would lose GenDecl documentation if the TypeSpec |
| // has documentation as well. |
| doc.addType(&ast.GenDecl{d.Doc, d.Pos(), token.TYPE, noPos, []ast.Spec{spec}, noPos}) |
| // A new GenDecl node is created, no need to nil out d.Doc. |
| } |
| } |
| } |
| case *ast.FuncDecl: |
| doc.addFunc(d) |
| } |
| } |
| |
| |
| func copyCommentList(list []*ast.Comment) []*ast.Comment { |
| copy := make([]*ast.Comment, len(list)); |
| for i, c := range list { |
| copy[i] = c |
| } |
| return copy; |
| } |
| |
| |
| var ( |
| bug_markers = regexp.MustCompile("^/[/*][ \t]*BUG\\(.*\\):[ \t]*"); // BUG(uid): |
| bug_content = regexp.MustCompile("[^ \n\r\t]+"); // at least one non-whitespace char |
| ) |
| |
| |
| // addFile adds the AST for a source file to the docReader. |
| // Adding the same AST multiple times is a no-op. |
| // |
| func (doc *docReader) addFile(src *ast.File) { |
| // add package documentation |
| if src.Doc != nil { |
| // TODO(gri) This won't do the right thing if there is more |
| // than one file with package comments. Consider |
| // using ast.MergePackageFiles which handles these |
| // comments correctly (but currently looses BUG(...) |
| // comments). |
| doc.doc = src.Doc; |
| src.Doc = nil; // doc consumed - remove from ast.File node |
| } |
| |
| // add all declarations |
| for _, decl := range src.Decls { |
| doc.addDecl(decl) |
| } |
| |
| // collect BUG(...) comments |
| for c := src.Comments; c != nil; c = c.Next { |
| text := c.List[0].Text; |
| cstr := string(text); |
| if m := bug_markers.ExecuteString(cstr); len(m) > 0 { |
| // found a BUG comment; maybe empty |
| if bstr := cstr[m[1]:]; bug_content.MatchString(bstr) { |
| // non-empty BUG comment; collect comment without BUG prefix |
| list := copyCommentList(c.List); |
| list[0].Text = text[m[1]:]; |
| doc.bugs.Push(&ast.CommentGroup{list, nil}); |
| } |
| } |
| } |
| src.Comments = nil; // consumed unassociated comments - remove from ast.File node |
| } |
| |
| |
| func NewFileDoc(file *ast.File) *PackageDoc { |
| var r docReader; |
| r.init(file.Name.Value); |
| r.addFile(file); |
| return r.newDoc("", "", nil); |
| } |
| |
| |
| func NewPackageDoc(pkg *ast.Package, importpath string) *PackageDoc { |
| var r docReader; |
| r.init(pkg.Name); |
| filenames := make([]string, len(pkg.Files)); |
| i := 0; |
| for filename, f := range pkg.Files { |
| r.addFile(f); |
| filenames[i] = filename; |
| i++; |
| } |
| return r.newDoc(importpath, pkg.Path, filenames); |
| } |
| |
| |
| // ---------------------------------------------------------------------------- |
| // Conversion to external representation |
| |
| // ValueDoc is the documentation for a group of declared |
| // values, either vars or consts. |
| // |
| type ValueDoc struct { |
| Doc string; |
| Decl *ast.GenDecl; |
| order int; |
| } |
| |
| type sortValueDoc []*ValueDoc |
| |
| func (p sortValueDoc) Len() int { return len(p) } |
| func (p sortValueDoc) Swap(i, j int) { p[i], p[j] = p[j], p[i] } |
| |
| |
| func declName(d *ast.GenDecl) string { |
| if len(d.Specs) != 1 { |
| return "" |
| } |
| |
| switch v := d.Specs[0].(type) { |
| case *ast.ValueSpec: |
| return v.Names[0].Value |
| case *ast.TypeSpec: |
| return v.Name.Value |
| } |
| |
| return ""; |
| } |
| |
| |
| func (p sortValueDoc) Less(i, j int) bool { |
| // sort by name |
| // pull blocks (name = "") up to top |
| // in original order |
| if ni, nj := declName(p[i].Decl), declName(p[j].Decl); ni != nj { |
| return ni < nj |
| } |
| return p[i].order < p[j].order; |
| } |
| |
| |
| func makeValueDocs(v *vector.Vector, tok token.Token) []*ValueDoc { |
| d := make([]*ValueDoc, v.Len()); // big enough in any case |
| n := 0; |
| for i := range d { |
| decl := v.At(i).(*ast.GenDecl); |
| if decl.Tok == tok { |
| d[n] = &ValueDoc{CommentText(decl.Doc), decl, i}; |
| n++; |
| decl.Doc = nil; // doc consumed - removed from AST |
| } |
| } |
| d = d[0:n]; |
| sort.Sort(sortValueDoc(d)); |
| return d; |
| } |
| |
| |
| // FuncDoc is the documentation for a func declaration, |
| // either a top-level function or a method function. |
| // |
| type FuncDoc struct { |
| Doc string; |
| Recv ast.Expr; // TODO(rsc): Would like string here |
| Name string; |
| Decl *ast.FuncDecl; |
| } |
| |
| type sortFuncDoc []*FuncDoc |
| |
| func (p sortFuncDoc) Len() int { return len(p) } |
| func (p sortFuncDoc) Swap(i, j int) { p[i], p[j] = p[j], p[i] } |
| func (p sortFuncDoc) Less(i, j int) bool { return p[i].Name < p[j].Name } |
| |
| |
| func makeFuncDocs(m map[string]*ast.FuncDecl) []*FuncDoc { |
| d := make([]*FuncDoc, len(m)); |
| i := 0; |
| for _, f := range m { |
| doc := new(FuncDoc); |
| doc.Doc = CommentText(f.Doc); |
| f.Doc = nil; // doc consumed - remove from ast.FuncDecl node |
| if f.Recv != nil { |
| doc.Recv = f.Recv.Type |
| } |
| doc.Name = f.Name.Value; |
| doc.Decl = f; |
| d[i] = doc; |
| i++; |
| } |
| sort.Sort(sortFuncDoc(d)); |
| return d; |
| } |
| |
| |
| // TypeDoc is the documentation for a declared type. |
| // Consts and Vars are sorted lists of constants and variables of (mostly) that type. |
| // Factories is a sorted list of factory functions that return that type. |
| // Methods is a sorted list of method functions on that type. |
| type TypeDoc struct { |
| Doc string; |
| Type *ast.TypeSpec; |
| Consts []*ValueDoc; |
| Vars []*ValueDoc; |
| Factories []*FuncDoc; |
| Methods []*FuncDoc; |
| Decl *ast.GenDecl; |
| order int; |
| } |
| |
| type sortTypeDoc []*TypeDoc |
| |
| func (p sortTypeDoc) Len() int { return len(p) } |
| func (p sortTypeDoc) Swap(i, j int) { p[i], p[j] = p[j], p[i] } |
| func (p sortTypeDoc) Less(i, j int) bool { |
| // sort by name |
| // pull blocks (name = "") up to top |
| // in original order |
| if ni, nj := p[i].Type.Name.Value, p[j].Type.Name.Value; ni != nj { |
| return ni < nj |
| } |
| return p[i].order < p[j].order; |
| } |
| |
| |
| // NOTE(rsc): This would appear not to be correct for type ( ) |
| // blocks, but the doc extractor above has split them into |
| // individual declarations. |
| func (doc *docReader) makeTypeDocs(m map[string]*typeDoc) []*TypeDoc { |
| d := make([]*TypeDoc, len(m)); |
| i := 0; |
| for _, old := range m { |
| // all typeDocs should have a declaration associated with |
| // them after processing an entire package - be conservative |
| // and check |
| if decl := old.decl; decl != nil { |
| typespec := decl.Specs[0].(*ast.TypeSpec); |
| t := new(TypeDoc); |
| doc := typespec.Doc; |
| typespec.Doc = nil; // doc consumed - remove from ast.TypeSpec node |
| if doc == nil { |
| // no doc associated with the spec, use the declaration doc, if any |
| doc = decl.Doc |
| } |
| decl.Doc = nil; // doc consumed - remove from ast.Decl node |
| t.Doc = CommentText(doc); |
| t.Type = typespec; |
| t.Consts = makeValueDocs(old.values, token.CONST); |
| t.Vars = makeValueDocs(old.values, token.VAR); |
| t.Factories = makeFuncDocs(old.factories); |
| t.Methods = makeFuncDocs(old.methods); |
| t.Decl = old.decl; |
| t.order = i; |
| d[i] = t; |
| i++; |
| } else { |
| // no corresponding type declaration found - move any associated |
| // values, factory functions, and methods back to the top-level |
| // so that they are not lost (this should only happen if a package |
| // file containing the explicit type declaration is missing or if |
| // an unqualified type name was used after a "." import) |
| // 1) move values |
| doc.values.AppendVector(old.values); |
| // 2) move factory functions |
| for name, f := range old.factories { |
| doc.funcs[name] = f |
| } |
| // 3) move methods |
| for name, f := range old.methods { |
| // don't overwrite functions with the same name |
| if _, found := doc.funcs[name]; !found { |
| doc.funcs[name] = f |
| } |
| } |
| } |
| } |
| d = d[0:i]; // some types may have been ignored |
| sort.Sort(sortTypeDoc(d)); |
| return d; |
| } |
| |
| |
| func makeBugDocs(v *vector.Vector) []string { |
| d := make([]string, v.Len()); |
| for i := 0; i < v.Len(); i++ { |
| d[i] = CommentText(v.At(i).(*ast.CommentGroup)) |
| } |
| return d; |
| } |
| |
| |
| // PackageDoc is the documentation for an entire package. |
| // |
| type PackageDoc struct { |
| PackageName string; |
| ImportPath string; |
| FilePath string; |
| Filenames []string; |
| Doc string; |
| Consts []*ValueDoc; |
| Types []*TypeDoc; |
| Vars []*ValueDoc; |
| Funcs []*FuncDoc; |
| Bugs []string; |
| } |
| |
| |
| // newDoc returns the accumulated documentation for the package. |
| // |
| func (doc *docReader) newDoc(importpath, filepath string, filenames []string) *PackageDoc { |
| p := new(PackageDoc); |
| p.PackageName = doc.pkgName; |
| p.ImportPath = importpath; |
| p.FilePath = filepath; |
| sort.SortStrings(filenames); |
| p.Filenames = filenames; |
| p.Doc = CommentText(doc.doc); |
| // makeTypeDocs may extend the list of doc.values and |
| // doc.funcs and thus must be called before any other |
| // function consuming those lists |
| p.Types = doc.makeTypeDocs(doc.types); |
| p.Consts = makeValueDocs(doc.values, token.CONST); |
| p.Vars = makeValueDocs(doc.values, token.VAR); |
| p.Funcs = makeFuncDocs(doc.funcs); |
| p.Bugs = makeBugDocs(doc.bugs); |
| return p; |
| } |
| |
| |
| // ---------------------------------------------------------------------------- |
| // Filtering by name |
| |
| // Does s look like a regular expression? |
| func isRegexp(s string) bool { |
| metachars := ".(|)*+?^$[]"; |
| for _, c := range s { |
| for _, m := range metachars { |
| if c == m { |
| return true |
| } |
| } |
| } |
| return false; |
| } |
| |
| |
| func match(s string, a []string) bool { |
| for _, t := range a { |
| if isRegexp(t) { |
| if matched, _ := regexp.MatchString(t, s); matched { |
| return true |
| } |
| } |
| if s == t { |
| return true |
| } |
| } |
| return false; |
| } |
| |
| |
| func matchDecl(d *ast.GenDecl, names []string) bool { |
| for _, d := range d.Specs { |
| switch v := d.(type) { |
| case *ast.ValueSpec: |
| for _, name := range v.Names { |
| if match(name.Value, names) { |
| return true |
| } |
| } |
| case *ast.TypeSpec: |
| if match(v.Name.Value, names) { |
| return true |
| } |
| } |
| } |
| return false; |
| } |
| |
| |
| func filterValueDocs(a []*ValueDoc, names []string) []*ValueDoc { |
| w := 0; |
| for _, vd := range a { |
| if matchDecl(vd.Decl, names) { |
| a[w] = vd; |
| w++; |
| } |
| } |
| return a[0:w]; |
| } |
| |
| |
| func filterFuncDocs(a []*FuncDoc, names []string) []*FuncDoc { |
| w := 0; |
| for _, fd := range a { |
| if match(fd.Name, names) { |
| a[w] = fd; |
| w++; |
| } |
| } |
| return a[0:w]; |
| } |
| |
| |
| func filterTypeDocs(a []*TypeDoc, names []string) []*TypeDoc { |
| w := 0; |
| for _, td := range a { |
| match := false; |
| if matchDecl(td.Decl, names) { |
| match = true |
| } else { |
| // type name doesn't match, but we may have matching factories or methods |
| td.Factories = filterFuncDocs(td.Factories, names); |
| td.Methods = filterFuncDocs(td.Methods, names); |
| match = len(td.Factories) > 0 || len(td.Methods) > 0; |
| } |
| if match { |
| a[w] = td; |
| w++; |
| } |
| } |
| return a[0:w]; |
| } |
| |
| |
| // Filter eliminates information from d that is not |
| // about one of the given names. |
| // TODO: Recognize "Type.Method" as a name. |
| // TODO(r): maybe precompile the regexps. |
| // |
| func (p *PackageDoc) Filter(names []string) { |
| p.Consts = filterValueDocs(p.Consts, names); |
| p.Vars = filterValueDocs(p.Vars, names); |
| p.Types = filterTypeDocs(p.Types, names); |
| p.Funcs = filterFuncDocs(p.Funcs, names); |
| p.Doc = ""; // don't show top-level package doc |
| } |