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  }