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