From 7ea67b5534b911c2ae72ba41722563e525b06a68 Mon Sep 17 00:00:00 2001 From: Unknown Date: Sat, 27 Jul 2013 09:44:44 +0800 Subject: [PATCH] Added func getControllerInfo --- README.md | 7 +- autorouter.go | 282 ++++++++++++++++++++++++++++++++++++++ autorouter_test.go | 9 ++ bee.go | 15 ++ bee.json | 10 +- code.go | 262 +++++++++++++++++++++++++++++++++++ run.go | 17 ++- testdata/router/router.go | 28 ++++ 8 files changed, 622 insertions(+), 8 deletions(-) create mode 100644 autorouter.go create mode 100644 autorouter_test.go create mode 100644 code.go create mode 100644 testdata/router/router.go diff --git a/README.md b/README.md index 2007357..28b76ff 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ bee === + [![Build Status](https://drone.io/github.com/astaxie/bee/status.png)](https://drone.io/github.com/astaxie/bee/latest) -Bee is a tool for managing beego framework. \ No newline at end of file +Bee is a tool for managing beego framework. + +## License + +[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). \ No newline at end of file diff --git a/autorouter.go b/autorouter.go new file mode 100644 index 0000000..2de7bfb --- /dev/null +++ b/autorouter.go @@ -0,0 +1,282 @@ +// Copyright 2013 Bee Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package main + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + gobuild "go/build" + "go/doc" + "go/parser" + "go/token" + "io" + "io/ioutil" + "os" + "path" + "runtime" + "strings" + "time" +) + +var cmdRouter = &Command{ + UsageLine: "router", + Short: "auto-generate routers for the app controllers", + Long: ` + +`, +} + +func init() { + cmdRouter.Run = autoRouter +} + +func autoRouter(cmd *Command, args []string) { + fmt.Println("[INFO] Starting auto-generating routers...") +} + +// getControllerInfo returns controllers that embeded "beego.controller" +// and their methods of package in given path. +func getControllerInfo(path string) (map[string][]string, error) { + now := time.Now() + path = strings.TrimSuffix(path, "/") + dir, err := os.Open(path) + if err != nil { + return nil, err + } + + fis, err := dir.Readdir(0) + if err != nil { + return nil, err + } + + files := make([]*source, 0, len(fis)) + for _, fi := range fis { + // Only load go files. + if strings.HasSuffix(fi.Name(), ".go") { + f, err := os.Open(path + "/" + fi.Name()) + if err != nil { + return nil, err + } + + p := make([]byte, fi.Size()) + _, err = f.Read(p) + if err != nil { + return nil, err + } + + f.Close() + files = append(files, &source{ + name: path + "/" + fi.Name(), + data: p, + }) + } + } + + rw := &routerWalker{ + pdoc: &Package{ + ImportPath: path, + }, + } + + cm := make(map[string][]string) + pdoc, err := rw.build(files) + for _, t := range pdoc.Types { + // Check if embeded "beego.Controller". + if strings.Index(t.Decl, "beego.Controller") > -1 { + for _, f := range t.Methods { + cm[t.Name] = append(cm[t.Name], f.Name) + } + } + } + fmt.Println(time.Since(now)) + return cm, nil +} + +// A source describles a source code file. +type source struct { + name string + data []byte +} + +func (s *source) Name() string { return s.name } +func (s *source) Size() int64 { return int64(len(s.data)) } +func (s *source) Mode() os.FileMode { return 0 } +func (s *source) ModTime() time.Time { return time.Time{} } +func (s *source) IsDir() bool { return false } +func (s *source) Sys() interface{} { return nil } + +// A routerWalker holds the state used when building the documentation. +type routerWalker struct { + pdoc *Package + srcs map[string]*source // Source files. + fset *token.FileSet + buf []byte // scratch space for printNode method. +} + +// Package represents full information and documentation for a package. +type Package struct { + ImportPath string + + // Top-level declarations. + Types []*Type +} + +// Type represents structs and interfaces. +type Type struct { + Name string // Type name. + Decl string + Methods []*Func // Exported methods. +} + +// Func represents functions +type Func struct { + Name string +} + +// build generates data from source files. +func (w *routerWalker) build(srcs []*source) (*Package, error) { + // Add source files to walker, I skipped references here. + w.srcs = make(map[string]*source) + for _, src := range srcs { + w.srcs[src.name] = src + } + + w.fset = token.NewFileSet() + + // Find the package and associated files. + ctxt := gobuild.Context{ + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + CgoEnabled: true, + JoinPath: path.Join, + IsAbsPath: path.IsAbs, + SplitPathList: func(list string) []string { return strings.Split(list, ":") }, + IsDir: func(path string) bool { panic("unexpected") }, + HasSubdir: func(root, dir string) (rel string, ok bool) { panic("unexpected") }, + ReadDir: func(dir string) (fi []os.FileInfo, err error) { return w.readDir(dir) }, + OpenFile: func(path string) (r io.ReadCloser, err error) { return w.openFile(path) }, + Compiler: "gc", + } + + bpkg, err := ctxt.ImportDir(w.pdoc.ImportPath, 0) + // Continue if there are no Go source files; we still want the directory info. + _, nogo := err.(*gobuild.NoGoError) + if err != nil { + if nogo { + err = nil + } else { + return nil, errors.New("routerWalker.build -> " + err.Error()) + } + } + + // Parse the Go files + files := make(map[string]*ast.File) + for _, name := range append(bpkg.GoFiles, bpkg.CgoFiles...) { + file, err := parser.ParseFile(w.fset, name, w.srcs[name].data, parser.ParseComments) + if err != nil { + return nil, errors.New("routerWalker.build -> parse go files: " + err.Error()) + } + files[name] = file + } + + apkg, _ := ast.NewPackage(w.fset, files, simpleImporter, nil) + + mode := doc.Mode(0) + if w.pdoc.ImportPath == "builtin" { + mode |= doc.AllDecls + } + + pdoc := doc.New(apkg, w.pdoc.ImportPath, mode) + + w.pdoc.Types = w.types(pdoc.Types) + + return w.pdoc, err +} + +func (w *routerWalker) funcs(fdocs []*doc.Func) []*Func { + var result []*Func + for _, d := range fdocs { + result = append(result, &Func{ + Name: d.Name, + }) + } + return result +} + +func (w *routerWalker) types(tdocs []*doc.Type) []*Type { + var result []*Type + for _, d := range tdocs { + result = append(result, &Type{ + Decl: w.printDecl(d.Decl), + Name: d.Name, + Methods: w.funcs(d.Methods), + }) + } + return result +} + +func (w *routerWalker) printDecl(decl ast.Node) string { + var d Code + d, w.buf = printDecl(decl, w.fset, w.buf) + return d.Text +} + +func (w *routerWalker) readDir(dir string) ([]os.FileInfo, error) { + if dir != w.pdoc.ImportPath { + panic("unexpected") + } + fis := make([]os.FileInfo, 0, len(w.srcs)) + for _, src := range w.srcs { + fis = append(fis, src) + } + return fis, nil +} + +func (w *routerWalker) openFile(path string) (io.ReadCloser, error) { + if strings.HasPrefix(path, w.pdoc.ImportPath+"/") { + if src, ok := w.srcs[path[len(w.pdoc.ImportPath)+1:]]; ok { + return ioutil.NopCloser(bytes.NewReader(src.data)), nil + } + } + panic("unexpected") +} + +func simpleImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) { + pkg := imports[path] + if pkg == nil { + // Guess the package name without importing it. Start with the last + // element of the path. + name := path[strings.LastIndex(path, "/")+1:] + + // Trim commonly used prefixes and suffixes containing illegal name + // runes. + name = strings.TrimSuffix(name, ".go") + name = strings.TrimSuffix(name, "-go") + name = strings.TrimPrefix(name, "go.") + name = strings.TrimPrefix(name, "go-") + name = strings.TrimPrefix(name, "biogo.") + + // It's also common for the last element of the path to contain an + // extra "go" prefix, but not always. TODO: examine unresolved ids to + // detect when trimming the "go" prefix is appropriate. + + pkg = ast.NewObj(ast.Pkg, name) + pkg.Data = ast.NewScope(nil) + imports[path] = pkg + } + return pkg, nil +} diff --git a/autorouter_test.go b/autorouter_test.go new file mode 100644 index 0000000..437f3ee --- /dev/null +++ b/autorouter_test.go @@ -0,0 +1,9 @@ +package main + +import ( + "testing" +) + +func TestGetControllerInfo(t *testing.T) { + getControllerInfo("testdata/router/") +} diff --git a/bee.go b/bee.go index 1ad6cb7..5450aea 100644 --- a/bee.go +++ b/bee.go @@ -1,3 +1,17 @@ +// Copyright 2013 Bee Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + // Bee is a tool for developling applications based on beego framework. package main @@ -61,6 +75,7 @@ var commands = []*Command{ cmdRun, cmdPack, cmdApiapp, + cmdRouter, //cmdReStart, } diff --git a/bee.json b/bee.json index b8e8a23..8b21388 100644 --- a/bee.json +++ b/bee.json @@ -2,9 +2,11 @@ "go_install": false, "dir_structure":{ "controllers": "", - "models": "" + "models": "", + "others": [] }, - "files": [ - "main.go" - ] + "main_files":{ + "main.go": "", + "others": [] + } } \ No newline at end of file diff --git a/code.go b/code.go new file mode 100644 index 0000000..5e4c3e2 --- /dev/null +++ b/code.go @@ -0,0 +1,262 @@ +// Copyright 2011 Gary Burd +// Copyright 2013 Unknown +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package main + +import ( + "go/ast" + "go/printer" + "go/scanner" + "go/token" + "math" + "strconv" +) + +const ( + notPredeclared = iota + predeclaredType + predeclaredConstant + predeclaredFunction +) + +// predeclared represents the set of all predeclared identifiers. +var predeclared = map[string]int{ + "bool": predeclaredType, + "byte": predeclaredType, + "complex128": predeclaredType, + "complex64": predeclaredType, + "error": predeclaredType, + "float32": predeclaredType, + "float64": predeclaredType, + "int16": predeclaredType, + "int32": predeclaredType, + "int64": predeclaredType, + "int8": predeclaredType, + "int": predeclaredType, + "rune": predeclaredType, + "string": predeclaredType, + "uint16": predeclaredType, + "uint32": predeclaredType, + "uint64": predeclaredType, + "uint8": predeclaredType, + "uint": predeclaredType, + "uintptr": predeclaredType, + + "true": predeclaredConstant, + "false": predeclaredConstant, + "iota": predeclaredConstant, + "nil": predeclaredConstant, + + "append": predeclaredFunction, + "cap": predeclaredFunction, + "close": predeclaredFunction, + "complex": predeclaredFunction, + "copy": predeclaredFunction, + "delete": predeclaredFunction, + "imag": predeclaredFunction, + "len": predeclaredFunction, + "make": predeclaredFunction, + "new": predeclaredFunction, + "panic": predeclaredFunction, + "print": predeclaredFunction, + "println": predeclaredFunction, + "real": predeclaredFunction, + "recover": predeclaredFunction, +} + +const ( + ExportLinkAnnotation AnnotationKind = iota + AnchorAnnotation + CommentAnnotation + PackageLinkAnnotation + BuiltinAnnotation +) + +// annotationVisitor collects annotations. +type annotationVisitor struct { + annotations []Annotation +} + +func (v *annotationVisitor) add(kind AnnotationKind, importPath string) { + v.annotations = append(v.annotations, Annotation{Kind: kind, ImportPath: importPath}) +} + +func (v *annotationVisitor) ignoreName() { + v.add(-1, "") +} + +func (v *annotationVisitor) Visit(n ast.Node) ast.Visitor { + switch n := n.(type) { + case *ast.TypeSpec: + v.ignoreName() + ast.Walk(v, n.Type) + case *ast.FuncDecl: + if n.Recv != nil { + ast.Walk(v, n.Recv) + } + v.ignoreName() + ast.Walk(v, n.Type) + case *ast.Field: + for _ = range n.Names { + v.ignoreName() + } + ast.Walk(v, n.Type) + case *ast.ValueSpec: + for _ = range n.Names { + v.add(AnchorAnnotation, "") + } + if n.Type != nil { + ast.Walk(v, n.Type) + } + for _, x := range n.Values { + ast.Walk(v, x) + } + case *ast.Ident: + switch { + case n.Obj == nil && predeclared[n.Name] != notPredeclared: + v.add(BuiltinAnnotation, "") + case n.Obj != nil && ast.IsExported(n.Name): + v.add(ExportLinkAnnotation, "") + default: + v.ignoreName() + } + case *ast.SelectorExpr: + if x, _ := n.X.(*ast.Ident); x != nil { + if obj := x.Obj; obj != nil && obj.Kind == ast.Pkg { + if spec, _ := obj.Decl.(*ast.ImportSpec); spec != nil { + if path, err := strconv.Unquote(spec.Path.Value); err == nil { + v.add(PackageLinkAnnotation, path) + if path == "C" { + v.ignoreName() + } else { + v.add(ExportLinkAnnotation, path) + } + return nil + } + } + } + } + ast.Walk(v, n.X) + v.ignoreName() + default: + return v + } + return nil +} + +func printDecl(decl ast.Node, fset *token.FileSet, buf []byte) (Code, []byte) { + v := &annotationVisitor{} + ast.Walk(v, decl) + + buf = buf[:0] + err := (&printer.Config{Mode: printer.UseSpaces, Tabwidth: 4}).Fprint(sliceWriter{&buf}, fset, decl) + if err != nil { + return Code{Text: err.Error()}, buf + } + + var annotations []Annotation + var s scanner.Scanner + fset = token.NewFileSet() + file := fset.AddFile("", fset.Base(), len(buf)) + s.Init(file, buf, nil, scanner.ScanComments) +loop: + for { + pos, tok, lit := s.Scan() + switch tok { + case token.EOF: + break loop + case token.COMMENT: + p := file.Offset(pos) + e := p + len(lit) + if p > math.MaxInt16 || e > math.MaxInt16 { + break loop + } + annotations = append(annotations, Annotation{Kind: CommentAnnotation, Pos: int16(p), End: int16(e)}) + case token.IDENT: + if len(v.annotations) == 0 { + // Oops! + break loop + } + annotation := v.annotations[0] + v.annotations = v.annotations[1:] + if annotation.Kind == -1 { + continue + } + p := file.Offset(pos) + e := p + len(lit) + if p > math.MaxInt16 || e > math.MaxInt16 { + break loop + } + annotation.Pos = int16(p) + annotation.End = int16(e) + if len(annotations) > 0 && annotation.Kind == ExportLinkAnnotation { + prev := annotations[len(annotations)-1] + if prev.Kind == PackageLinkAnnotation && + prev.ImportPath == annotation.ImportPath && + prev.End+1 == annotation.Pos { + // merge with previous + annotation.Pos = prev.Pos + annotations[len(annotations)-1] = annotation + continue loop + } + } + annotations = append(annotations, annotation) + } + } + return Code{Text: string(buf), Annotations: annotations}, buf +} + +type AnnotationKind int16 + +type Annotation struct { + Pos, End int16 + Kind AnnotationKind + ImportPath string +} + +type Code struct { + Text string + Annotations []Annotation +} + +func commentAnnotations(src string) []Annotation { + var annotations []Annotation + var s scanner.Scanner + fset := token.NewFileSet() + file := fset.AddFile("", fset.Base(), len(src)) + s.Init(file, []byte(src), nil, scanner.ScanComments) + for { + pos, tok, lit := s.Scan() + switch tok { + case token.EOF: + return annotations + case token.COMMENT: + p := file.Offset(pos) + e := p + len(lit) + if p > math.MaxInt16 || e > math.MaxInt16 { + return annotations + } + annotations = append(annotations, Annotation{Kind: CommentAnnotation, Pos: int16(p), End: int16(e)}) + } + } + return nil +} + +type sliceWriter struct{ p *[]byte } + +func (w sliceWriter) Write(p []byte) (int, error) { + *w.p = append(*w.p, p...) + return len(p), nil +} diff --git a/run.go b/run.go index f9ba02e..a386e42 100644 --- a/run.go +++ b/run.go @@ -38,11 +38,17 @@ var appname string var conf struct { // Indicates whether execute "go install" before "go build". GoInstall bool `json:"go_install"` + DirStruct struct { Controllers string Models string + Others []string // Other directories. } `json:"dir_structure"` - Files []string + + MainFiles struct { + Main string `json:"main.go"` + Others []string // Others files of package main. + } `json:"main_files"` } func runApp(cmd *Command, args []string) { @@ -60,8 +66,10 @@ func runApp(cmd *Command, args []string) { var paths []string paths = append(paths, path.Join(crupath, conf.DirStruct.Controllers), - path.Join(crupath, conf.DirStruct.Models)) - paths = append(paths, conf.Files...) + path.Join(crupath, conf.DirStruct.Models), + path.Join(crupath, conf.MainFiles.Main)) + paths = append(paths, conf.DirStruct.Others...) + paths = append(paths, conf.MainFiles.Others...) NewWatcher(paths) appname = args[0] @@ -94,5 +102,8 @@ func loadConfig() error { if len(conf.DirStruct.Models) == 0 { conf.DirStruct.Models = "models" } + if len(conf.MainFiles.Main) == 0 { + conf.MainFiles.Main = "main.go" + } return nil } diff --git a/testdata/router/router.go b/testdata/router/router.go new file mode 100644 index 0000000..beb8ba2 --- /dev/null +++ b/testdata/router/router.go @@ -0,0 +1,28 @@ +package router + +import ( + "github.com/astaxie/beego" +) + +type Router struct { + beego.Controller +} + +func (this *Router) Get() { + +} + +func (this *Router) Post() { + +} + +type Controller struct { +} + +func (this *Controller) Put() { + +} + +func (this *Controller) Delete() { + +}