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  }