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.
7 // mkmerge.go parses generated source files and merges common
8 // consts, funcs, and types into a common source file, per GOOS.
11 // $ go run mkmerge.go -out MERGED FILE [FILE ...]
14 // # Remove all common consts, funcs, and types from zerrors_linux_*.go
15 // # and write the common code into zerrors_linux.go
16 // $ go run mkmerge.go -out zerrors_linux.go zerrors_linux_*.go
18 // mkmerge.go performs the merge in the following steps:
19 // 1. Construct the set of common code that is idential in all
20 // architecture-specific files.
21 // 2. Write this common code to the merged file.
22 // 3. Remove the common code from all architecture-specific files.
45 const validGOOS = "aix|darwin|dragonfly|freebsd|linux|netbsd|openbsd|solaris"
47 // getValidGOOS returns GOOS, true if filename ends with a valid "_GOOS.go"
48 func getValidGOOS(filename string) (string, bool) {
49 matches := regexp.MustCompile(`_(` + validGOOS + `)\.go$`).FindStringSubmatch(filename)
50 if len(matches) != 2 {
53 return matches[1], true
56 // codeElem represents an ast.Decl in a comparable way.
57 type codeElem struct {
58 tok token.Token // e.g. token.CONST, token.TYPE, or token.FUNC
59 src string // the declaration formatted as source code
62 // newCodeElem returns a codeElem based on tok and node, or an error is returned.
63 func newCodeElem(tok token.Token, node ast.Node) (codeElem, error) {
65 err := format.Node(&b, token.NewFileSet(), node)
67 return codeElem{}, err
69 return codeElem{tok, b.String()}, nil
72 // codeSet is a set of codeElems
74 set map[codeElem]bool // true for all codeElems in the set
77 // newCodeSet returns a new codeSet
78 func newCodeSet() *codeSet { return &codeSet{make(map[codeElem]bool)} }
81 func (c *codeSet) add(elem codeElem) { c.set[elem] = true }
83 // has returns true if elem is in c
84 func (c *codeSet) has(elem codeElem) bool { return c.set[elem] }
86 // isEmpty returns true if the set is empty
87 func (c *codeSet) isEmpty() bool { return len(c.set) == 0 }
89 // intersection returns a new set which is the intersection of c and a
90 func (c *codeSet) intersection(a *codeSet) *codeSet {
93 for elem := range c.set {
101 // keepCommon is a filterFn for filtering the merged file with common declarations.
102 func (c *codeSet) keepCommon(elem codeElem) bool {
105 // Remove all vars from the merged file
107 case token.CONST, token.TYPE, token.FUNC, token.COMMENT:
108 // Remove arch-specific consts, types, functions, and file-level comments from the merged file
111 // Keep imports, they are handled by filterImports
115 log.Fatalf("keepCommon: invalid elem %v", elem)
119 // keepArchSpecific is a filterFn for filtering the GOARC-specific files.
120 func (c *codeSet) keepArchSpecific(elem codeElem) bool {
122 case token.CONST, token.TYPE, token.FUNC:
123 // Remove common consts, types, or functions from the arch-specific file
129 // srcFile represents a source file
130 type srcFile struct {
135 // filterFn is a helper for filter
136 type filterFn func(codeElem) bool
138 // filter parses and filters Go source code from src, removing top
139 // level declarations using keep as predicate.
140 // For src parameter, please see docs for parser.ParseFile.
141 func filter(src interface{}, keep filterFn) ([]byte, error) {
142 // Parse the src into an ast
143 fset := token.NewFileSet()
144 f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
148 cmap := ast.NewCommentMap(fset, f, f.Comments)
150 // Group const/type specs on adjacent lines
151 var groups specGroups = make(map[string]int)
155 f.Decls = f.Decls[:0]
156 for _, decl := range decls {
157 switch decl := decl.(type) {
159 // Filter imports, consts, types, vars
161 decl.Specs = decl.Specs[:0]
162 for i, spec := range specs {
163 elem, err := newCodeElem(decl.Tok, spec)
168 // Create new group if there are empty lines between this and the previous spec
169 if i > 0 && fset.Position(specs[i-1].End()).Line < fset.Position(spec.Pos()).Line-1 {
173 // Check if we should keep this spec
175 decl.Specs = append(decl.Specs, spec)
176 groups.add(elem.src, groupID)
179 // Check if we should keep this decl
180 if len(decl.Specs) > 0 {
181 f.Decls = append(f.Decls, decl)
185 elem, err := newCodeElem(token.FUNC, decl)
190 f.Decls = append(f.Decls, decl)
195 // Filter file level comments
197 commentGroups := cmap[f]
198 cmap[f] = cmap[f][:0]
199 for _, cGrp := range commentGroups {
200 if keep(codeElem{token.COMMENT, cGrp.Text()}) {
201 cmap[f] = append(cmap[f], cGrp)
205 f.Comments = cmap.Filter(f).Comments()
207 // Generate code for the filtered ast
209 if err = format.Node(&buf, fset, f); err != nil {
213 groupedSrc, err := groups.filterEmptyLines(&buf)
218 return filterImports(groupedSrc)
221 // getCommonSet returns the set of consts, types, and funcs that are present in every file.
222 func getCommonSet(files []srcFile) (*codeSet, error) {
224 return nil, fmt.Errorf("no files provided")
226 // Use the first architecture file as the baseline
227 baseSet, err := getCodeSet(files[0].src)
232 // Compare baseline set with other architecture files: discard any element,
233 // that doesn't exist in other architecture files.
234 for _, f := range files[1:] {
235 set, err := getCodeSet(f.src)
240 baseSet = baseSet.intersection(set)
245 // getCodeSet returns the set of all top-level consts, types, and funcs from src.
246 // src must be string, []byte, or io.Reader (see go/parser.ParseFile docs)
247 func getCodeSet(src interface{}) (*codeSet, error) {
250 fset := token.NewFileSet()
251 f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
256 for _, decl := range f.Decls {
257 switch decl := decl.(type) {
259 // Add const, and type declarations
260 if !(decl.Tok == token.CONST || decl.Tok == token.TYPE) {
264 for _, spec := range decl.Specs {
265 elem, err := newCodeElem(decl.Tok, spec)
273 // Add func declarations
274 elem, err := newCodeElem(token.FUNC, decl)
283 // Add file level comments
284 cmap := ast.NewCommentMap(fset, f, f.Comments)
285 for _, cGrp := range cmap[f] {
286 set.add(codeElem{token.COMMENT, cGrp.Text()})
292 // importName returns the identifier (PackageName) for an imported package
293 func importName(iSpec *ast.ImportSpec) (string, error) {
294 if iSpec.Name == nil {
295 name, err := strconv.Unquote(iSpec.Path.Value)
299 return path.Base(name), nil
301 return iSpec.Name.Name, nil
304 // specGroups tracks grouped const/type specs with a map of line: groupID pairs
305 type specGroups map[string]int
307 // add spec source to group
308 func (s specGroups) add(src string, groupID int) error {
309 srcBytes, err := format.Source(bytes.TrimSpace([]byte(src)))
313 s[string(srcBytes)] = groupID
317 // filterEmptyLines removes empty lines within groups of const/type specs.
318 // Returns the filtered source.
319 func (s specGroups) filterEmptyLines(src io.Reader) ([]byte, error) {
320 scanner := bufio.NewScanner(src)
323 var emptyLines bytes.Buffer
324 prevGroupID := -1 // Initialize to invalid group
326 line := bytes.TrimSpace(scanner.Bytes())
329 fmt.Fprintf(&emptyLines, "%s\n", scanner.Bytes())
333 // Discard emptyLines if previous non-empty line belonged to the same
334 // group as this line
335 if src, err := format.Source(line); err == nil {
336 groupID, ok := s[string(src)]
337 if ok && groupID == prevGroupID {
340 prevGroupID = groupID
343 emptyLines.WriteTo(&out)
344 fmt.Fprintf(&out, "%s\n", scanner.Bytes())
346 if err := scanner.Err(); err != nil {
349 return out.Bytes(), nil
352 // filterImports removes unused imports from fileSrc, and returns a formatted src.
353 func filterImports(fileSrc []byte) ([]byte, error) {
354 fset := token.NewFileSet()
355 file, err := parser.ParseFile(fset, "", fileSrc, parser.ParseComments)
359 cmap := ast.NewCommentMap(fset, file, file.Comments)
361 // create set of references to imported identifiers
362 keepImport := make(map[string]bool)
363 for _, u := range file.Unresolved {
364 keepImport[u.Name] = true
367 // filter import declarations
369 file.Decls = file.Decls[:0]
370 for _, decl := range decls {
371 importDecl, ok := decl.(*ast.GenDecl)
373 // Keep non-import declarations
374 if !ok || importDecl.Tok != token.IMPORT {
375 file.Decls = append(file.Decls, decl)
379 // Filter the import specs
380 specs := importDecl.Specs
381 importDecl.Specs = importDecl.Specs[:0]
382 for _, spec := range specs {
383 iSpec := spec.(*ast.ImportSpec)
384 name, err := importName(iSpec)
389 if keepImport[name] {
390 importDecl.Specs = append(importDecl.Specs, iSpec)
393 if len(importDecl.Specs) > 0 {
394 file.Decls = append(file.Decls, importDecl)
398 // filter file.Imports
399 imports := file.Imports
400 file.Imports = file.Imports[:0]
401 for _, spec := range imports {
402 name, err := importName(spec)
407 if keepImport[name] {
408 file.Imports = append(file.Imports, spec)
411 file.Comments = cmap.Filter(file).Comments()
414 err = format.Node(&buf, fset, file)
419 return buf.Bytes(), nil
422 // merge extracts duplicate code from archFiles and merges it to mergeFile.
423 // 1. Construct commonSet: the set of code that is idential in all archFiles.
424 // 2. Write the code in commonSet to mergedFile.
425 // 3. Remove the commonSet code from all archFiles.
426 func merge(mergedFile string, archFiles ...string) error {
427 // extract and validate the GOOS part of the merged filename
428 goos, ok := getValidGOOS(mergedFile)
430 return fmt.Errorf("invalid GOOS in merged file name %s", mergedFile)
433 // Read architecture files
435 for _, file := range archFiles {
436 src, err := ioutil.ReadFile(file)
438 return fmt.Errorf("cannot read archfile %s: %w", file, err)
441 inSrc = append(inSrc, srcFile{file, src})
444 // 1. Construct the set of top-level declarations common for all files
445 commonSet, err := getCommonSet(inSrc)
449 if commonSet.isEmpty() {
450 // No common code => do not modify any files
454 // 2. Write the merged file
455 mergedSrc, err := filter(inSrc[0].src, commonSet.keepCommon)
460 f, err := os.Create(mergedFile)
465 buf := bufio.NewWriter(f)
466 fmt.Fprintln(buf, "// Code generated by mkmerge.go; DO NOT EDIT.")
468 fmt.Fprintf(buf, "// +build %s\n", goos)
481 // 3. Remove duplicate declarations from the architecture files
482 for _, inFile := range inSrc {
483 src, err := filter(inFile.src, commonSet.keepArchSpecific)
487 err = ioutil.WriteFile(inFile.name, src, 0644)
496 var mergedFile string
497 flag.StringVar(&mergedFile, "out", "", "Write merged code to `FILE`")
501 var filenames []string
502 for _, arg := range flag.Args() {
503 matches, err := filepath.Glob(arg)
505 fmt.Fprintf(os.Stderr, "Invalid command line argument %q: %v\n", arg, err)
508 filenames = append(filenames, matches...)
511 if len(filenames) < 2 {
516 err := merge(mergedFile, filenames...)
518 fmt.Fprintf(os.Stderr, "Merge failed with error: %v\n", err)