github.com/giovannyortegon/go@v0.0.0-20220115155912-8890063f5bdd/src/pkg/mod/golang.org/x/sys@v0.0.0-20210927094055-39ccf1dd6fa6/unix/mkmerge.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build ignore
     6  // +build ignore
     7  
     8  // mkmerge.go parses generated source files and merges common
     9  // consts, funcs, and types into a common source file, per GOOS.
    10  //
    11  // Usage:
    12  //     $ go run mkmerge.go -out MERGED FILE [FILE ...]
    13  //
    14  // Example:
    15  //     # Remove all common consts, funcs, and types from zerrors_linux_*.go
    16  //     # and write the common code into zerrors_linux.go
    17  //     $ go run mkmerge.go -out zerrors_linux.go zerrors_linux_*.go
    18  //
    19  // mkmerge.go performs the merge in the following steps:
    20  // 1. Construct the set of common code that is idential in all
    21  //    architecture-specific files.
    22  // 2. Write this common code to the merged file.
    23  // 3. Remove the common code from all architecture-specific files.
    24  package main
    25  
    26  import (
    27  	"bufio"
    28  	"bytes"
    29  	"flag"
    30  	"fmt"
    31  	"go/ast"
    32  	"go/format"
    33  	"go/parser"
    34  	"go/token"
    35  	"io"
    36  	"io/ioutil"
    37  	"log"
    38  	"os"
    39  	"path"
    40  	"path/filepath"
    41  	"regexp"
    42  	"strconv"
    43  	"strings"
    44  )
    45  
    46  const validGOOS = "aix|darwin|dragonfly|freebsd|linux|netbsd|openbsd|solaris"
    47  
    48  // getValidGOOS returns GOOS, true if filename ends with a valid "_GOOS.go"
    49  func getValidGOOS(filename string) (string, bool) {
    50  	matches := regexp.MustCompile(`_(` + validGOOS + `)\.go$`).FindStringSubmatch(filename)
    51  	if len(matches) != 2 {
    52  		return "", false
    53  	}
    54  	return matches[1], true
    55  }
    56  
    57  // codeElem represents an ast.Decl in a comparable way.
    58  type codeElem struct {
    59  	tok token.Token // e.g. token.CONST, token.TYPE, or token.FUNC
    60  	src string      // the declaration formatted as source code
    61  }
    62  
    63  // newCodeElem returns a codeElem based on tok and node, or an error is returned.
    64  func newCodeElem(tok token.Token, node ast.Node) (codeElem, error) {
    65  	var b strings.Builder
    66  	err := format.Node(&b, token.NewFileSet(), node)
    67  	if err != nil {
    68  		return codeElem{}, err
    69  	}
    70  	return codeElem{tok, b.String()}, nil
    71  }
    72  
    73  // codeSet is a set of codeElems
    74  type codeSet struct {
    75  	set map[codeElem]bool // true for all codeElems in the set
    76  }
    77  
    78  // newCodeSet returns a new codeSet
    79  func newCodeSet() *codeSet { return &codeSet{make(map[codeElem]bool)} }
    80  
    81  // add adds elem to c
    82  func (c *codeSet) add(elem codeElem) { c.set[elem] = true }
    83  
    84  // has returns true if elem is in c
    85  func (c *codeSet) has(elem codeElem) bool { return c.set[elem] }
    86  
    87  // isEmpty returns true if the set is empty
    88  func (c *codeSet) isEmpty() bool { return len(c.set) == 0 }
    89  
    90  // intersection returns a new set which is the intersection of c and a
    91  func (c *codeSet) intersection(a *codeSet) *codeSet {
    92  	res := newCodeSet()
    93  
    94  	for elem := range c.set {
    95  		if a.has(elem) {
    96  			res.add(elem)
    97  		}
    98  	}
    99  	return res
   100  }
   101  
   102  // keepCommon is a filterFn for filtering the merged file with common declarations.
   103  func (c *codeSet) keepCommon(elem codeElem) bool {
   104  	switch elem.tok {
   105  	case token.VAR:
   106  		// Remove all vars from the merged file
   107  		return false
   108  	case token.CONST, token.TYPE, token.FUNC, token.COMMENT:
   109  		// Remove arch-specific consts, types, functions, and file-level comments from the merged file
   110  		return c.has(elem)
   111  	case token.IMPORT:
   112  		// Keep imports, they are handled by filterImports
   113  		return true
   114  	}
   115  
   116  	log.Fatalf("keepCommon: invalid elem %v", elem)
   117  	return true
   118  }
   119  
   120  // keepArchSpecific is a filterFn for filtering the GOARC-specific files.
   121  func (c *codeSet) keepArchSpecific(elem codeElem) bool {
   122  	switch elem.tok {
   123  	case token.CONST, token.TYPE, token.FUNC:
   124  		// Remove common consts, types, or functions from the arch-specific file
   125  		return !c.has(elem)
   126  	}
   127  	return true
   128  }
   129  
   130  // srcFile represents a source file
   131  type srcFile struct {
   132  	name string
   133  	src  []byte
   134  }
   135  
   136  // filterFn is a helper for filter
   137  type filterFn func(codeElem) bool
   138  
   139  // filter parses and filters Go source code from src, removing top
   140  // level declarations using keep as predicate.
   141  // For src parameter, please see docs for parser.ParseFile.
   142  func filter(src interface{}, keep filterFn) ([]byte, error) {
   143  	// Parse the src into an ast
   144  	fset := token.NewFileSet()
   145  	f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  	cmap := ast.NewCommentMap(fset, f, f.Comments)
   150  
   151  	// Group const/type specs on adjacent lines
   152  	var groups specGroups = make(map[string]int)
   153  	var groupID int
   154  
   155  	decls := f.Decls
   156  	f.Decls = f.Decls[:0]
   157  	for _, decl := range decls {
   158  		switch decl := decl.(type) {
   159  		case *ast.GenDecl:
   160  			// Filter imports, consts, types, vars
   161  			specs := decl.Specs
   162  			decl.Specs = decl.Specs[:0]
   163  			for i, spec := range specs {
   164  				elem, err := newCodeElem(decl.Tok, spec)
   165  				if err != nil {
   166  					return nil, err
   167  				}
   168  
   169  				// Create new group if there are empty lines between this and the previous spec
   170  				if i > 0 && fset.Position(specs[i-1].End()).Line < fset.Position(spec.Pos()).Line-1 {
   171  					groupID++
   172  				}
   173  
   174  				// Check if we should keep this spec
   175  				if keep(elem) {
   176  					decl.Specs = append(decl.Specs, spec)
   177  					groups.add(elem.src, groupID)
   178  				}
   179  			}
   180  			// Check if we should keep this decl
   181  			if len(decl.Specs) > 0 {
   182  				f.Decls = append(f.Decls, decl)
   183  			}
   184  		case *ast.FuncDecl:
   185  			// Filter funcs
   186  			elem, err := newCodeElem(token.FUNC, decl)
   187  			if err != nil {
   188  				return nil, err
   189  			}
   190  			if keep(elem) {
   191  				f.Decls = append(f.Decls, decl)
   192  			}
   193  		}
   194  	}
   195  
   196  	// Filter file level comments
   197  	if cmap[f] != nil {
   198  		commentGroups := cmap[f]
   199  		cmap[f] = cmap[f][:0]
   200  		for _, cGrp := range commentGroups {
   201  			if keep(codeElem{token.COMMENT, cGrp.Text()}) {
   202  				cmap[f] = append(cmap[f], cGrp)
   203  			}
   204  		}
   205  	}
   206  	f.Comments = cmap.Filter(f).Comments()
   207  
   208  	// Generate code for the filtered ast
   209  	var buf bytes.Buffer
   210  	if err = format.Node(&buf, fset, f); err != nil {
   211  		return nil, err
   212  	}
   213  
   214  	groupedSrc, err := groups.filterEmptyLines(&buf)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	return filterImports(groupedSrc)
   220  }
   221  
   222  // getCommonSet returns the set of consts, types, and funcs that are present in every file.
   223  func getCommonSet(files []srcFile) (*codeSet, error) {
   224  	if len(files) == 0 {
   225  		return nil, fmt.Errorf("no files provided")
   226  	}
   227  	// Use the first architecture file as the baseline
   228  	baseSet, err := getCodeSet(files[0].src)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	// Compare baseline set with other architecture files: discard any element,
   234  	// that doesn't exist in other architecture files.
   235  	for _, f := range files[1:] {
   236  		set, err := getCodeSet(f.src)
   237  		if err != nil {
   238  			return nil, err
   239  		}
   240  
   241  		baseSet = baseSet.intersection(set)
   242  	}
   243  	return baseSet, nil
   244  }
   245  
   246  // getCodeSet returns the set of all top-level consts, types, and funcs from src.
   247  // src must be string, []byte, or io.Reader (see go/parser.ParseFile docs)
   248  func getCodeSet(src interface{}) (*codeSet, error) {
   249  	set := newCodeSet()
   250  
   251  	fset := token.NewFileSet()
   252  	f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  
   257  	for _, decl := range f.Decls {
   258  		switch decl := decl.(type) {
   259  		case *ast.GenDecl:
   260  			// Add const, and type declarations
   261  			if !(decl.Tok == token.CONST || decl.Tok == token.TYPE) {
   262  				break
   263  			}
   264  
   265  			for _, spec := range decl.Specs {
   266  				elem, err := newCodeElem(decl.Tok, spec)
   267  				if err != nil {
   268  					return nil, err
   269  				}
   270  
   271  				set.add(elem)
   272  			}
   273  		case *ast.FuncDecl:
   274  			// Add func declarations
   275  			elem, err := newCodeElem(token.FUNC, decl)
   276  			if err != nil {
   277  				return nil, err
   278  			}
   279  
   280  			set.add(elem)
   281  		}
   282  	}
   283  
   284  	// Add file level comments
   285  	cmap := ast.NewCommentMap(fset, f, f.Comments)
   286  	for _, cGrp := range cmap[f] {
   287  		set.add(codeElem{token.COMMENT, cGrp.Text()})
   288  	}
   289  
   290  	return set, nil
   291  }
   292  
   293  // importName returns the identifier (PackageName) for an imported package
   294  func importName(iSpec *ast.ImportSpec) (string, error) {
   295  	if iSpec.Name == nil {
   296  		name, err := strconv.Unquote(iSpec.Path.Value)
   297  		if err != nil {
   298  			return "", err
   299  		}
   300  		return path.Base(name), nil
   301  	}
   302  	return iSpec.Name.Name, nil
   303  }
   304  
   305  // specGroups tracks grouped const/type specs with a map of line: groupID pairs
   306  type specGroups map[string]int
   307  
   308  // add spec source to group
   309  func (s specGroups) add(src string, groupID int) error {
   310  	srcBytes, err := format.Source(bytes.TrimSpace([]byte(src)))
   311  	if err != nil {
   312  		return err
   313  	}
   314  	s[string(srcBytes)] = groupID
   315  	return nil
   316  }
   317  
   318  // filterEmptyLines removes empty lines within groups of const/type specs.
   319  // Returns the filtered source.
   320  func (s specGroups) filterEmptyLines(src io.Reader) ([]byte, error) {
   321  	scanner := bufio.NewScanner(src)
   322  	var out bytes.Buffer
   323  
   324  	var emptyLines bytes.Buffer
   325  	prevGroupID := -1 // Initialize to invalid group
   326  	for scanner.Scan() {
   327  		line := bytes.TrimSpace(scanner.Bytes())
   328  
   329  		if len(line) == 0 {
   330  			fmt.Fprintf(&emptyLines, "%s\n", scanner.Bytes())
   331  			continue
   332  		}
   333  
   334  		// Discard emptyLines if previous non-empty line belonged to the same
   335  		// group as this line
   336  		if src, err := format.Source(line); err == nil {
   337  			groupID, ok := s[string(src)]
   338  			if ok && groupID == prevGroupID {
   339  				emptyLines.Reset()
   340  			}
   341  			prevGroupID = groupID
   342  		}
   343  
   344  		emptyLines.WriteTo(&out)
   345  		fmt.Fprintf(&out, "%s\n", scanner.Bytes())
   346  	}
   347  	if err := scanner.Err(); err != nil {
   348  		return nil, err
   349  	}
   350  	return out.Bytes(), nil
   351  }
   352  
   353  // filterImports removes unused imports from fileSrc, and returns a formatted src.
   354  func filterImports(fileSrc []byte) ([]byte, error) {
   355  	fset := token.NewFileSet()
   356  	file, err := parser.ParseFile(fset, "", fileSrc, parser.ParseComments)
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  	cmap := ast.NewCommentMap(fset, file, file.Comments)
   361  
   362  	// create set of references to imported identifiers
   363  	keepImport := make(map[string]bool)
   364  	for _, u := range file.Unresolved {
   365  		keepImport[u.Name] = true
   366  	}
   367  
   368  	// filter import declarations
   369  	decls := file.Decls
   370  	file.Decls = file.Decls[:0]
   371  	for _, decl := range decls {
   372  		importDecl, ok := decl.(*ast.GenDecl)
   373  
   374  		// Keep non-import declarations
   375  		if !ok || importDecl.Tok != token.IMPORT {
   376  			file.Decls = append(file.Decls, decl)
   377  			continue
   378  		}
   379  
   380  		// Filter the import specs
   381  		specs := importDecl.Specs
   382  		importDecl.Specs = importDecl.Specs[:0]
   383  		for _, spec := range specs {
   384  			iSpec := spec.(*ast.ImportSpec)
   385  			name, err := importName(iSpec)
   386  			if err != nil {
   387  				return nil, err
   388  			}
   389  
   390  			if keepImport[name] {
   391  				importDecl.Specs = append(importDecl.Specs, iSpec)
   392  			}
   393  		}
   394  		if len(importDecl.Specs) > 0 {
   395  			file.Decls = append(file.Decls, importDecl)
   396  		}
   397  	}
   398  
   399  	// filter file.Imports
   400  	imports := file.Imports
   401  	file.Imports = file.Imports[:0]
   402  	for _, spec := range imports {
   403  		name, err := importName(spec)
   404  		if err != nil {
   405  			return nil, err
   406  		}
   407  
   408  		if keepImport[name] {
   409  			file.Imports = append(file.Imports, spec)
   410  		}
   411  	}
   412  	file.Comments = cmap.Filter(file).Comments()
   413  
   414  	var buf bytes.Buffer
   415  	err = format.Node(&buf, fset, file)
   416  	if err != nil {
   417  		return nil, err
   418  	}
   419  
   420  	return buf.Bytes(), nil
   421  }
   422  
   423  // merge extracts duplicate code from archFiles and merges it to mergeFile.
   424  // 1. Construct commonSet: the set of code that is idential in all archFiles.
   425  // 2. Write the code in commonSet to mergedFile.
   426  // 3. Remove the commonSet code from all archFiles.
   427  func merge(mergedFile string, archFiles ...string) error {
   428  	// extract and validate the GOOS part of the merged filename
   429  	goos, ok := getValidGOOS(mergedFile)
   430  	if !ok {
   431  		return fmt.Errorf("invalid GOOS in merged file name %s", mergedFile)
   432  	}
   433  
   434  	// Read architecture files
   435  	var inSrc []srcFile
   436  	for _, file := range archFiles {
   437  		src, err := ioutil.ReadFile(file)
   438  		if err != nil {
   439  			return fmt.Errorf("cannot read archfile %s: %w", file, err)
   440  		}
   441  
   442  		inSrc = append(inSrc, srcFile{file, src})
   443  	}
   444  
   445  	// 1. Construct the set of top-level declarations common for all files
   446  	commonSet, err := getCommonSet(inSrc)
   447  	if err != nil {
   448  		return err
   449  	}
   450  	if commonSet.isEmpty() {
   451  		// No common code => do not modify any files
   452  		return nil
   453  	}
   454  
   455  	// 2. Write the merged file
   456  	mergedSrc, err := filter(inSrc[0].src, commonSet.keepCommon)
   457  	if err != nil {
   458  		return err
   459  	}
   460  
   461  	f, err := os.Create(mergedFile)
   462  	if err != nil {
   463  		return err
   464  	}
   465  
   466  	buf := bufio.NewWriter(f)
   467  	fmt.Fprintln(buf, "// Code generated by mkmerge.go; DO NOT EDIT.")
   468  	fmt.Fprintln(buf)
   469  	fmt.Fprintf(buf, "//go:build %s\n", goos)
   470  	fmt.Fprintf(buf, "// +build %s\n", goos)
   471  	fmt.Fprintln(buf)
   472  	buf.Write(mergedSrc)
   473  
   474  	err = buf.Flush()
   475  	if err != nil {
   476  		return err
   477  	}
   478  	err = f.Close()
   479  	if err != nil {
   480  		return err
   481  	}
   482  
   483  	// 3. Remove duplicate declarations from the architecture files
   484  	for _, inFile := range inSrc {
   485  		src, err := filter(inFile.src, commonSet.keepArchSpecific)
   486  		if err != nil {
   487  			return err
   488  		}
   489  		err = ioutil.WriteFile(inFile.name, src, 0644)
   490  		if err != nil {
   491  			return err
   492  		}
   493  	}
   494  	return nil
   495  }
   496  
   497  func main() {
   498  	var mergedFile string
   499  	flag.StringVar(&mergedFile, "out", "", "Write merged code to `FILE`")
   500  	flag.Parse()
   501  
   502  	// Expand wildcards
   503  	var filenames []string
   504  	for _, arg := range flag.Args() {
   505  		matches, err := filepath.Glob(arg)
   506  		if err != nil {
   507  			fmt.Fprintf(os.Stderr, "Invalid command line argument %q: %v\n", arg, err)
   508  			os.Exit(1)
   509  		}
   510  		filenames = append(filenames, matches...)
   511  	}
   512  
   513  	if len(filenames) < 2 {
   514  		// No need to merge
   515  		return
   516  	}
   517  
   518  	err := merge(mergedFile, filenames...)
   519  	if err != nil {
   520  		fmt.Fprintf(os.Stderr, "Merge failed with error: %v\n", err)
   521  		os.Exit(1)
   522  	}
   523  }