| 1 | package main |
|---|---|
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "go/build" |
| 7 | "go/types" |
| 8 | "io" |
| 9 | "log" |
| 10 | "net/http" |
| 11 | "os" |
| 12 | "path/filepath" |
| 13 | "strings" |
| 14 | |
| 15 | "golang.org/x/tools/go/callgraph" |
| 16 | "golang.org/x/tools/go/callgraph/cha" |
| 17 | "golang.org/x/tools/go/callgraph/rta" |
| 18 | "golang.org/x/tools/go/callgraph/static" |
| 19 | |
| 20 | "golang.org/x/tools/go/packages" |
| 21 | "golang.org/x/tools/go/pointer" |
| 22 | "golang.org/x/tools/go/ssa" |
| 23 | "golang.org/x/tools/go/ssa/ssautil" |
| 24 | ) |
| 25 | |
| 26 | type CallGraphType string |
| 27 | |
| 28 | const ( |
| 29 | CallGraphTypeStatic = "static" |
| 30 | CallGraphTypeCha = "cha" |
| 31 | CallGraphTypeRta = "rta" |
| 32 | CallGraphTypePointer CallGraphType = "pointer" |
| 33 | ) |
| 34 | |
| 35 | //==[ type def/func: analysis ]=============================================== |
| 36 | type renderOpts struct { |
| 37 | cacheDir string |
| 38 | focus string |
| 39 | group []string |
| 40 | ignore []string |
| 41 | include []string |
| 42 | limit []string |
| 43 | nointer bool |
| 44 | refresh bool |
| 45 | nostd bool |
| 46 | algo CallGraphType |
| 47 | } |
| 48 | |
| 49 | // mainPackages returns the main packages to analyze. |
| 50 | // Each resulting package is named "main" and has a main function. |
| 51 | func mainPackages(pkgs []*ssa.Package) ([]*ssa.Package, error) { |
| 52 | var mains []*ssa.Package |
| 53 | for _, p := range pkgs { |
| 54 | if p != nil && p.Pkg.Name() == "main" && p.Func("main") != nil { |
| 55 | mains = append(mains, p) |
| 56 | } |
| 57 | } |
| 58 | if len(mains) == 0 { |
| 59 | return nil, fmt.Errorf("no main packages") |
| 60 | } |
| 61 | return mains, nil |
| 62 | } |
| 63 | |
| 64 | //==[ type def/func: analysis ]=============================================== |
| 65 | type analysis struct { |
| 66 | opts *renderOpts |
| 67 | prog *ssa.Program |
| 68 | pkgs []*ssa.Package |
| 69 | mainPkg *ssa.Package |
| 70 | callgraph *callgraph.Graph |
| 71 | } |
| 72 | |
| 73 | var Analysis *analysis |
| 74 | |
| 75 | func (a *analysis) DoAnalysis( |
| 76 | algo CallGraphType, |
| 77 | dir string, |
| 78 | tests bool, |
| 79 | args []string, |
| 80 | ) error { |
| 81 | cfg := &packages.Config{ |
| 82 | Mode: packages.LoadAllSyntax, |
| 83 | Tests: tests, |
| 84 | Dir: dir, |
| 85 | BuildFlags: build.Default.BuildTags, |
| 86 | } |
| 87 | |
| 88 | initial, err := packages.Load(cfg, args...) |
| 89 | if err != nil { |
| 90 | return err |
| 91 | } |
| 92 | |
| 93 | if packages.PrintErrors(initial) > 0 { |
| 94 | return fmt.Errorf("packages contain errors") |
| 95 | } |
| 96 | |
| 97 | // Create and build SSA-form program representation. |
| 98 | prog, pkgs := ssautil.AllPackages(initial, 0) |
| 99 | prog.Build() |
| 100 | |
| 101 | var graph *callgraph.Graph |
| 102 | var mainPkg *ssa.Package |
| 103 | |
| 104 | switch algo { |
| 105 | case CallGraphTypeStatic: |
| 106 | graph = static.CallGraph(prog) |
| 107 | case CallGraphTypeCha: |
| 108 | graph = cha.CallGraph(prog) |
| 109 | case CallGraphTypeRta: |
| 110 | mains, err := mainPackages(prog.AllPackages()) |
| 111 | if err != nil { |
| 112 | return err |
| 113 | } |
| 114 | var roots []*ssa.Function |
| 115 | mainPkg = mains[0] |
| 116 | for _, main := range mains { |
| 117 | roots = append(roots, main.Func("main")) |
| 118 | } |
| 119 | graph = rta.Analyze(roots, true).CallGraph |
| 120 | case CallGraphTypePointer: |
| 121 | mains, err := mainPackages(prog.AllPackages()) |
| 122 | if err != nil { |
| 123 | return err |
| 124 | } |
| 125 | mainPkg = mains[0] |
| 126 | config := &pointer.Config{ |
| 127 | Mains: mains, |
| 128 | BuildCallGraph: true, |
| 129 | } |
| 130 | ptares, err := pointer.Analyze(config) |
| 131 | if err != nil { |
| 132 | return err |
| 133 | } |
| 134 | graph = ptares.CallGraph |
| 135 | default: |
| 136 | return fmt.Errorf("invalid call graph type: %s", a.opts.algo) |
| 137 | } |
| 138 | |
| 139 | //cg.DeleteSyntheticNodes() |
| 140 | |
| 141 | a.prog = prog |
| 142 | a.pkgs = pkgs |
| 143 | a.mainPkg = mainPkg |
| 144 | a.callgraph = graph |
| 145 | return nil |
| 146 | } |
| 147 | |
| 148 | func (a *analysis) OptsSetup() { |
| 149 | a.opts = &renderOpts{ |
| 150 | cacheDir: *cacheDir, |
| 151 | focus: *focusFlag, |
| 152 | group: []string{*groupFlag}, |
| 153 | ignore: []string{*ignoreFlag}, |
| 154 | include: []string{*includeFlag}, |
| 155 | limit: []string{*limitFlag}, |
| 156 | nointer: *nointerFlag, |
| 157 | nostd: *nostdFlag, |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | func (a *analysis) ProcessListArgs() (e error) { |
| 162 | var groupBy []string |
| 163 | var ignorePaths []string |
| 164 | var includePaths []string |
| 165 | var limitPaths []string |
| 166 | |
| 167 | for _, g := range strings.Split(a.opts.group[0], ",") { |
| 168 | g := strings.TrimSpace(g) |
| 169 | if g == "" { |
| 170 | continue |
| 171 | } |
| 172 | if g != "pkg" && g != "type" { |
| 173 | e = errors.New("invalid group option") |
| 174 | return |
| 175 | } |
| 176 | groupBy = append(groupBy, g) |
| 177 | } |
| 178 | |
| 179 | for _, p := range strings.Split(a.opts.ignore[0], ",") { |
| 180 | p = strings.TrimSpace(p) |
| 181 | if p != "" { |
| 182 | ignorePaths = append(ignorePaths, p) |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | for _, p := range strings.Split(a.opts.include[0], ",") { |
| 187 | p = strings.TrimSpace(p) |
| 188 | if p != "" { |
| 189 | includePaths = append(includePaths, p) |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | for _, p := range strings.Split(a.opts.limit[0], ",") { |
| 194 | p = strings.TrimSpace(p) |
| 195 | if p != "" { |
| 196 | limitPaths = append(limitPaths, p) |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | a.opts.group = groupBy |
| 201 | a.opts.ignore = ignorePaths |
| 202 | a.opts.include = includePaths |
| 203 | a.opts.limit = limitPaths |
| 204 | |
| 205 | return |
| 206 | } |
| 207 | |
| 208 | func (a *analysis) OverrideByHTTP(r *http.Request) { |
| 209 | if f := r.FormValue("f"); f == "all" { |
| 210 | a.opts.focus = "" |
| 211 | } else if f != "" { |
| 212 | a.opts.focus = f |
| 213 | } |
| 214 | if std := r.FormValue("std"); std != "" { |
| 215 | a.opts.nostd = false |
| 216 | } |
| 217 | if inter := r.FormValue("nointer"); inter != "" { |
| 218 | a.opts.nointer = true |
| 219 | } |
| 220 | if refresh := r.FormValue("refresh"); refresh != "" { |
| 221 | a.opts.refresh = true |
| 222 | } |
| 223 | if g := r.FormValue("group"); g != "" { |
| 224 | a.opts.group[0] = g |
| 225 | } |
| 226 | if l := r.FormValue("limit"); l != "" { |
| 227 | a.opts.limit[0] = l |
| 228 | } |
| 229 | if ign := r.FormValue("ignore"); ign != "" { |
| 230 | a.opts.ignore[0] = ign |
| 231 | } |
| 232 | if inc := r.FormValue("include"); inc != "" { |
| 233 | a.opts.include[0] = inc |
| 234 | } |
| 235 | return |
| 236 | } |
| 237 | |
| 238 | // basically do printOutput() with previously checking |
| 239 | // focus option and respective package |
| 240 | func (a *analysis) Render() ([]byte, error) { |
| 241 | var ( |
| 242 | err error |
| 243 | ssaPkg *ssa.Package |
| 244 | focusPkg *types.Package |
| 245 | ) |
| 246 | |
| 247 | if a.opts.focus != "" { |
| 248 | if ssaPkg = a.prog.ImportedPackage(a.opts.focus); ssaPkg == nil { |
| 249 | if strings.Contains(a.opts.focus, "/") { |
| 250 | return nil, fmt.Errorf("focus failed: %v", err) |
| 251 | } |
| 252 | // try to find package by name |
| 253 | var foundPaths []string |
| 254 | for _, p := range a.pkgs { |
| 255 | if p.Pkg.Name() == a.opts.focus { |
| 256 | foundPaths = append(foundPaths, p.Pkg.Path()) |
| 257 | } |
| 258 | } |
| 259 | if len(foundPaths) == 0 { |
| 260 | return nil, fmt.Errorf("focus failed, could not find package: %v", a.opts.focus) |
| 261 | } else if len(foundPaths) > 1 { |
| 262 | for _, p := range foundPaths { |
| 263 | fmt.Fprintf(os.Stderr, " - %s\n", p) |
| 264 | } |
| 265 | return nil, fmt.Errorf("focus failed, found multiple packages with name: %v", a.opts.focus) |
| 266 | } |
| 267 | // found single package |
| 268 | if ssaPkg = a.prog.ImportedPackage(foundPaths[0]); ssaPkg == nil { |
| 269 | return nil, fmt.Errorf("focus failed: %v", err) |
| 270 | } |
| 271 | } |
| 272 | focusPkg = ssaPkg.Pkg |
| 273 | logf("focusing: %v", focusPkg.Path()) |
| 274 | } |
| 275 | |
| 276 | dot, err := printOutput( |
| 277 | a.prog, |
| 278 | a.mainPkg, |
| 279 | a.callgraph, |
| 280 | focusPkg, |
| 281 | a.opts.limit, |
| 282 | a.opts.ignore, |
| 283 | a.opts.include, |
| 284 | a.opts.group, |
| 285 | a.opts.nostd, |
| 286 | a.opts.nointer, |
| 287 | ) |
| 288 | if err != nil { |
| 289 | return nil, fmt.Errorf("processing failed: %v", err) |
| 290 | } |
| 291 | |
| 292 | return dot, nil |
| 293 | } |
| 294 | |
| 295 | func (a *analysis) FindCachedImg() string { |
| 296 | if a.opts.cacheDir == "" || a.opts.refresh { |
| 297 | return "" |
| 298 | } |
| 299 | |
| 300 | focus := a.opts.focus |
| 301 | if focus == "" { |
| 302 | focus = "all" |
| 303 | } |
| 304 | focusFilePath := focus + "." + *outputFormat |
| 305 | absFilePath := filepath.Join(a.opts.cacheDir, focusFilePath) |
| 306 | |
| 307 | if exists, err := pathExists(absFilePath); err != nil || !exists { |
| 308 | log.Println("not cached img:", absFilePath) |
| 309 | return "" |
| 310 | } |
| 311 | |
| 312 | log.Println("hit cached img") |
| 313 | return absFilePath |
| 314 | } |
| 315 | |
| 316 | func (a *analysis) CacheImg(img string) error { |
| 317 | if a.opts.cacheDir == "" || img == "" { |
| 318 | return nil |
| 319 | } |
| 320 | |
| 321 | focus := a.opts.focus |
| 322 | if focus == "" { |
| 323 | focus = "all" |
| 324 | } |
| 325 | absCacheDirPrefix := filepath.Join(a.opts.cacheDir, focus) |
| 326 | absCacheDirPath := strings.TrimRightFunc(absCacheDirPrefix, func(r rune) bool { |
| 327 | return r != '\\' && r != '/' |
| 328 | }) |
| 329 | err := os.MkdirAll(absCacheDirPath, os.ModePerm) |
| 330 | if err != nil { |
| 331 | return err |
| 332 | } |
| 333 | |
| 334 | absFilePath := absCacheDirPrefix + "." + *outputFormat |
| 335 | _, err = copyFile(img, absFilePath) |
| 336 | if err != nil { |
| 337 | return err |
| 338 | } |
| 339 | |
| 340 | return nil |
| 341 | } |
| 342 | |
| 343 | func pathExists(path string) (bool, error) { |
| 344 | _, err := os.Stat(path) |
| 345 | if err == nil { |
| 346 | return true, nil |
| 347 | } |
| 348 | if os.IsNotExist(err) { |
| 349 | return false, nil |
| 350 | } |
| 351 | return false, err |
| 352 | } |
| 353 | |
| 354 | func copyFile(src, dst string) (int64, error) { |
| 355 | sourceFileStat, err := os.Stat(src) |
| 356 | |
| 357 | if err != nil { |
| 358 | return 0, err |
| 359 | } |
| 360 | |
| 361 | if !sourceFileStat.Mode().IsRegular() { |
| 362 | return 0, fmt.Errorf("%s is not a regular file", src) |
| 363 | } |
| 364 | |
| 365 | source, err := os.Open(src) |
| 366 | if err != nil { |
| 367 | return 0, err |
| 368 | } |
| 369 | defer source.Close() |
| 370 | |
| 371 | destination, err := os.Create(dst) |
| 372 | if err != nil { |
| 373 | return 0, err |
| 374 | } |
| 375 | defer destination.Close() |
| 376 | nBytes, err := io.Copy(destination, source) |
| 377 | return nBytes, err |
| 378 | } |
| 379 |
Members