github.com/vugu/vugu@v0.3.5/gen/missing-fixer.go (about) 1 package gen 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "go/ast" 8 "go/parser" 9 "go/token" 10 "io" 11 "io/ioutil" 12 "os" 13 "path/filepath" 14 "sort" 15 "strings" 16 "unicode" 17 ) 18 19 // missingFixer handles generating various missing types and methods. 20 // Looks at file structure and scans for `//vugugen:` comments 21 // See https://github.com/vugu/vugu/issues/128 for more explanation and background. 22 type missingFixer struct { 23 pkgPath string // absolute path to package 24 pkgName string // short name of package from the `package` statement 25 vuguComps map[string]string // map of comp.vugu -> comp_vgen.go (all just relative base name of file, no dir) 26 outfile string // file name of output file (relative), 0_missing_vgen.go by default 27 } 28 29 func newMissingFixer(pkgPath, pkgName string, vuguComps map[string]string) *missingFixer { 30 return &missingFixer{ 31 pkgPath: pkgPath, 32 pkgName: pkgName, 33 vuguComps: vuguComps, 34 } 35 } 36 37 // run does work for this one package 38 func (mf *missingFixer) run() error { 39 40 // remove the output file if it doesn't exist, 41 // and then below we re-create it if it turns out 42 // we need it 43 _ = mf.removeOutfile() 44 45 // parse the package 46 var fset token.FileSet 47 pkgMap, err := parser.ParseDir(&fset, mf.pkgPath, nil, 0) 48 if err != nil { 49 return err 50 } 51 pkg := pkgMap[mf.pkgName] 52 if pkg == nil { 53 return fmt.Errorf("unable to find package %q after parsing dir %s", mf.pkgName, mf.pkgPath) 54 } 55 // log.Printf("pkg: %#v", pkg) 56 57 var fout *os.File 58 59 // read each _vgen.go file 60 for _, goFile := range mf.vuguComps { 61 62 // var ffset token.FileSet 63 // file, err := parser.ParseFile(&ffset, filepath.Join(mf.pkgPath, goFile), nil, 0) 64 // if err != nil { 65 // return fmt.Errorf("error while reading %s: %w", goFile, err) 66 // } 67 // ast.Print(&ffset, file.Decls) 68 69 file := fileInPackage(pkg, goFile) 70 if file == nil { 71 return fmt.Errorf("unable to find file %q in package (i.e. parse.ParseDir did not give us this file)", goFile) 72 } 73 74 compTypeName := findFileBuildMethodType(file) 75 76 // if we didn't find a build method, we don't need to do anything 77 if compTypeName == "" { 78 continue 79 } 80 81 // log.Printf("found compTypeName=%s", compTypeName) 82 83 // see if the type is already declared somewhere in the package 84 compTypeDecl := findTypeDecl(&fset, pkg, compTypeName) 85 86 // the type exists, we don't need to emit a declaration for it 87 if compTypeDecl != nil { 88 continue 89 } 90 91 // open outfile if it doesn't exist 92 if fout == nil { 93 fout, err = mf.createOutfile() 94 if err != nil { 95 return err 96 } 97 defer fout.Close() 98 } 99 100 fmt.Fprintf(fout, `// %s is a Vugu component and implements the vugu.Builder interface. 101 type %s struct {} 102 103 `, compTypeName, compTypeName) 104 105 // log.Printf("aaa compTypeName=%s, compTypeDecl=%v", compTypeName, compTypeDecl) 106 107 } 108 109 // scan all .go files for known vugugen comments 110 gcomments, err := readVugugenComments(mf.pkgPath) 111 if err != nil { 112 return err 113 } 114 115 // open outputfile if not already done above 116 if len(gcomments) > 0 { 117 if fout == nil { 118 fout, err = mf.createOutfile() 119 if err != nil { 120 return err 121 } 122 defer fout.Close() 123 } 124 } 125 126 gcommentFnames := make([]string, 0, len(gcomments)) 127 for fname := range gcomments { 128 gcommentFnames = append(gcommentFnames, fname) 129 } 130 sort.Strings(gcommentFnames) // try to get deterministic output 131 132 // for each file with vugugen comments 133 for _, fname := range gcommentFnames { 134 135 commentList := gcomments[fname] 136 sort.Strings(commentList) // try to get deterministic output 137 138 for _, c := range commentList { 139 140 c := strings.TrimSpace(c) 141 c = strings.TrimPrefix(c, "//vugugen:") 142 143 cparts := strings.Fields(c) // split by whitespace 144 145 if len(cparts) == 0 { 146 return fmt.Errorf("error parsing %s vugugen comment with no type found %q", fname, c) 147 } 148 149 switch cparts[0] { 150 151 case "event": 152 153 args := cparts[1:] 154 155 if len(args) < 1 { 156 return fmt.Errorf("error parsing %s vugugen event comment with no args %q", fname, c) 157 } 158 159 eventName := args[0] 160 161 if !unicode.IsUpper(rune(eventName[0])) { 162 return fmt.Errorf("error parsing %s vugugen event comment, event name must start with a capital letter: %q", fname, c) 163 } 164 165 opts := args[1:] 166 // isInterface := false 167 168 // try to keep the option parsing very strict, especially for now before we get this 169 // all figured out 170 /*if len(opts) == 1 && opts[0] == "interface" { 171 isInterface = true 172 } else */ 173 if len(opts) == 0 { 174 // no opts is fine 175 } else { 176 return fmt.Errorf("error parsing %s vugugen event comment unexpected options %q", fname, c) 177 } 178 179 // check for NameEvent 180 decl := findTypeDecl(&fset, pkg, eventName+"Event") 181 182 // emit type if missing as a struct wrapper around a DOMEvent 183 if decl == nil { 184 fmt.Fprintf(fout, `// %sEvent is a component event. 185 type %sEvent struct { 186 vugu.DOMEvent 187 } 188 189 `, eventName, eventName) 190 } 191 192 // check for NameHandler type, emit if missing 193 decl = findTypeDecl(&fset, pkg, eventName+"Handler") 194 if decl == nil { 195 fmt.Fprintf(fout, `// %sHandler is the interface for things that can handle %sEvent. 196 type %sHandler interface { 197 %sHandle(event %sEvent) 198 } 199 200 `, eventName, eventName, eventName, eventName, eventName) 201 } 202 203 // check for NameFunc type, emit if missing along with method and type check 204 decl = findTypeDecl(&fset, pkg, eventName+"Func") 205 if decl == nil { 206 fmt.Fprintf(fout, `// %sFunc implements %sHandler as a function. 207 type %sFunc func(event %sEvent) 208 209 // %sHandle implements the %sHandler interface. 210 func (f %sFunc) %sHandle(event %sEvent) { f(event) } 211 212 // assert %sFunc implements %sHandler 213 var _ %sHandler = %sFunc(nil) 214 215 `, eventName, eventName, eventName, eventName, eventName, eventName, eventName, eventName, eventName, eventName, eventName, eventName, eventName) 216 } 217 218 default: 219 return fmt.Errorf("error parsing %s vugugen comment with unknown type %q", fname, c) 220 } 221 222 } 223 224 } 225 226 return nil 227 } 228 229 func (mf *missingFixer) fullOutfilePath() string { 230 if mf.outfile == "" { 231 return filepath.Join(mf.pkgPath, "0_missing_vgen.go") 232 } 233 return filepath.Join(mf.pkgPath, mf.outfile) 234 } 235 236 func (mf *missingFixer) removeOutfile() error { 237 return os.Remove(mf.fullOutfilePath()) 238 } 239 240 func (mf *missingFixer) createOutfile() (*os.File, error) { 241 p := mf.fullOutfilePath() 242 fout, err := os.Create(p) 243 if err != nil { 244 return nil, fmt.Errorf("failed to create missingFixer outfile %s: %w", p, err) 245 } 246 fmt.Fprintf(fout, "package %s\n\nimport \"github.com/vugu/vugu\"\n\nvar _ vugu.DOMEvent // import fixer\n\n", mf.pkgName) 247 return fout, nil 248 } 249 250 // readVugugenComments will look in every .go file for a //vugugen: comment 251 // and return a map with file name keys and a slice of the comments found as the values. 252 // vugugen comment lines that are exactly identical will be deduplicated (even across files) 253 // as it will never be correct to generate two of the same thing in one package 254 func readVugugenComments(pkgPath string) (map[string][]string, error) { 255 fis, err := ioutil.ReadDir(pkgPath) 256 if err != nil { 257 return nil, err 258 } 259 260 foundLines := make(map[string]bool) 261 262 ret := make(map[string][]string, len(fis)) 263 for _, fi := range fis { 264 if fi.IsDir() { 265 continue 266 } 267 bname := filepath.Base(fi.Name()) 268 if !strings.HasSuffix(bname, ".go") { 269 continue 270 } 271 f, err := os.Open(filepath.Join(pkgPath, bname)) 272 if err != nil { 273 return ret, err 274 } 275 defer f.Close() 276 277 br := bufio.NewReader(f) 278 279 var fc []string 280 281 pfx := []byte("//vugugen:") 282 for { 283 line, err := br.ReadBytes('\n') 284 if err == io.EOF { 285 if len(line) == 0 { 286 break 287 } 288 } else if err != nil { 289 return ret, fmt.Errorf("missingFixer error while reading %s: %w", bname, err) 290 } 291 // ignoring whitespace 292 line = bytes.TrimSpace(line) 293 // line must start with prefix exactly 294 if !bytes.HasPrefix(line, pfx) { 295 continue 296 } 297 // and not be a duplicate 298 lineStr := string(line) 299 if foundLines[lineStr] { 300 continue 301 } 302 foundLines[lineStr] = true 303 fc = append(fc, lineStr) 304 } 305 306 if fc != nil { 307 ret[bname] = fc 308 } 309 310 } 311 return ret, nil 312 } 313 314 // fileInPackage given pkg and "blah.go" will return the file whose base name is "blah.go" 315 // (i.e. it ignores the directory part of the map key in pkg.Files) 316 // Will return nil if not found. 317 func fileInPackage(pkg *ast.Package, fileName string) *ast.File { 318 for fpath, file := range pkg.Files { 319 if filepath.Base(fpath) == fileName { 320 return file 321 } 322 } 323 return nil 324 } 325 326 // findTypeDecl looks through the package for the given type and returns 327 // the declaraction or nil if not found 328 func findTypeDecl(fset *token.FileSet, pkg *ast.Package, typeName string) ast.Decl { 329 for _, file := range pkg.Files { 330 for _, decl := range file.Decls { 331 332 // ast.Print(fset, decl) 333 334 // looking for genDecl 335 genDecl, ok := decl.(*ast.GenDecl) 336 if !ok { 337 continue 338 } 339 340 // which is a type declaration 341 if genDecl.Tok != token.TYPE { 342 continue 343 } 344 345 // with one TypeSpec 346 if len(genDecl.Specs) != 1 { 347 continue 348 } 349 spec, ok := genDecl.Specs[0].(*ast.TypeSpec) 350 if !ok { 351 continue 352 } 353 354 // with a name 355 if spec.Name == nil { 356 continue 357 } 358 359 // that matches the one we're looking for 360 if spec.Name.Name == typeName { 361 return genDecl 362 } 363 364 } 365 } 366 return nil 367 } 368 369 // findFileBuildMethodType will return "Comp" given `func (c *Root) Comp` exists in the file. 370 func findFileBuildMethodType(file *ast.File) string { 371 372 for _, decl := range file.Decls { 373 // only care about a function declaration 374 funcDecl, ok := decl.(*ast.FuncDecl) 375 if !ok { 376 continue 377 } 378 // named Build 379 if funcDecl.Name.Name != "Build" { 380 continue 381 } 382 // with exactly one receiver 383 if !(funcDecl.Recv != nil && len(funcDecl.Recv.List) == 1) { 384 continue 385 } 386 // which is a pointer 387 recv := funcDecl.Recv.List[0] 388 starExpr, ok := recv.Type.(*ast.StarExpr) 389 if !ok { 390 continue 391 } 392 // to an identifier 393 xident, ok := starExpr.X.(*ast.Ident) 394 if !ok { 395 continue 396 } 397 // whose name is the component type we're after 398 return xident.Name 399 } 400 401 return "" 402 }